言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

アメリカでUberにiPhone置き忘れて無事に回収した。SIMは通話付きを買おうな!

[2018/06/13 追記しました]

事の顛末

一週間くらい出張でUS(CA, Palo Alto)に来ている。Palo AltoはSan Fransiscoと異なり移動は車が基本になる。なので出勤はUberを利用しているのだが、車内にiPhoneを置き忘れる事件が発生。先日のGoogle I/OiPhoneを置き忘れた話を見かけたがまさか自分が同じ事になるとは思わなかった 😇

azihsoyn.hatenablog.com

私がラッキーだったのは、メインで使っていた端末が無くしたiPhoneではなく、別のAndroid端末だった事。日本から持って来た端末は2台だが、現地でSIMを1枚だけ買ってAndroidの方に挿して使っていた。つまり、iPhoneを無くしても常にネットワークに接続できるし、帰りのUberも呼べるしで緊急で困ることはなかった。あとiPhoneはプライベートで使っていただけだったので会社の情報とかも入っていなかったので一安心。とはいえまたiPhone買い直して$800とか払うのもバカバカしいので取り戻す事にした。ちなみにiPhoneはSIMさしてないとオフラインなので「iPhoneを探す」が使えない😇😇

Uberアプリは無くしたものを取り戻すのも便利。ただし電話番号とSMSが必須。

ドライバーとなんとか連絡をとりたくてUberアプリを色々さわる。乗車情報より「持ち物を紛失した」のメニューを発見。ここまではいいんだけど、Uberはプライバシーとかの観点からかドライバーと乗客が直接やりとりするのを極力減らす設計になっており、Uberを経由した電話で連絡を取る事になる。

どういうことかと言うと、こちらが連絡してほしい番号をUberにアプリ経由で送信し、Uberがドライバーに連絡するのだが、ドライバーはUberから連絡された番号にかけても直接乗客の番号に繋がるのではなくUberの番号に繋がり、そこから自動で通話が転送される。通話終了後、どちらかが掛け直しても相手に繋がらないような仕組みになっていた。

私の場合は電話番号を送信したらすぐにかかって来た。1 "Are you an Uber driver?"って聞いたらそうだって言われたので、「自分が今朝の乗客であること 」と「iPhoneを無くしたらしいこと」を伝えると、「今日は何人か乗せたから誰かわかんないけど、とりあえずiPhoneには気がつかなかった。でも調べてまた折り返し連絡するね」って言ってくれて通話終了。この時点では気がついてなかったけど、向こうから折り返すことがUberの仕組み上できなかった。

ずっと待ってても連絡が来ないのでどうしたもんかなぁと次の日の朝周りに相談したら「アメリカはグイグイpushしないと無くしたものは戻って来ない」と言われたのでPushすることにする。

もう一度同じメニューから連絡をとってみたけど今度は繋がらず。別の問い合わせ先を探していたら "I couldn't reach my driver about a lost item" 2 があったのでこちらからも連絡してみる事にする。先ほどと同様電話番号を送信する仕組みになっていた。今度はサポートからメッセージが来て、「ドライバーと連絡がついてiPhone見つかったらしい。向こうから連絡くると思うからちょっと待っててくれ」3 と言われたので待つ。

しばらくするとSMSがドライバーより届いて、「どこに住んでるの?」と聞かれて住所を教えると「$20でそこまで持っていくか、San JoseのUberのオフィスに預けるかどっちがいいか選んで」と言われたので持って来てもらう事にした。そこから時間とかのやりとりを全てSMS上で行い、その日の晩4にドライバーが来てくれて無事iPhoneを受け取った。彼曰く、「本当はもっと早く(最初の電話で約束した通り)掛け直してあげたかったんだけど繋がらなかったんだよねー」とのこと。ちなみに受け取る時に$20をキャッシュで支払ったのだが、どうやらUberを通じて返すと$15がアカウントに課金されるので$5で私はSan Joseにいく手間と時間を買って、ドライバーは$20をポケットに入れることができたっぽい。5

