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

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

TabItemをxml layout fileに記述してもview hierarchyからfindViewById()できない理由

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を認識してくれる) f:id:muumuumuumuu:20190211155020p:plain

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のコードを読んだ結果ですごめんなさい 😇

まず前提としてTabLayoutHorizontalScrollViewをextendsしたviewである。でHorizontalScrollViewViewGroupなので、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;
    }

tabPooltabViewPoolのmaxPoolSizeが16と12で異なっているのはなんでなんだろう?