ConstraintLayout 2.0でFlexboxLayoutとGridLayoutを代用する
はじめに
ConstraintLayout 2.0から導入された Flow
を使ってFlexboxLayoutやGridLayoutと同じようなことができるようになる。
こんな感じでできた。
(上がFlexboxLayoutっぽいやつ、下がGridLayoutっぽいやつ)
ConstraintLayoutは 2.0.0-beta2
を使っている。
コードサンプルはこちら
FlexboxLayoutっぽいのを作ってみる
<androidx.constraintlayout.helper.widget.Flow android:id="@+id/flexbox_flow" android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="16dp" android:orientation="horizontal" app:constraint_referenced_ids="text_1,text_2,text_3,text_4,text_5" app:flow_wrapMode="chain" app:flow_horizontalStyle="packed" app:flow_verticalStyle="packed" app:flow_horizontalGap="8dp" app:flow_verticalGap="8dp" app:flow_firstHorizontalBias="0" app:flow_horizontalBias="0" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/grid_flow" />
配置したい要素となるviewとは別に Flow
を定義してやる。以下各attributeの説明
- 要素は
app:constraint_referenced_ids
でカンマ区切りで指定 - FlexboxLayoutっぽくするポイントは
app:flow_wrapMode
でchain
を指定すること app:flow_horizontalStyle
とapp:flow_verticalStyle
にpacked
を指定しているので詰めて表示される- 各要素のmerginは
app:flow_horizontalGap
とapp:flow_verticalGap
で指定 layout_constraintHorizontal_bias
などが効かないので、代わりにapp:flow_horizontalBias
を使う
GridLayoutっぽいのを作ってみる
<androidx.constraintlayout.helper.widget.Flow android:id="@+id/grid_flow" android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="16dp" android:orientation="horizontal" app:constraint_referenced_ids="number_1,number_2,number_3,number_4,number_5,number_6,number_7,number_8,number_9" app:flow_wrapMode="aligned" app:flow_maxElementsWrap="3" app:flow_horizontalGap="4dp" app:flow_verticalGap="4dp" app:layout_constraintTop_toBottomOf="@id/flexbox_flow" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" />
FlexboxLayoutっぽい方とほとんど一緒だが、違いは app:flow_wrapMode
に aligned
を指定すること。あと、1行(orientationがverticalの場合は1列)にいくつ要素を配置するかを app:flow_maxElementsWrap
で指定する。
終わりに
かなり使いやすい印象なので、早くbetaが取れて欲しい。FlexboxLayoutとGridLayoutではなくてConstraintLayoutを使う理由は何かあるのかと思う人がいるかもしれないが、個人的にはこの辺りの理由でConstraintLayoutを推す。
- デザイン変更が容易にできる(各要素は変更せず、Flowだけの変更で対応できる。運がよければだけど)
- Layoutがnestしないのでパフォーマンス観点で期待できる(計測して無いですごめんなさい)
- 同じくConstraintLayout 2.0 から追加された
Layer
やCircular Reveal
などのdecoratorが使いたくなるかも?
Androidのdark theme
Androidのdark themeをさわってみたのでメモを残す。
Dark themeとは?
公式documentはこちら
電池の節約だったり、low visionなuser向けにより良いUIを提供することを目的に、黒背景のthemeがAndroid Q からサポートされた。
アプリでDark themeに対応する
App Theme
まずはApplicationのThemeのparentをDayNight
にする
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
もしくはMaterialComponents
の方を使ってもいいらしい。
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
MaterialComponentsの方がいい感じに色をそれっぽく自動で変えてくれる気がする。
(ActionBarのcolorPrimary
の色とかbuttonとか。)
Resource
dark themeがONになっているときは基本的にvalues-night
などnightのqualifierがついたresource directoryに置いているものが読まれる。
(values-notnight
も作ることができる。defaultをdark themeにしたいときはこれを使えば良さそう。)
dark themeで変えなきゃいけないのはcolorとアイコンくらいかなぁ。
ちなみにdark themeは端末のSettings > Display > Dark Theme
からONにすることができる。 Qより前のOS(Pieとか)でも同じメニューが存在するが、ONにしても values-night
配下のリソースが使われないので注意。
Dark themeを動的に切り替える
端末のDark Themeの設定ではなく、アプリ固有でDark Themeを設定することもできる。
例えば端末の設定に関わらず、アプリ全体でDark ThemeをONにしたいときは下記のような1行を呼んでやれば良い。
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES)
アプリ全体ではなく、Activity単位でDark ThemeをONにしたいときは下記のような1行を呼んでやれば良い。
// Call this from AppCompatActivity
delegate.localNightMode = MODE_NIGHT_YES
引数に使える定数は以下(deprecatedなものは除く)
const | values | description |
---|---|---|
MODE_NIGHT_AUTO_BATTERY | 3 | システムの Battery Saver 機能が有効のときはdark modeを使って、それ以外はlight modeになる。 |
MODE_NIGHT_FOLLOW_SYSTEM | 0 | システムの night modeに従う。 |
MODE_NIGHT_NO | 1 | 常にlight modeを使う。また、notnight qualified resourcesが時間に関わらず(夜中でも)使われる。 |
MODE_NIGHT_UNSPECIFIED | 100 | night modeをunspecifiedしたい時に使う。主にsetLocalNightMode() でdefaultのnight modeに戻したい時に使うっぽい。 |
MODE_NIGHT_YES | 2 | 常にdark modeを使う。また、 night qualified resourcesが時間に関わらず(昼間でも)使われる。 |
おまけ
実装だけではなく、material designとしてのdark themeの考え方も読んでみると面白い。 背景(elevation = 0)は黒なんだけど、Surface (elevation = 1)は黒みがかった灰色。dark modeはelevationを影の強さで表現することができないので、高いelevationになるほど白みがかって行くことで表現しているらしい。なるほど。 material.io
Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる
はじめに
Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる現象を見つけて面白かったので調べてみた。
具体的に
まずは下記のgifを見てほしい。リストアイテム押下時にわざと1秒のdelayを入れてstartActivity()
している。
この時、Activity Shared Element Transitionするように bundle
をstartActivity()
の第2引数に渡している。
gifでやっていること手順をまとめると、
- 左上のアイテムをタップ、1秒のdelayが走る(A)
- 1のdelayが終わる前に右上のアイテムをタップ(B)
- Aのdelayが終わり、左上のタップイベントが発火、
startActivity()
が走る - Bのdelayが終わり、右上のタップイベントが発火、
startActivity()
が走る - Aのタップイベント契機のActivityが起動される
- Bのタップイベント契機のActivityが起動される
- Back keyをタップしてBのactivityがfinishされる
- Back keyをタップしてAのactivityがfinishされる
step 8のあと、A, B両方のshared itemはなぜかBのpositionに戻っていき、Aのitemがいなくなってしまうのである。
結論だけ言うと
一つのActivityに登録できるActivity Shared Element Transition用のbundleは一つだけで、しかも上書き。このようなケースを防ぎたかったらActivityの多重起動はできないようにするしかない。
何が起こったのか
何が起こったかを理解するためには、Activity Shared Element Transitionの仕組みを知らないといけない。そのためにFrameworkの内部実装を読んでいく。
このシーケンス図を見てもらえればだいたいわかるんだけど、
まずActivity起動時にstartActivity()の第2引数として渡すbundle (ActivityOptions
) は色々あって最終的にActivityStarter
がActivityRecord
を作るときに使われる。
別のactivityから戻って来たときのアニメーション用にこのoptionを取得する必要があるが、このときActivityRecord
のstatic methodであるisInStackLocked()
が使われる。
static ActivityRecord isInStackLocked(IBinder token) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); return (r != null) ? r.getStack().isInStackLocked(r) : null; }
このmethodはactivityのtokenからActivityRecord
を返してくれる。そしてActivityRecord
内に格納されているActivityOptions
を使ってActivityTransitionState#setEnterActivityOptions()
がコールされる。
と言うわけでActivityOptionsとactivityのtokenは一対一で対応される。Activityが多重で起動されると起動された順番とfinishされる順番がおかしくなるとアニメーションもおかしくなる。この仕組みはアプリからは避けられないので、アプリとしてはActivityの多重起動を避けると言うアプローチがいいと思う。
おまけ
今回貼ったシーケンス図は下記のサイトで作りました。めっちゃ便利。 シーケンス図って最近エンジニアになった人には(特にweb系)馴染みのない古のツールかもしれないけど、私は好きです。
title How to handle enter transition note over User, Activity: Search Result Screen User->User:click item User->Activity:startActivityForResult() Activity->Instrumentation:execStartActivity() Instrumentation->ActivityManagerService:startActivity() ActivityManagerService->ActivityManagerService:startActivityMayWait() ActivityManagerService->ActivityStarter:setActivityOptions() note over ActivityStarter:mRequest.activityOptions = options; ActivityManagerService->ActivityStarter:execute() ActivityStarter->ActivityStarter:startActivityMayWait() ActivityStarter->ActivityStarter:startActivity() note over ActivityStarter: create ActivityRecord using activity option as a one of parameter note over User,ActivityStarter: New Activity is displayed User->User:click back key User->Activity:performStart() Activity->Activity:getActivityOptions() Activity->ActivityManagerService:getActivityOptions(mToken) note over Activity,ActivityManagerService: get acvitivy options from ActivityRecord from static method Activity<--ActivityManagerService:options Activity->ActivityTransitionState:setEnterActivityOptions()
Javaのmethod referenceは初回しか評価されない
はじめに
言いたいことは掲題の通り。method referenceを渡しちゃうと実行時に毎度評価されるわけではない。 毎度評価されたければlambdaを渡した方がいい。
Reference
公式のドキュメントには以下のように記述がある。
The timing of method reference expression evaluation is more complex than that of lambda expressions (§15.27.4). When a method reference expression has an expression (rather than a type) preceding the :: separator, that subexpression is evaluated immediately. The result of evaluation is stored until the method of the corresponding functional interface type is invoked; at that point, the result is used as the target reference for the invocation. This means the expression preceding the :: separator is evaluated only when the program encounters the method reference expression, and is not re-evaluated on subsequent invocations on the functional interface type.
上記の様に最後の一文に「merhod referenceのプログラムに出会った時だけ評価されて、呼び出しのタイミングでは再評価されない」と明言されている。
具体的に
どう言う時に困るかと言うと、例えばクラス生成時に何かをsubscribeしていてviewを更新したいんだけど、viewだけが作り変えられてしまう時とか困る。viewが作り変えられるのでviewのinstanceは新しくなってるんだけど、method referenceでviewを更新しようとしてもこちらは再評価されず古いinstanceを更新しようとしてUIは何も変わらないということが起こりうる。
実際にコード例を示す。
例えばFragmentでBetterKnifeを使っていて、onCreateView()
でviewをbindし、
@BindView(R.id.button) Button button; @BindView(R.id.image) MyImageView imageView; @Nullable @Override public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState ) { View view = inflater.inflate(R.layout.fragment_main, container, false); ButterKnife.bind(this, view); return view; }
onViewCreated()
で本当に初回だけ以下のようなsubscribeを開始するとする。
以下は何かbuttonがあって、押されるたびに別のimage viewのvisibilityがVISIBLE <-> GONEでtoggleになる処理だ。
(何かしらの事情があってviewが作り変えられるたびにsubscribeしたくないものと仮定して読んでほしい :pray: )
@Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (!isInitialized) { disposable.add( RxView.clicks(button) .observeOn(AndroidSchedulers.mainThread()) .subscribe(imageView::toggleVisibility) ); } isInitialized = true; }
一見良さそうに見えるがfragmentが一度detachされ、再度attachされた後、buttonを押してもimage viewのvisibilityは変わらない。この時点でsubscribe()
の引数に渡されたmethod referenceは古いview imageのinstanceに対する参照を持っているからだ。
@Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (!isInitialized) { disposable.add( RxView.clicks(button) .observeOn(AndroidSchedulers.mainThread()) .subscribe((signal) -> { imageView.toggleVisibility(signal); }) ); } isInitialized = true; }
こんな感じでlambdaを渡してやることで回避できる。
AppBarLayoutを使う時にlayout_behaviorをFragmentに持たせるとviewが重ねて表示される
困った問題に出会ったのでメモ。
AppBarLayoutがActivityにいて、layout_behaviorつけたscrollableなviewがFragmentにいる場合、AppBarLayoutはちゃんと動くんだけど、scrollするviewの方がAppBarLayoutと同じ高さからlayoutされるので隠れてしまう…
— むーむー/Atsuko FUKUI (@muumuumuumuu) February 21, 2019
みんな同じActivityにいる場合はちゃんとAppBarの下に表示される…
なんでーーー
何が起こったか
Activityが CoordinatorLayout
, その中に AppBarLayout
を持っていて、(諸々省略するとこんな感じ)
<androidx.coordinatorlayout.widget.CoordinatorLayout> <com.google.android.material.appbar.AppBarLayout> <TextView app:layout_scrollFlags="scroll|enterAlways" /> </com.google.android.material.appbar.AppBarLayout> <RelativeLayout /> <!-- Fragmentをhostingするcontainerのview group --> </androidx.coordinatorlayout.widget.CoordinatorLayout>
上記のcontainerに app:layout_behavior
をセットしたRecyclerViewを持ったFragmentをaddします。
<androidx.constraintlayout.widget.ConstraintLayout> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout app:layout_behavior="@string/appbar_scrolling_view_behavior"> <androidx.recyclerview.widget.RecyclerView /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.constraintlayout.widget.ConstraintLayout>
するとこんな感じでRecyclerViewとAppBarLayoutが重ねて表示されてしまう。
Fragmentにわけない、もしくはActivityのcontainerに app:layout_behavior
を設定する場合は問題なく表示されます。
なんでこんなことになるのか?
せっかくなのでAppBarLayout
とCoordinatorLayout
の中身を見ていく。
そもそも前提として、app:layout_behavior
をつけると何が起きるのか?
CoordinatorLayout.LayoutParams
のコンストラクタでattribute setからapp:layout_behavior
で取得したbehaviorを取得しメンバに保持している。
public static class LayoutParams extends MarginLayoutParams { LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) { R.styleable.CoordinatorLayout_Layout_layout_behavior); if (mBehaviorResolved) { mBehavior = parseBehavior(context, attrs, a.getString( R.styleable.CoordinatorLayout_Layout_layout_behavior)); }
app:layout_behavior
に指定するのは@string/appbar_scrolling_view_behavior
を使うように公式referenceにも書いているが、この参照値の実態はcom.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior
というクラスパスである。上記のparseBehavior()
でreflectionを使ってBehaviorのインスタンスを生成し、メンバのmBehavior
に格納する。
CoordinatorLayout
のlayoutがonPreDraw()
で走る時に、onChildViewsChanged()
が呼ばれるが、このメソッドの中で先ほど格納したmBehavior
をgetしてonDependentViewChanged()
をコールする。
final void onChildViewsChanged(@DispatchChangeEvent final int type) { // Update any behavior-dependent views for the change for (int j = i + 1; j < childCount; j++) { final View checkChild = mDependencySortedChildren.get(j); final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams(); final Behavior b = checkLp.getBehavior(); switch (type) { case EVENT_VIEW_REMOVED: break; default: // typeはEVENT_PRE_DRAWなのでdefaultに入る // Otherwise we dispatch onDependentViewChanged() handled = b.onDependentViewChanged(this, checkChild, child); break; }
onDependentViewChanged()
が呼ばれた後はいくつかのメソッドを経由してAppBarLayout.ScrollingViewBehavior#offsetChildAsNeeded()
に辿り着く。この中でdependencyであるAppBarLayoutのbottom位置を取得して重ならないように良い感じに調整してくれる。
public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior { private void offsetChildAsNeeded(View child, View dependency) { final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior(); if (behavior instanceof BaseBehavior) { // Offset the child, pinning it to the bottom the header-dependency, maintaining // any vertical gap and overlap final BaseBehavior ablBehavior = (BaseBehavior) behavior; ViewCompat.offsetTopAndBottom( child, (dependency.getBottom() - child.getTop()) + ablBehavior.offsetDelta + getVerticalLayoutGap() - getOverlapPixelsForOffset(dependency)); } }
というわけで一連の処理の起点にはmBehavior
が必要だ。しかし、最初に見た通りmBehavior
はCoordinatorLayout.LayoutParams
のコンストラクタで取得される。fragmentで独立したxmlを持ってそこからinflateしたfragmentをコードから動的にattachする場合、fragmentのLayoutParamsはCoordinatorLayout.LayoutParams
にならないので1mBehavior
が取得できない。
そのためapp:layout_behavior
を記述したにも関わらずoffsetが正しく設定されずviewが重なって表示されていた。
で、どうすれば良い?
CoordinatorLayout.LayoutParams
からbehaviorが取得できれば良いので、CoordinatorLayout
のchildにapp:layout_behavior
をつけるしかないと思う。以下のようなStack Overflowの記事を見つけたけれどそもそもLayoutParamsがCoordinatorLayout.LayoutParams
にcastできないのでハイハイ解散という感じだ。
そもそもなんでこんなことやりたかったかというと、containerにattachするfragmentによってAppBarLayoutがscrollしたりしなかったりという挙動を実装したくて、fragmentの中にapp:layout_behavior
が書けるのであればそっちでscroll制御できるので楽そうという理由だった。
楽はできなさそうなのでattachするfragmentによってscrollFragを変更してやるしかなさそう。
// fragmentをattach/replaceするタイミング val text = findViewById<TextView>(R.id.text) // `app:layout_scrollFlags` を設定していたview val layoutParam = text.layoutParams as? AppBarLayout.LayoutParams ?: return val scrollFrag = layoutParam.scrollFlags if ((scrollFrag and AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL) == 1) { // もしもscrollのオプションが付いていたらとる(scrollしないようにする) layoutParam.scrollFlags = scrollFrag - AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL }
Kotlin Bytecodeを読みたいけど、何から勉強したらいいかわからないあなたへ。ASMのドキュメントを読みましょう。
はじめに
このblog postは半年以上下書き放置していて自分の中では旬がすぎた話題ではありますが、最近Android系日本語技術podcastであるdex.fmのep.66でbytecodeが話題になったので、勿体無い精神で公開します :pray: それではどうぞ。
Kotlin書いているとたまに「これ、JavaのBytecodeだとどう変換されて動いているんだろう?」と思う時がしばしばある。
例えばこれとか
で、Android Studioの "Show Kotlin Bytecode" 機能を使ってbytecodeをのぞいてみるんだけど、完全に雰囲気で読んでいたので、わかるようなわからないようなゆるふわな感じだった😇これの意味は?とか聞かれても説明するなんて無理〜〜と言う程度の理解レベル。
Javaのbyte codeの読み方ちゃんと勉強したいな〜 `L0` とか `L1` とかよくわかってない。byte code勉強会誰かやってくれないかな…
— むーむー/Atsuko FUKUI (@muumuumuumuu) 2018年6月24日
何がつらいかっていうと、ググってもいい感じのリファレンスがないというか、まず何を読むべきかが見つけられないという状況😩😩😩 しかし軽い気持ちで呟いた結果、神々が降臨した。圧倒的感謝 :pray: 1
あの L0 とか L1 は、対応するブロックを表すための表記で Java のバイトコードの仕様とは別な気がします。 javap -c -l <クラスファイル> で表示すると、Bytecode Viewer との対応がすこし分かりやすくなるかもしれません。
— Hiroshi Kurokawa (@hydrakecat) 2018年6月24日
L0とかラベリングが入るのはjavapでなくasmを使ってるときの可視形態なので、https://t.co/sRtLnFl7Np を読むともっとわかりやすいと思います
— おなかすいた (@red_fat_daruma) 2018年6月24日
というわけで、Bytecodeに対してゆるふわな理解しかない人が勉強して行った記録を残していく。間違ったこと書いてたら教えてくださいmm
手取り早くbytecodeを理解した人へ
ASM User GuideのAppendixにBytecode instructionsがまとまっているので、ここを読みましょう。
ASMとは?
まずは公式の定義から。
ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).
KotlinのコンバイラはASMライブラリを使ってbytecodeを生成している。なのでKotlinで生成されたBytecodeを読みたいだけだったら、ASMが何をしてどういう生成物を吐き出すか知っていれば良いっぽい。
余談だけどASMはOSSで、しかもGithubではなくGitlabでhostingされている。
User Guide
まずは使う側としてUser Guide読むことにする。めちゃめちゃ長いPDFになっている。
https://asm.ow2.io/asm4-guide.pdf
読んでて面白かった部分を書いていく。なんかたまにルー大柴っぽくなった気がする。気になる人は原文読んでください…
1. Introduction
- ASMはできるだけ早くて小さくて堅牢というのを目指している
- ASMライブラリの目的はbyte arrayで表現されるコンパイルずみのJava classesを生成、変換、分析すること
- class loading processは対象外
- ASMという名前に意味はない。Cの
__asm__
keyword からとっただけ - core APIはevent basedなクラス表現
- tree APIはobject basedなクラス表現
- この辺 見ると、Kotlin compilerはtreeの方使ってるのかな?という感じがする
Tree API
(2. Classes
3. Methods
4. Metadata
5. Backward compatibility
の章はCore APIについてだったのでスキップ)
6. Classes
7. Method
- methodの中身はInsnListでinstructionが記述されていく
- こいつが
getOpCode()
とかを持っている getNext()
があるからjumpが簡単にできる
- こいつが
- Label, frame, line numberもinstructionではないけど
AbstractInsnNode
のサブクラスとして表現される- これにより実instructionsの前とかにLabelとかをinsertすることができる
8. Method Analysis
- ASMで使われるcode analysis techniqueには
data flow analysis
とcontrol flow analysis
がある data flow analysis
- methodのexecution frameのstateを計算する。
- forward analysisとbackward analysisがある
- stackから値をpopして計算して結果をstackにpush
- interpreterやJVMのように見えるが、違いは可能性がある全てのpath, argumentについてsimulateする
- manipulated valueは可能性がある値の集合になるのでめっちゃ大きくなる。
- 例)intergerをP="positive or null", N="negative or null, A="all integers" として表すと
IADD
instructionはoperandがどちらもPだったらPを返し、どちらもNだったらNを返し、そのほかのケースは全てAを返す
control flow analysis
- methodのcontrol data flow graphを計算し、このgraphを解析する
- graphはbasic blockにdecompileされる。それぞれのbasic blockは(最初のblockを除き)jumpの対象となりうる。
- Interface and components
- stackからのpopとかpushはframeworkで、valueをcombineしたり集合の計算とかはInterpreterとかValueと行ったuserが定義したsubclassで行われる
- Basic Data Flow Analysis
definition | means |
---|---|
UNINITIALIZED_VALUE | all possible values |
INT_VALUE | all int, short, byte, boolean or char values |
FLOAT_VALUE | all float values |
LONG_VALUE | “all long values” |
DOUBLE_VALUE | “all double values” |
REFERENCE_VALUE | “all object and array values” |
RETURNADDRESS_VALUE | is used for subroutines (see Appendix A.2) |
- Appendix
- 上記にも書いたが、ここにinstructionsとその操作例がまとまっているのでここを見ればだいたいbytecodeが読めるようになっている
Developer Guide読んでいく
こっちの方が短いけど難しい。3割くらいしか理解できていない気がするのでさらっとメモだけ。 ASM - Developer Guide
Main Algorithms
- class loader
- constant poolと(constructorの中の)bootstrap methodをparseする
- classをparseする
- class attributeをparseする
- class attributeに対応したvisit methodをコールする
- fieldについて上記と同じようなことをする
- methodについて同上
- ただしattributeはparseした後local variableにstore
- labelを探してstore
- instructionsをparse
- 3.4.3 An example
- 要約するのがむずいのでこの実例を見てくれたらなんとなくわかる
以上、わかるようなわからないような状態から一歩踏み出すためのリファレンス集でした。
TabItemをxml layout fileに記述してもview hierarchyからfindViewById()できない理由
TabItem、xml layout fileに書いててもViewHierarchyに追加されないの知らなくて昨日すごく時間を無駄にした😩😩😩https://t.co/hc8WyqfHV2
— むーむー/Atsuko FUKUI (@muumuumuumuu) January 31, 2019
Tweetにリンクを貼っている通り、公式ドキュメントによると
TabItem is a special 'view' which allows you to declare tab items for a TabLayout within a layout. This view is not actually added to TabLayout, it is just a dummy which allows setting of a tab items's text, icon and custom layout.
This view (Layout fileに定義されたTabItem)はただのダミーで、text, icon, それからcustom layoutの設定だけができるとある。
どうやって実装しているのか気になったので、内部実装を調べたのでメモ。
知りたいこと
こんな感じでTabItemをTabLayoutのchild viewとして記述しても、
<android.support.design.widget.TabLayout android:id="@+id/tab" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <android.support.design.widget.TabItem android:id="@+id/tabItem" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.design.widget.TabLayout>
コードからアクセスしようとしたら(ちなみにIDEはR.idを認識してくれる)
NPEが発生する。
java.lang.NullPointerException: Attempt to read from field 'android.graphics.drawable.Drawable android.support.design.widget.TabItem.icon' on a null object reference
ViewHierarchyに存在する(?)のにidからviewが引けない仕組みを調べる。
TabLayoutの実装
例によってAndroidXではなくsupport libraryのコードを読んだ結果ですごめんなさい 😇
まず前提としてTabLayout
はHorizontalScrollView
をextendsしたviewである。でHorizontalScrollView
はViewGroup
なので、addView()
メソッドをコールされることでchild viewを持つことができる。(LayoutInflatorがxmlを読み込んでview hierarchyを構成するときにparent viewに対してこのaddView()
が呼ばれる。)
というわけでTabLayout#addView()
を見て行く。
public void addView(View child) { this.addViewInternal(child); } public void addView(View child, int index) { this.addViewInternal(child); } public void addView(View child, android.view.ViewGroup.LayoutParams params) { this.addViewInternal(child); } public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) { this.addViewInternal(child); }
こんな感じでただaddViewInternal()
をコールしているだけ。このaddViewInternal()
は何をしているかというと、
private void addViewInternal(View child) { if (child instanceof TabItem) { this.addTabFromItemView((TabItem)child); } else { throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout"); } }
childの型を調べてTabItem
だったらaddTabFromItemView()
を呼んでいる。
private void addTabFromItemView(@NonNull TabItem item) { TabLayout.Tab tab = this.newTab(); if (item.text != null) { tab.setText(item.text); } if (item.icon != null) { tab.setIcon(item.icon); } if (item.customLayout != 0) { tab.setCustomView(item.customLayout); } if (!TextUtils.isEmpty(item.getContentDescription())) { tab.setContentDescription(item.getContentDescription()); } this.addTab(tab); }
ここで面白いのはTabItem
はViewをextendsしたclassなのに、ただのdataを保持するだけのクラスのように扱われている。xmlからattributeSetを引回す必要があるのでViewである必要があるのだろうけど。
public class TabItem extends View { public final CharSequence text; public final Drawable icon; public final int customLayout; public TabItem(Context context) { this(context, (AttributeSet)null); } public TabItem(Context context, AttributeSet attrs) { super(context, attrs); TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, styleable.TabItem); this.text = a.getText(styleable.TabItem_android_text); this.icon = a.getDrawable(styleable.TabItem_android_icon); this.customLayout = a.getResourceId(styleable.TabItem_android_layout, 0); a.recycle(); } }
生成したTabLayout.Tab
に対して保持していたtext, icon, customLayout, contentDescriptionの4種類の情報のみをセットしてaddTab()
している。
ここでidが捨てられているので、実際にview hierarchyに追加されたtabをidからfindできない仕組みになっていたのだ。
以下は余談になるが、この辺りは読んでいて色々面白い。
TabLayout.Tabを生成する
newTab()`メソッドはpoolを利用してrecycleしている。
private static final Pool<TabLayout.Tab> tabPool = new SynchronizedPool(16); private final Pool<TabLayout.TabView> tabViewPool; public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); ... this.tabViewPool = new SimplePool(12); ... } @NonNull public TabLayout.Tab newTab() { TabLayout.Tab tab = this.createTabFromPool(); tab.parent = this; tab.view = this.createTabView(tab); return tab; } protected TabLayout.Tab createTabFromPool() { TabLayout.Tab tab = (TabLayout.Tab)tabPool.acquire(); if (tab == null) { tab = new TabLayout.Tab(); } return tab; }
tabPool
とtabViewPool
のmaxPoolSizeが16と12で異なっているのはなんでなんだろう?