[2018/06/13 追記 ここから]
後日Uberのサポートから「落し物見つかったって聞いたから$15がライド料金に含まれるように調整しておいたよ、$15は全額ドライバーに支払われるよ」という連絡が来ました。
[2018/06/13 追記 ここまで]

タイムライン

  • 6/6 9:00
    • 出勤途中のUberiPhoneを置き忘れる。多分上記のブログと同じくポケットから落ちたのに気がつかないまま降りてしまったと思われる。
    • 出勤してiPhoneがない事に気がつくも、部屋に置き忘れたのかも?と思って特に何もせず
  • 6/6 18:30
    • 帰宅。iPhoneがないか部屋中探すが見つからず。Paring済みのBluetooth earphoneで端末を探すの結構便利だった。
  • 6/6 19:00
    • AndroidUberのアプリを立ち上げヘルプメニューになんかないか探し始める。乗車情報より「持ち物を紛失した」のメニューを発見
    • ドライバーと電話で話す。車内をチェックして折り返し連絡をくれることに
  • 6/7 9:00
    • 一晩明けてもドライバーから連絡がないので再度連絡を試みるが繋がらない
    • Uberサポートに連絡
    • ドライバーからSMSが来て$20で持って来てもらうことを約束する
  • 6/7 20:00
    • ドライバーからiPhoneを受け取る

雑感

  • 今回の出張でたまたまSIMは通話付きのもの6を買っていたのでとてもスムーズにいった。通話なしのSIMを買っていたら相当面倒だったと思う。
    • 日本だとSMSはMFAくらいにしか使っていないけど意外と他の国では使われるのだろうか。
  • Uberのドライバーと乗客を直接繋がないプライバシーに配慮したサービス設計は参考になった。
  • 今回ドライバーに私た$20が出張中支払った唯一のキャッシュだった。念のため空港で換金しておいてよかった


言いたいことは以上です。


  1. 相手もどういう感じでかけてるかわからないので、"Hello?“ "Hello?"ってお互い何度か言う羽目になってしまった。コントか。

  2. この辺なぜか英語で表示されている。ローカライズ頑張れ!とどうしてもアプリ開発者目線でみてしまう。

  3. 実際はおきまりの「Sorry to hear about your phone」で始まる丁寧でながーい文章が送られて来た。

  4. 晩といってもサマータイムで20:00でも明るくてびっくりした。

  5. Uberが受け取る$15のうち、いくらドライバーに渡るかは不明。

  6. San FransiscoのAT&Tの店舗で通話付きSIMを買った。店員のお姉さんがすごいネイルで器用にSIMを扱っていたのが印象的だった。

TransitionのShowcaseアプリを作って公開しました

はじめに

先日droid girls meetupに参加し、Animationのハンズオンを楽しんできた。 GoogleのオフィスでGooglerの方(Support libraryを作っている方)が講師というなんとも贅沢な回だった。

droidgirls.connpass.com

そういえば業務でがっつりtransitionをさわる機会がないなぁと思っていたので折角なので色々試してみた。 ついでに、

そういえばこんなことをつぶやいていたのでShowcase的なアプリにして公開することにした。

Source code

コードはこちら。

github.com

サンプルの中にソースコードgithubページに飛べるボタンを置いておいたので、ここどうやって実装してるのかな?って思ったらすぐコードを確認できるようにしておいたのでよければみて見て下さい。

とはいうもののあまりまだサンプルを作れていなくて、作ったのは下記の3つ。

  • ObjectAnimator(Scale)
  • Arc Motion Transition
  • Shared element Activity Transition

それぞれについて気になったポイントとかハマったポイントを書き残しておく。

ObjectAnimator(Scale)

連続するanimationを記述するときに ktxのAnimatorクラスの拡張関数が便利だった。ただしsupport libraryのtransitionとかに対する拡張関数は生えてないようなので実はそんなに使い所は多くないかもしれない。

Arc Motion Transition

