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

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

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

[海外対応] AndroidのLocale周りまとめ [多言語対応]

はじめに

複数の国や地域、あるいは言語に対応する場合に避けられないMulti Locale対応ですが、Android Frameworkのバージョンによってできること・しなくてはならないことが変わって来ます。 各APIでの大きな変化を時系列でまとめてご紹介します。 また、実際にMulti Localeに対応して来た中で遭遇したハマり所とその対策方法・tipsなども共有していきます。

ってDroidKaigi2018のCfPに応募したけどRejectされたのでブログにまとめます😇 「急にグローバル対応しなきゃいけなくなったけど何していいかわかんない!」みたいな人にざっと見ていただきたいような内容です。

それでは各APIで何が対応されたのかまとめます。ハマり所とその対策方法とかは別のエントリで書けたらいいな。

Locale周りの変遷

API Level 1 : Localeの基礎

LocaleクラスはAPI Level 1の時代から存在している。というかAndroidで独自に定義したものではなく、java.util.Localeを使っている。そもそもLocaleってなに?という話をすると、ドキュメントには

A Locale object represents a specific geographical, political, or cultural region.

と定義されている。単に国というわけではなく、地理的・政治的・文化的な特定の地域をさすものである。例を挙げると、日本だとja-JPになる。これはlanguageとcountry(region)を合わせたものだ。この頃はlanguage codeについてはISO 639-1、region codeについてはISO 3166-1で定義されているものが使える。後述するが、API Level 24以降ではIETF BCP 47をサポートする。

「地理的・政治的・文化的」という部分について補足すると、languageにおいて同じ中国語でも簡体字繁体字が別のものとして扱われていたり、regionの方も中国(本土)と香港と台湾が別のコードが割り当てられている例がわかりやすいかもしれない。

さて、話をAndroidに戻そう。LocaleオブジェクトはContextからgetResources()getConfiguration()と経由して、Configurationのpublicなメンバであるlocaleにアクセスすることができる。アプリやActivity独自でLocaleの設定を持ちたい場合は、ApplicationやActivityのContextのlocaleを書き換えることで言語切り替えを行うことができる。具体的にはいい感じにLocaleを書き換えたConfigurationオブジェクトを使って Resources#updateConfiguration()を呼んでやるとできる。(端末の言語設定に従う場合は端末で設定したLocaleがここに入ってくるので特に個別のアプリで対応する必要はない。)端末の言語設定で何が選択されているか知りたい時にはLocale#getDefault()が使える。

では各Localeごとのリソースをどう定義するか。ドキュメントはこの辺りを参照してほしいが、ざっくりいうとres/配下のディレクトリにconfig_qualifierを付与することでLocaleごとのリソースを定義することができる。ディレクトリ名のルールはこのようにハイフンつなぎ。

<resources_name>-<config_qualifier>

例を挙げると、 アメリカ・英語用の言語リソースは下記のように定義できる。valuesresources_nameenrUSconfig_qualifierだ。

values-en-rUS/strings.xml

上記の例のように、qualifierはハイフンで複数繋げることができる。Localeクラス同様languageは ISO 639-1、regionは ISO 3166-1で定義されたものが使える。ただし、regionの方は頭にrをつけなければならない。なぜかというとcase sensitiveではないので、ハイフンでqualifierを繋げた場合にregionを明示的に示すために必要なのだ。また、regionを指定する場合は必ずlanguageとセットで指定しなければならない。(region単体では使えない。)

例ではvalues/を示したが、values以外にももちろん使える。例えばdrawable/でも文字埋め込み画像の出し分けをするのに必要だし、xml/menu/でLocaleによって機能の出し分けをするのに使うことができる。

API Level 17 : RTL対応

ここからConfigurationクラスにsetLocale()メソッドが追加された。とは言え、このクラスのメンバであるlocaleはpublicのままだ。では直接localeの値を書き換えるのとsetLocale()メソッドでやっていることは何が違うかコード見てみよう。

     /**
      * Set the locale. This is the preferred way for setting up the locale (instead of using the
      * direct accessor). This will also set the userLocale and layout direction according to
      * the locale.
      *
      * @param loc The locale. Can be null.
      */
     public void setLocale(Locale loc) {
         locale = loc;
         userSetLocale = true;
         setLayoutDirection(locale);
     }

