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

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

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じゃないとダメだった気がする。