最初 ArcMotion() を一次元上での移動に対して設定しており、カーブを描くanimationにならずにハマった。ArcMotionのコードを読んでいるとどうやらベジエ曲線を軌跡とするため、一次元だと曲線にならないっぽい。(一次ベジエ曲線は単なる線分なので。)
余談だけどこれのおかげで TransitionManager 周りのコードを読んで`TransitionManager#beginDelayedTransition()`が何をやっているか理解できた。sceneChangeRunTransition() のなかで対象のViewGroupのViewTreeObserverを登録しておき、view parameterが変わったりしてそのViewGroupにlayoutが走った時とかに onPreDraw() 等を検知して登録したtransitionに対して playTransition() をコールする仕組みだった。へー。

Shared element Activity Transition

Shared element Activity Transition自体は思ったよりかなり簡単にできたが、全く関係のない別の箇所でハマった。 CoordinatorLayoutの中に入れるScrollできるViewはRecyclerViewNestedScrollVIewしかダメっぽい。普通のScrollViewを入れてもAppBarLayoutはcollapsedされないし、AppBarLayoutScrollViewが重なってしまう。これ毎回忘れて毎回ハマる気がする…




もっといろんなanimation作ってどんどん足して行きたいなー

Toolbarにセットしたheightが無視されるケース

まずはこちらのコード、どのような表示になるか考えながら読んでみてください。
(タイトルでネタバレしている気がするけど気にしないで!)

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?android:actionBarSize"
        android:background="?android:colorPrimary" />

    <!-- 省略 -->

</android.support.v4.widget.DrawerLayout>

この場合、以下のような表示になります。
(確認環境:Pixel 2 API 27 Emulator)

f:id:muumuumuumuu:20180424220309p:plain:w200

Toolbarのheightを指定しているにも関わらず親のheightいっぱいに表示されてしまいます。 ちなみこんな感じで、

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="?android:actionBarSize">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?android:actionBarSize"
            android:background="?android:colorPrimary" />
    </LinearLayout>

高さを android:actionBarSize に指定したViewGroupで囲ってやると期待通りの表示になります。

f:id:muumuumuumuu:20180424221150p:plain:w200

これはなかなか興味深い挙動になっています。それではToolbarのコードを読んでどうしてこうなったのか見ていきましょう。

Toolbarのソースコードを読む

Support LibraryのToolbarのコードで、自分の高さを計算しているのはこの辺りです。

Cross Reference: /frameworks/support/v7/appcompat/src/android/support/v7/widget/Toolbar.java

読んでいくと、「NavigationViewは出すのか」「Menuは表示するのか」「ロゴは表示するのか」などで細かい計算ロジックが入っていて面白いです。
最後の最後にView.resolveSizeAndState() が呼ばれます。ここに来る時点でheightgetSuggestedMinimumHeight()も147なのですが、View.resolveSizeAndState()の結果measuredHeightは一気に1731になっていました。

    140 public class Toolbar extends ViewGroup {

   1567     @Override
   1568     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

// 省略

   1694         final int measuredHeight = View.resolveSizeAndState(
   1695                 Math.max(height, getSuggestedMinimumHeight()),
   1696                 heightMeasureSpec, childState << View.MEASURED_HEIGHT_STATE_SHIFT);
   1697 
   1698         setMeasuredDimension(measuredWidth, shouldCollapse() ? 0 : measuredHeight);
   1699     }

ここで渡っているheightMesureSpecですが、modeは EXACTLY, sizeは1731になっています。ViewGroupで囲んだときは、modeは 同様にEXACTLYですが、sizeは親のViewGroupの高さ( android:actionBarSizeを指定しているので147)になっていました。ViewGroupで囲まない場合はなぜこんなに大きなsizeになっているのか?

