TransitionのShowcaseアプリを作って公開しました
はじめに
先日droid girls meetupに参加し、Animationのハンズオンを楽しんできた。 GoogleのオフィスでGooglerの方(Support libraryを作っている方)が講師というなんとも贅沢な回だった。
そういえば業務でがっつりtransitionをさわる機会がないなぁと思っていたので折角なので色々試してみた。 ついでに、
アニメーション何種類か切り替えるサンプルアプリ作ってデザイナさんと共有したい!!!! #droidgirls
— むーむー (@muumuumuumuu) 2018年4月20日
そういえばこんなことをつぶやいていたのでShowcase的なアプリにして公開することにした。
Source code
コードはこちら。
サンプルの中にソースコードの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はRecyclerView
かNestedScrollVIew
しかダメっぽい。普通のScrollView
を入れてもAppBarLayout
はcollapsedされないし、AppBarLayout
とScrollView
が重なってしまう。これ毎回忘れて毎回ハマる気がする…
もっといろんな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)
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で囲ってやると期待通りの表示になります。
これはなかなか興味深い挙動になっています。それではToolbarのコードを読んでどうしてこうなったのか見ていきましょう。
Toolbarのソースコードを読む
Support LibraryのToolbarのコードで、自分の高さを計算しているのはこの辺りです。
Cross Reference: /frameworks/support/v7/appcompat/src/android/support/v7/widget/Toolbar.java
読んでいくと、「NavigationViewは出すのか」「Menuは表示するのか」「ロゴは表示するのか」などで細かい計算ロジックが入っていて面白いです。
最後の最後にView.resolveSizeAndState()
が呼ばれます。ここに来る時点でheight
もgetSuggestedMinimumHeight()
も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
の要素として扱うしかないんじゃないかな。
なんでこんなことになるのか、せっかくなのでなのでNestedScrollView
とRecyclerView
周りのコードを読んでみた。
RecyclerView
がどうやって要素をlayoutしていくかはこちらの資料が詳しいので色々省略
www.slideshare.net
問題はRecyclerViewの中をどんどん埋めていくこちらのメソッド
/** * 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についてはこちらが詳しいです。
親が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); } } }
ここでheightMode
はMeasureSpec.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してみる。
UNSPECIFIED
をEXACTLY
に変えて見たらallocateされる個数が30個から14個まで減った。どういうロジックで14個になるかまではわからず…
というわけでまとめると、
NestedScrollView#onMeasure()
がコールされるNestedScrollView#measureChildWithMargins()
がコールされるが、ここでchildHeightMeasureSpec
が0 (UNSPECIFIED
)としてchild.measure()
がコールされる- childである
RecyclerView
もHeightのMeasureSpecがUNSPECIFIED
になるので、layoutState.mInfinite
フラグがtrueになり要素分全てがlayoutされる
という挙動でした。
南の島で読んだ本たち
先日ふらっと南の島(タイのサムイ島)に行ってきた。日常から頭を切り離すことが目的でとにかくぼんやりしてみたくなったのだ。 ぼんやりするにも時間は潰したいので本を持って行くことにしたのだか、日常を想起させる技術書・ビジネス書はNGという縛りで何冊かピックアップ+Twitterでオススメしてもらった本を持って行った。その備忘ログ。
1冊目:走ることについて語るときに僕の語ること(村上春樹)
- 作者: 村上春樹
- 出版社/メーカー: 文藝春秋
- 発売日: 2010/06/10
- メディア: ペーパーバック
- 購入: 25人 クリック: 137回
- この商品を含むブログ (182件) を見る
村上春樹によるエッセイ。村上春樹の書籍は「村上さんのところ」くらいしか読んだことがなかったが、私もサボりがちながらも走ったりするので以前から興味があり購入。走ることだけじゃなくて、著者の考え方だったり作家という職業だったり、ほんの少し知らない世界が覗き見れた気がして楽しかった。
2冊目:美味礼讃(ブリア=サヴァラン)
- 作者: ブリア=サヴァラン,Jean Anthelme Brillat‐Savarin,玉村豊男
- 出版社/メーカー: 新潮社
- 発売日: 2017/04/27
- メディア: 単行本
- この商品を含むブログを見る
19世紀のフランスでベストセラーになった書籍の玉村豊男の新訳の方。新訳じゃなかったら絶対に途中で心折れてただろうなってくらい訳が秀逸。訳だけじゃなくて、2,3ページに毎くらいに解説(ツッコミ?)が入っているのが楽しくてどんどん読める。食(美味学)というのが本書のテーマだが、食だけじゃなくて恋愛とそれに関わる一連の行動について謎の圧で語られていて笑える。
3冊目:サイコパス(中野 信子)
- 作者: 中野信子
- 出版社/メーカー: 文藝春秋
- 発売日: 2016/11/18
- メディア: 新書
- この商品を含むブログ (11件) を見る
新宿紀伊国屋の精神医学や犯罪心理学の棚で見つけた一冊。これはかなり当たりだった!!本書はサイコパスを脳科学の観点から解き明かして行く。目次をみただけでも「サイコパスと恋愛はできるか」、「サイコパスかもしれないあなたへ」、「サイコパス向けの仕事を探そう」という具合にパワーワードが並ぶ。身の回りの誰かの顔を思い浮かべながら読むと楽しい。
ここまでの3冊は自分で選んで買ったもの。以降はTwitterでオススメしてもらったものたち。
4冊目:世界の終わりとハードボイルド・ワンダーランド(村上春樹)
- 作者: 村上春樹
- 出版社/メーカー: 新潮社
- 発売日: 2005/09/15
- メディア: 単行本
- 購入: 7人 クリック: 288回
- この商品を含むブログ (93件) を見る
またまた村上春樹。こちらはshiroyamaさんのオススメの一冊。
世界の終わりとハードボイルドワンダーランド
— 父 (@fushiroyama) 2018年2月12日
村上春樹の小説初めて読んだけど、ミステリー調で想像していたものより100倍読みやすかった!食べ物とお酒とファッションと音楽と女の子が魅力的に語られるシャレオツ小説でした。世界の終わりの影が好きでした。ハードボイルド・ワンダーランドの方だと博士と図書館の女性が好き。あと、よく「やれやれ」っていうと村上春樹っぽいってインターネッツでネタにされてるのがようやく理解できた。本当にたくさん「やれやれ」って言ってた。
5冊目:EAT&RUN(スコット・ジュレク)
- 作者: スコット・ジュレク,スティーヴ・フリードマン,小原久典,北村ポーリン
- 出版社/メーカー: NHK出版
- 発売日: 2013/02/21
- メディア: 単行本(ソフトカバー)
- クリック: 1回
- この商品を含むブログ (7件) を見る
こちらはimaiさんのオススメ。ヴィーガンでウルトラマラソン走っちゃうとんでもない人の自伝。
— Tomoaki Imai (@tomoaki_imai) 2018年2月13日
「とにかくいいからやるんだ」期と「どうやったら勝てるか?」を考える期、どちらが適切か受け入れる姿勢は取り入れていきたい。そんな難しいことを考えながら読んで勉強になったけど、最後にはシンプルに「あぁ、走りたいなぁ」と思えるいい本だった。
6冊目:仁義なきキリスト教史(架神恭介)
- 作者: 架神恭介
- 出版社/メーカー: 筑摩書房
- 発売日: 2016/12/07
- メディア: 文庫
- この商品を含むブログ (9件) を見る
こちら、オススメしていただいた時の元tweetが消されているため、本当はオススメじゃなかった説あるけど気になったので読んだ。色々ぶっ飛んでて楽しかった!セリフが全部広島弁の極道。多少なりともキリスト教の知識があったほうが笑えると思う。これ読んで日本帰ったらポプテピピック8話が同じく広島弁極道ネタだったのでまた笑えた。
以下、持っていけなかったけどオススメしてもらったのでいつか読みたい本たち。
おまけ1:野火(大岡昇平)
- 作者: 大岡昇平
- 出版社/メーカー: 新潮社
- 発売日: 1954/05/12
- メディア: 文庫
- 購入: 9人 クリック: 520回
- この商品を含むブログ (95件) を見る
こちらはkurokawaさんオススメ。あらすじ読んで怖くなったのでいつかメンタルが強くなったら読みたい。
なにも思い付かなかったのですが、大岡昇平さんの「野火」はどうでしょう。太平洋戦争中の南の島の話です。
— Hiroshi Kurokawa (@hydrakecat) 2018年2月13日
おまけ2:孤島の冒険(N. ヴヌーコフ)
- 作者: N.ヴヌーコフ,ジマイロフ,Nikolai Andreevich Vnukov,島原落穂
- 出版社/メーカー: 童心社
- 発売日: 1998/06
- メディア: 文庫
- 購入: 2人 クリック: 2回
- この商品を含むブログ (1件) を見る
こちらはmitsuyoshiのオススメ。児童書なのかな?絶版っぽいので今度会った時に貸してもらう。
「孤島の冒険」
— みつよし (@vespid) 2018年2月13日
おまけ3:蜜蜂と遠雷(恩田陸)
こちらは鍵付きアカウントからのオススメ。恩田陸は高校生の頃めちゃめちゃ読んでたなぁ。
- 作者: 恩田陸
- 出版社/メーカー: 幻冬舎
- 発売日: 2016/09/21
- メディア: Kindle版
- この商品を含むブログ (2件) を見る
おまけ4:グラゼニ(原作 森高夕次 漫画 アダチケイジ)
こちらはnakagawaさんのオススメ。Webコミックなのかな?
野球のことは全くわからないけどせっかくオススメされたので読んでみたい。
グラゼニ(漫画)
— Shinichi Nakagawa (@shinyorke) 2018年2月13日
こうやってみると仕事以外だと食べることと走ることに興味があるんだなー。
DroidKaigi公式アプリにRTL周りのPull Request出した時にやったことまとめ
DroidKaigiお疲れ様でした!公式アプリでRTL周りのPull Requestを出したので何をしたのか備忘録のメモを残しておきます。
Issue
Pull Request
やったこと
Left / Right をgrepして適切なものに置き換え
ConstraintLayout周りとかLintで指摘してくれないものがあるのでleft
/ right
でgrepした方がいいと思います。
grepした結果を機械的に置き換えれば良いというわけではなく、例えば情報としてはひとまとまりなんだけどviewとしては別れているみたいなケースがあります。
例えば、下記の画像の場合、
What is "
が一つのTextView, DroidKaigi
が一つのImageVIew, " ?
が一つのTextViewで構成されています。
RTL表示の場合でも英字はLTRの方向で表示するため、単純にleft / right をstart / end に置き換えてしまうと表示がおかしくなってしまいます。
RTL対応、今までParent ViewGroup作って対応していたところがConstraintLayoutでどうやるか悩む… A B C Dみたいなviewを4つ並べるデザインで、B Cが意味のひとかたまりだと、D B C A みたいな表示にしたいけど、各Viewに制約つけるとうまくいかないし、ConstraintLayoutで子の階層深くするのは悔しい
— むーむー (@muumuumuumuu) 2018年2月5日
ConstraintLayoutの中に置いたRelativeLayoutのwidthをwrap_contentにしているのだけど、RTLの時だけなぜかmatch_parentとして振る舞うようになってしまって泣いてる
— むーむー (@muumuumuumuu) 2018年2月5日
RelativeLayoutに `android:layoutDirection="ltr"` つけるのも考えたけど、RelativeLayout自体のgravityをrtlにしたいので却下。さてどうすればいいんだ…
— むーむー (@muumuumuumuu) 2018年2月5日
上記のように色々試行錯誤した結果たどり着いたのがこちら。
真ん中のViewに余白大きめにとってそこを起点に制約をつけていくというハックを覚えた
— むーむー (@muumuumuumuu) 2018年2月5日
具体的に書くと、DroidKaigi
のImageViewの(画像自体に)左右に余白を取って、そこを起点に左右のViewに制約をつける方法で回避しました。
今気がついたんだけど、なぜか" ?
の方だけRTL設定の時に? "
になっちゃってますね。
What is "
の方は大丈夫なんだけど。
" ?
の方のTextViewにandroid:textDirection="ltr"
を指定してLocaleに関係なく文字方向を固定することで回避できます。
Localeに関わらずTextViewにRTLを強制する場合は android:textAlignment を設定
例えばこのような条件の場合、
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があって気がついた方に直してもらいました…
今まで出したPR見たら29個できりが悪かった感じしたので30個目のPR出した(Typo修正)https://t.co/VIxwSR8fCo
— Rui Kowase (@rkowase) 2018年2月11日
圧倒的申し訳なさ…!
アアアアtypo気づかなかった本当に申し訳ないありがとうございます🙏🙏🙏🙏🙏
— むーむー (@muumuumuumuu) 2018年2月11日
AndroidのMVIについて勉強した時のメモ
AndroidのMVIについて勉強した時のメモです。自分用のメモなので雑です。
MVIとは
「MVCを元にして、jsのreduxやreactの思想を取り入れた何か」のようなもの。リアクティブで関数型プログラミングを採用している。
MVCだとViewを操作するのがControllerかもしれないし、Modelかもしれないが、MVIはreduxのようにデータの流れが単一方向。また、Reduxのようにいろんな部分を純粋関数で記述するのでテストも書きやすい。
詳しくは
この辺のリンクに目を通したらだいたいわかった気になれる。
雑感
Reduxと違うところ
すでにReduxが何か理解している人向けに書くと、Reduxと比べてこの辺が良さそう。
- 非同期処理というかデータの取得と整形をProcessorでやると定まっている
- Reduxだとこの辺がふわふわしている印象
- ほとんどの人はCreatorProducerがやるんだろうけど
- 初めての人でも迷わなさそう
- Reduxだとこの辺がふわふわしている印象
- 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のセッションがあるので今から楽しみにしている。
完全に余談になるが、AndroidでIntentというとおそらく大多数の人がandroid.content.Intent
だと思うし、Processorもio.reactivex.processors
を連想する人が多いと思う。名前の衝突が発生して、今どちらについて話しているのか?の認識合わせをしながらコミュニケーションとって行く必要があるので地味にストレス溜まるかもしれないなーと思った。チーム開発で取り入れてみないと何も言えないけれど。
おまけ:本家Circle.js
Twitterで釘宮さんに圧倒的知見をいただいた。感謝!
MVIってcycle.jsからきてるので、オリジナルのものも見てみるといいかも ( https://t.co/RtmPJx1SxG ) 。1hくらいの動画もあって手軽に学べますよ
— 有象無象 (@kgmyshin) 2018年1月25日
で、オリジナルだとReducerとかって言葉は出てこなかったはずなので、Android用に変えてるのか、ちょっとRedux混ぜてるかもですね (悪いとかではなく)
本家Cycle.jsだとかなりシンプルな構成で、DOM source(入力)とDOM sink (出力)を繋ぐのは、intentとmodelとviewだけ。面白いのはintentとか含めだいたいstreamとして扱われるところ。reactだからかな。
こんなにスッキリかけるのは羨ましい。
function main(sources) { return {DOM: view(model(intent(sources.DOM)))}; }
また、Eggheadのvideoコースもわかりやすいので、jsと英語にアレルギーがなかったらみてみると良いと思う。
[海外対応] 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>
例を挙げると、 アメリカ・英語用の言語リソースは下記のように定義できる。values
がresources_name
でen
とrUS
がconfig_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については以前ブログにまとめた。
詳細は上記を読んでもらうとして、ざっくり概要を書くと「これまではユーザは言語を一つしか設定できなかったが、このバージョンからは複数の言語とその優先順位を設定でき、かつシステム側もいくつか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にもちょっと書いた。
API Level 25 : Configurationの設定方法が変わった
Resources#updateConfiguration()
がdeprecatedになった。代わりに何を使うのが推奨されているかというと、Context#createConfigurationContext()
だ。(このAPIが追加されたのはAPI Level 17。)ただし、これは引数で指定したConfigurationが設定された新しいContextオブジェクトを返してくれるだけだ。ここで生成したContextをActivity#attachBaseContext()
に渡してやると新しいConfiguration(と、これが持っているLocale)が設定される。
(Twitterで教えてもらいました。感謝!)
以前、下のやり方を参考に言語設定を変えてました。参考になれば…https://t.co/mmYczmIgfe
— tsuyoshi uehara@転職活動中 (@uecchi) 2017年8月23日
API Level 26: Configurationの共有範囲が変わった
API Level 25でdeprecatedになったResources#updateConfiguration()
だが、25までは一度コールするとその設定がapplication内で共有されていたが、API Level 26からは各activity/applicationで別の設定になった。
このあたりの記事が参考になった。
最後に
以上でざっとLocale周りの変遷を書いた。最後の方とかLocaleともはや直接関係ないんじゃ…という気がしなくもないが、多言語、海外対応する際に必要になるケースも多いのではないかと思うので消さずに残しておく。
それにしてもAPI Level が上がれば上がるほどアプリ内で独自の言語設定させていかないぞ♡というGoogle先生のお気持ちが強まっているのかなと思わずにはいられない😇 😇 😇