まずはlocaleの更新を行なっている。これは予想通りだろう。次にuserSetLocaleフラグをtrueにしている。このフラグはpublicだがhideアノテーションが付いている。これはActivityManagerServiceがsystem propertyにlocale情報を書き込む時に使われる。 そして最後のsetLayoutDirection()メソッド。これもAPI Level 17から追加されたAPIだ。何をしているかと言うと、どちらからどちらの方向にレイアウトするをLocaleから判断している。

「どちらからどちらにレイアウトするか」と聞いてピンと来ない人がいるかもしれない。我々が普段目にする文字は(横書きの)日本語であれば左から右方向に流れる。よく目にする英語もそうだ。しかしアラビア語ヘブライ語など一部の言語では横書きでも右から左に文字を記述する。右から左、なのでRight to Left, 頭文字をとってRTL言語と言われる。文字だけでなく、ボタンの位置などもRTL方向に配置する必要があるが、言語によってレイアウトを複数分けて用意する必要はない。例えばRelative Layoutの子要素として、TextViewとその右にButtonを置きたいと思ったらandroid:layout_toRightOf ではなくandroid:layout_toEndOfを使おう。これもAPI Level 17から追加されたattributeだ。このattributeを使っているとsetLocale()を使ってLocaleを更新した際に画面の再描画が走って適切なLayout Directionで表示されるようになる。

API Level 24 : Multi Locale

このAPIバージョンから java.util.Locale が変わってIETF BCP 47をサポートするようになった。これによりサポートされる言語が大幅に増えた。 また、Multi LocaleをサポートするようになったのもAPI Level 24からだ。

Multi Localeについては以前ブログにまとめた。

muumuutech.hatenablog.com

詳細は上記を読んでもらうとして、ざっくり概要を書くと「これまではユーザは言語を一つしか設定できなかったが、このバージョンからは複数の言語とその優先順位を設定でき、かつシステム側もいくつかLocaleを準備しておくことによってより良いマッチングを行うことができる」ものである。

いくつかの言語をユーザが設定できるという部分だが、これに伴いConfigurationクラスのAPIにいくつか変更が入った。具体的には単一のLocaleを引数に取る setLocale() 、またpublicメンバのlocale がdeperecatedになった。代わりに使用が推奨されるのがLocaleListクラスを引数に取るsetLocales()だ。

    public void setLocales(@Nullable LocaleList locales) {
         mLocaleList = locales == null ? LocaleList.getEmptyLocaleList() : locales;
         locale = mLocaleList.get(0);
         setLayoutDirection(locale);
     }

3行のシンプルなメソッドだ。Localeのリストを更新して、その優先順位トップのものを現在のLocaleとして設定、最後に現在のLocaleに応じたLayout Directionを設定する。

ちなみにLocaleListクラスがマッチングをやってくれる。ロジックはこの辺。以前blogにもちょっと書いた。

muumuutech.hatenablog.com

API Level 25 : Configurationの設定方法が変わった

Resources#updateConfiguration() がdeprecatedになった。代わりに何を使うのが推奨されているかというと、Context#createConfigurationContext()だ。(このAPIが追加されたのはAPI Level 17。)ただし、これは引数で指定したConfigurationが設定された新しいContextオブジェクトを返してくれるだけだ。ここで生成したContextをActivity#attachBaseContext()に渡してやると新しいConfiguration(と、これが持っているLocale)が設定される。

Twitterで教えてもらいました。感謝!)

API Level 26: Configurationの共有範囲が変わった

API Level 25でdeprecatedになったResources#updateConfiguration()だが、25までは一度コールするとその設定がapplication内で共有されていたが、API Level 26からは各activity/applicationで別の設定になった。

このあたりの記事が参考になった。

proandroiddev.com

最後に

以上でざっとLocale周りの変遷を書いた。最後の方とかLocaleともはや直接関係ないんじゃ…という気がしなくもないが、多言語、海外対応する際に必要になるケースも多いのではないかと思うので消さずに残しておく。

それにしてもAPI Level が上がれば上がるほどアプリ内で独自の言語設定させていかないぞ♡というGoogle先生のお気持ちが強まっているのかなと思わずにはいられない😇 😇 😇