MesureSpecのsizeの計算ロジックはこちら。

   24587     public static class MeasureSpec {
   24588         private static final int MODE_SHIFT = 30;
   24589         private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

   24678         public static int getSize(int measureSpec) {
   24679             return (measureSpec & ~MODE_MASK);
   24680         }

びっくりするくらい特別なことはしていません。measureSpecからMODEを表現する部分をbit演算で削っているだけ。

肝心のresolveSizeAndState()のコードはこちら。

   22230     public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
   22231         final int specMode = MeasureSpec.getMode(measureSpec);
   22232         final int specSize = MeasureSpec.getSize(measureSpec);
   22233         final int result;
   22234         switch (specMode) {
   22235             case MeasureSpec.AT_MOST:
   22236                 if (specSize < size) {
   22237                     result = specSize | MEASURED_STATE_TOO_SMALL;
   22238                 } else {
   22239                     result = size;
   22240                 }
   22241                 break;
   22242             case MeasureSpec.EXACTLY:
   22243                 result = specSize;
   22244                 break;
   22245             case MeasureSpec.UNSPECIFIED:
   22246             default:
   22247                 result = size;
   22248         }
   22249         return result | (childMeasuredState & MEASURED_STATE_MASK);
   22250     }
   22251 

このように、modeがEXACTLYの場合はspecSizeが採用されます。mode(というかmeasureSpec)はparent viewから渡って来るものなので、Toolbarとしては特定のparentの中に入れられた場合に自分のheightの指定を無視してmeasureするというコードになっています。
したがって高さを指定したViewGroupで囲ってやるとその高さで表示されていたわけですね。おもしろい!

余談ですが今回親となったSupport LibraryのDrawerLayoutがchildに対してmeasureの要求を出すコードはこんな感じです。
sizeは自身の高さからmarginを引いたもの、modeはEXACTLYを指定したMeasureSpecをchildに渡してmeasure要求を出しています。

    968     @Override
    969     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

// 省略

   1054             if (isContentView(child)) {
   1055                 // Content views get measured at exactly the layout's size.
   1056                 final int contentWidthSpec = MeasureSpec.makeMeasureSpec(
   1057                         widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
   1058                 final int contentHeightSpec = MeasureSpec.makeMeasureSpec(
   1059                         heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
   1060                 child.measure(contentWidthSpec, contentHeightSpec);


自前でToolbarをセットしたくなるケースは少なくなってきていますが、まだまだToolbarを使わないとできないこともあるのでご注意ください。
そういえばDrawerLayoutでActionbarにハンバーガーアイコン置きたい場合もToolbarじゃないとダメだった気がする。

NestedScrollViewの中のRecyclerViewを配置すると要素全てメモリ上にallocateされて困った話

長いタイトルが全てを表していて、NestedScrollViewの中にRecyclerViewを配置した場合、要素全てメモリ上にallocateされて困った話。そのまんま。

こんな感じでNestedScrollViewの中にRecyclerViewを置いた場合

<NestedScrollView>
    <RelativeLayout>
        ....
        <RecyclerView />
    </RelativeLayout>
</NestedScrollView>

例えば30個RecyclerViewが表示すべきItemがあるとする。そのうち画面に表示されるのは6個だったとして、メモリ上に展開されるItemの個数は当然6個を期待するところだが、実際Android Profilerで見てみると30個allocateされる。30個だったらまだいいのだが、「スクロールしてbottomまで表示するとサーバに次の要素を問い合わせて永遠に表示していく」みたいなことをやりたいと困る。

ちなみにこんな感じで、NestedScrollViewの代わりにScrollViewにした場合だと期待通り6個分allocateされる。

<ScrollView>
    <RelativeLayout>
        ....
        <RecyclerView />
    </RelativeLayout>
</ScrollView>

じゃあどうする?ってなると思うけど、NestedScrollViewをやめる以外のいい解決策が今の所思いつかない。おそらくこういうデザインをしている場合、RecyclerView以外にもスクロースする要素を入れたいというケースだと思うので、それらもRecyclerViewの要素として扱うしかないんじゃないかな。




なんでこんなことになるのか、せっかくなのでなのでNestedScrollViewRecyclerView周りのコードを読んでみた。

RecyclerViewがどうやって要素をlayoutしていくかはこちらの資料が詳しいので色々省略

www.slideshare.net

問題はRecyclerViewの中をどんどん埋めていくこちらのメソッド

Cross Reference: /frameworks/support/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java

/**
 * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
 * independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager}
 * and with little change, can be made publicly available as a helper class.
 *
 * @param recycler        Current recycler that is attached to RecyclerView
 * @param layoutState     Configuration on how we should fill out the available space.
 * @param state           Context passed by the RecyclerView to control scroll steps.
 * @param stopOnFocusable If true, filling stops in the first focusable new child
 * @return Number of pixels that it added. Useful for scroll functions.
 */
 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
     RecyclerView.State state, boolean stopOnFocusable) {
     // max offset we should set is mFastScroll + available
     final int start = layoutState.mAvailable;
     if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
         // TODO ugly bug fix. should not happen
         if (layoutState.mAvailable < 0) {
             layoutState.mScrollingOffset += layoutState.mAvailable;
         }
         recycleByLayoutState(recycler, layoutState);
     }
     int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
     LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
     while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
         layoutChunkResult.resetInternal();
         if (VERBOSE_TRACING) {
             TraceCompat.beginSection("LLM LayoutChunk");
         }
         layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 以降も続くが省略

The magic functions :)ってコメント可愛い
ここのwhile文でlayoutState.mInfiniteがtrueになっている。layoutState.hasMore(state)はまだ描画すべきItemが残っているかどうかのフラグなので、最後のlayoutChunkが走ってしまっている様子。 ではlayoutState.mInfiniteはどこから来るのかというと、同じくLinearLayoutManagerのここ。

    boolean resolveIsInfinite() {
        return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
                && mOrientationHelper.getEnd() == 0;
    }

うーん、MeasureSpec.UNSPECIFIEDになっている。MeasureSpecについてはこちらが詳しいです。

seto-hi.hatenablog.com

親がmeasureを呼ぶ時に適切なMeasureSpecを引数として渡していたらUNSPECIFIEDにならないのでは?と思いNestedScrollViewのコードをみにいく。

Cross Reference: /frameworks/support/core-ui/java/android/support/v4/widget/NestedScrollView.java

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mFillViewport) {
            return;
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }

        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            int height = getMeasuredHeight();
            if (child.getMeasuredHeight() < height) {
                final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();

                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeft() + getPaddingRight(), lp.width);
                height -= getPaddingTop();
                height -= getPaddingBottom();
                int childHeightMeasureSpec =
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

