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で異なっているのはなんでなんだろう?