ここでheightModeMeasureSpec.EXACTLYになっているのだが、

if (child.getMeasuredHeight() < height) {

ここの条件式でchildのheightの方が大きくなってしまっているため、if文の中のchildのmeasureが呼ばれていなかった。

じゃあどこでchildのmeasureを呼んでるのかな〜〜と思ってNestedScrollView#measureChildWithMargins()を見に行ったら高さはMeasureSpec.UNSPECIFIEDを指定していた。

Cross Reference: /frameworks/support/core-ui/java/android/support/v4/widget/NestedScrollView.java

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

ここでUNSPECIFIEDじゃないものを渡すとどうなるのかな?と思って実験。NestedScrollViewを拡張した独自クラスを作って、このメソッドをoverrideしてみる。 UNSPECIFIEDEXACTLYに変えて見たらallocateされる個数が30個から14個まで減った。どういうロジックで14個になるかまではわからず…

というわけでまとめると、

  1. NestedScrollView#onMeasure()がコールされる
  2. NestedScrollView#measureChildWithMargins() がコールされるが、ここでchildHeightMeasureSpecが0 (UNSPECIFIED)としてchild.measure()がコールされる
  3. childであるRecyclerViewもHeightのMeasureSpecがUNSPECIFIEDになるので、layoutState.mInfiniteフラグがtrueになり要素分全てがlayoutされる

という挙動でした。

南の島で読んだ本たち

先日ふらっと南の島(タイのサムイ島)に行ってきた。日常から頭を切り離すことが目的でとにかくぼんやりしてみたくなったのだ。 ぼんやりするにも時間は潰したいので本を持って行くことにしたのだか、日常を想起させる技術書・ビジネス書はNGという縛りで何冊かピックアップ+Twitterでオススメしてもらった本を持って行った。その備忘ログ。

f:id:muumuumuumuu:20180228234012j:plain

1冊目:走ることについて語るときに僕の語ること(村上春樹

走ることについて語るときに僕の語ること (文春文庫)

走ることについて語るときに僕の語ること (文春文庫)

村上春樹によるエッセイ。村上春樹の書籍は「村上さんのところ」くらいしか読んだことがなかったが、私もサボりがちながらも走ったりするので以前から興味があり購入。走ることだけじゃなくて、著者の考え方だったり作家という職業だったり、ほんの少し知らない世界が覗き見れた気がして楽しかった。

2冊目:美味礼讃(ブリア=サヴァラン)

美味礼讃

美味礼讃

19世紀のフランスでベストセラーになった書籍の玉村豊男の新訳の方。新訳じゃなかったら絶対に途中で心折れてただろうなってくらい訳が秀逸。訳だけじゃなくて、2,3ページに毎くらいに解説(ツッコミ?)が入っているのが楽しくてどんどん読める。食(美味学)というのが本書のテーマだが、食だけじゃなくて恋愛とそれに関わる一連の行動について謎の圧で語られていて笑える。

3冊目:サイコパス(中野 信子)

サイコパス (文春新書)

サイコパス (文春新書)

新宿紀伊国屋の精神医学や犯罪心理学の棚で見つけた一冊。これはかなり当たりだった!!本書はサイコパス脳科学の観点から解き明かして行く。目次をみただけでもサイコパスと恋愛はできるか」サイコパスかもしれないあなたへ」サイコパス向けの仕事を探そう」という具合にパワーワードが並ぶ。身の回りの誰かの顔を思い浮かべながら読むと楽しい。

ここまでの3冊は自分で選んで買ったもの。以降はTwitterでオススメしてもらったものたち。

4冊目:世界の終わりとハードボイルド・ワンダーランド村上春樹

世界の終りとハードボイルド・ワンダーランド

世界の終りとハードボイルド・ワンダーランド

またまた村上春樹。こちらはshiroyamaさんのオススメの一冊。

村上春樹の小説初めて読んだけど、ミステリー調で想像していたものより100倍読みやすかった!食べ物とお酒とファッションと音楽と女の子が魅力的に語られるシャレオツ小説でした。世界の終わりの影が好きでした。ハードボイルド・ワンダーランドの方だと博士と図書館の女性が好き。あと、よく「やれやれ」っていうと村上春樹っぽいってインターネッツでネタにされてるのがようやく理解できた。本当にたくさん「やれやれ」って言ってた。

5冊目:EAT&RUN(スコット・ジュレク)

EAT&RUN 100マイルを走る僕の旅

EAT&RUN 100マイルを走る僕の旅

こちらはimaiさんのオススメ。ヴィーガンウルトラマラソン走っちゃうとんでもない人の自伝。

「とにかくいいからやるんだ」期と「どうやったら勝てるか?」を考える期、どちらが適切か受け入れる姿勢は取り入れていきたい。そんな難しいことを考えながら読んで勉強になったけど、最後にはシンプルに「あぁ、走りたいなぁ」と思えるいい本だった。

6冊目:仁義なきキリスト教史(架神恭介

こちら、オススメしていただいた時の元tweetが消されているため、本当はオススメじゃなかった説あるけど気になったので読んだ。色々ぶっ飛んでて楽しかった!セリフが全部広島弁の極道。多少なりともキリスト教の知識があったほうが笑えると思う。これ読んで日本帰ったらポプテピピック8話が同じく広島弁極道ネタだったのでまた笑えた。

以下、持っていけなかったけどオススメしてもらったのでいつか読みたい本たち。

おまけ1:野火(大岡昇平

野火 (新潮文庫)

野火 (新潮文庫)

こちらはkurokawaさんオススメ。あらすじ読んで怖くなったのでいつかメンタルが強くなったら読みたい。

おまけ2:孤島の冒険(N. ヴヌーコフ)

孤島の冒険 (フォア文庫)

孤島の冒険 (フォア文庫)

こちらはmitsuyoshiのオススメ。児童書なのかな?絶版っぽいので今度会った時に貸してもらう。

おまけ3:蜜蜂と遠雷恩田陸

こちらは鍵付きアカウントからのオススメ。恩田陸は高校生の頃めちゃめちゃ読んでたなぁ。

おまけ4:グラゼニ(原作 森高夕次 漫画 アダチケイジ)

こちらはnakagawaさんのオススメ。Webコミックなのかな?

www.moae.jp

野球のことは全くわからないけどせっかくオススメされたので読んでみたい。




こうやってみると仕事以外だと食べることと走ることに興味があるんだなー。

DroidKaigi公式アプリにRTL周りのPull Request出した時にやったことまとめ

DroidKaigiお疲れ様でした!公式アプリでRTL周りのPull Requestを出したので何をしたのか備忘録のメモを残しておきます。

Issue

github.com

Pull Request

github.com

やったこと

Left / Right をgrepして適切なものに置き換え

ConstraintLayout周りとかLintで指摘してくれないものがあるのでleft / rightgrepした方がいいと思います。 grepした結果を機械的に置き換えれば良いというわけではなく、例えば情報としてはひとまとまりなんだけどviewとしては別れているみたいなケースがあります。

例えば、下記の画像の場合、

f:id:muumuumuumuu:20180214101601p:plain

What is " が一つのTextView, DroidKaigiが一つのImageVIew, " ? が一つのTextViewで構成されています。 RTL表示の場合でも英字はLTRの方向で表示するため、単純にleft / right をstart / end に置き換えてしまうと表示がおかしくなってしまいます。

上記のように色々試行錯誤した結果たどり着いたのがこちら。

具体的に書くと、DroidKaigi のImageViewの(画像自体に)左右に余白を取って、そこを起点に左右のViewに制約をつける方法で回避しました。

今気がついたんだけど、なぜか" ?の方だけRTL設定の時に? "になっちゃってますね。 What is "の方は大丈夫なんだけど。
" ?の方のTextViewにandroid:textDirection="ltr"を指定してLocaleに関係なく文字方向を固定することで回避できます。

Localeに関わらずTextViewにRTLを強制する場合は android:textAlignment を設定

例えばこのような条件の場合、

  1. アラビア語のリソースをvalues-ar配下に定義
  2. widthがmatch_parentなTextViewのtextに1のリソースを設定
  3. 端末の言語設定をアラビア語に設定

2で設定したリソースは右から表示されます。

今回のDroidKaigiの公式アプリはアラビア語リソースは用意されていないので、「開発者向けオプション」> 「RTLレウアウト方向を使用」にチェックを入れて表示確認をして行きます。この場合だと英語リソースのままレイアウト方向はRTLになりますが、逆にいうとレイアウト方向しかRTLにならないのでmatch_parentなTextViewのtextは左から表示されます。

こんな感じ。

そこでandroid:textAlignment="viewStart"を設定することでレイアウト方向とテキスト方向が連動するようになります。

directional symbolは反転させる

directional symbolって日本語でなんていうのがポピュラーなのかわかっていませんが、矢印のように方向性を持ったシンボル(そのまんまや…)のことです。Droidkaigi公式アプリではほとんどvector drawableでアイコンが作られていたので<vector>tagのなかにandroid:autoMirrored="true"を指定してやればRTL表示時に反転します。

stringsのplaceholderは(必要があれば)RTL用のものを作る

そのまんま。必要があればこんな感じでRTL用のplaceholderを作ります。

     <string name="room_format" translatable="false">%1$s%2$s</string>
     <string name="room_format_rtl" translatable="false">%2$s%1$s</string>

アラビア語のリソースを用意するのであれば不要かもしれません。ケースバイケース。
上で書いたように今回は同じ言語リソース(英語と日本語)でRTLの場合とそうでない場合に対応するのでコードで、動的にどちらを読むか判断しています。 どちらを読むかについては下記の拡張関数を作ってレイアウト方向をチェックして判断するようにしました。

fun View.isLayoutDirectionRtl() =
        resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL

コードからPadding設定するとき用に拡張関数作る

View#setPadding() メソッドは引数を4つ持ちますが、そのうち第1引数は(startではなく)left固定、第3引数も同様にright固定となります。そのため、RTL表示の場合はleftとrightを入れ替える拡張関数を作りました。

fun View.setPaddingWithLayoutDirction(start: Int, top: Int, end: Int, bottom: Int) =
        if (isLayoutDirectionRtl()) {
            setPadding(end, top, start, bottom)
        } else {
            setPadding(start, top, end, bottom)
        }

おまけ

全く気がつかなかったのですが自分のPull Requestにtypoがあって気がついた方に直してもらいました…

圧倒的申し訳なさ…!

AndroidのMVIについて勉強した時のメモ

AndroidのMVIについて勉強した時のメモです。自分用のメモなので雑です。

MVIとは

MVCを元にして、jsのreduxやreactの思想を取り入れた何か」のようなもの。リアクティブで関数型プログラミングを採用している。

MVCだとViewを操作するのがControllerかもしれないし、Modelかもしれないが、MVIはreduxのようにデータの流れが単一方向。また、Reduxのようにいろんな部分を純粋関数で記述するのでテストも書きやすい。

詳しくは

この辺のリンクに目を通したらだいたいわかった気になれる。

hannesdorfmann.com

yuyakaido.hatenablog.com

github.com

雑感

Reduxと違うところ

すでにReduxが何か理解している人向けに書くと、Reduxと比べてこの辺が良さそう。

  • 非同期処理というかデータの取得と整形をProcessorでやると定まっている
    • Reduxだとこの辺がふわふわしている印象
      • ほとんどの人はCreatorProducerがやるんだろうけど
    • 初めての人でも迷わなさそう
  • ActionとIntentが別れているのが良い
    • ReduxだとViewがCreatorProducer経由でAction発行するの、わかるんだけど、User観点で何をしたいのか?と実際にデータをどうするか?を分けて記述できるのがわかりやすい
  • ActionとResultが別れているのが良い
    • ReduxだとActionに変更したいstateのデータを詰める感じだと思うけど、それをResultという別のものとして扱うのが直感的
  • ユーザの入力によって何をするべきかというのがコードで表現される
    • あまり仕様を書かない文化のチームだと助かる場面があるかも

一方、Reduxと比べてこの辺つらみありそうというところもあって、

  • SingleなStoreの概念がないので、一箇所で集中管理する感じではない
  • ViewModel層がいかつい
    • 細かく切ってる分ボイラープレート増える
      • IntentとAction増えてくるとここまでする必要あるのかな〜って気分になってきそう
  • まだそんなに流行ってないので知見がweb上で見つかりにくい

で、どうなの

自分の中では結局「どういうプロダクトかによる」という普通かよ〜〜〜みたいな結論になった。ユーザの入力というかIntentの種類がたくさんあって、かつ結びつけるActionがバラバラだったりすると、ActionをIntentを分ける恩恵はありそう。今Reduxを採用しているAndroidアプリの中からMVIに移行するケースがあれば話を聞いてみたい。でもStoreがないのでAndroidで採用するほどのモチベーションがありそうなケース、そんなになさそうかも?

今年のDroidKaigiでMVIのセッションがあるので今から楽しみにしている。

droidkaigi.jp

完全に余談になるが、AndroidでIntentというとおそらく大多数の人がandroid.content.Intentだと思うし、Processorio.reactivex.processorsを連想する人が多いと思う。名前の衝突が発生して、今どちらについて話しているのか?の認識合わせをしながらコミュニケーションとって行く必要があるので地味にストレス溜まるかもしれないなーと思った。チーム開発で取り入れてみないと何も言えないけれど。

おまけ:本家Circle.js

Twitterで釘宮さんに圧倒的知見をいただいた。感謝!

本家Cycle.jsだとかなりシンプルな構成で、DOM source(入力)とDOM sink (出力)を繋ぐのは、intentとmodelとviewだけ。面白いのはintentとか含めだいたいstreamとして扱われるところ。reactだからかな。

こんなにスッキリかけるのは羨ましい。

function main(sources) {
  return {DOM: view(model(intent(sources.DOM)))};
}

cycle.js.org

また、Eggheadのvideoコースもわかりやすいので、jsと英語にアレルギーがなかったらみてみると良いと思う。

egghead.io