在 Android 上使用 RelativeLayout 时的双重征税

Double Taxation when using a RelativeLayout on Android

为了在Android上理解Double Taxation,我写了下面的代码,非常简单。有一个 RelativeLayout 和三个 TextView

<?xml version="1.0" encoding="utf-8"?>
<ru.maksim.sample_app.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ru.maksim.sample_app.MainActivity">

    <ru.maksim.sample_app.MyTextView
        android:id="@+id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#fca"
        android:tag="text1"
        android:text="Text 1" />

    <ru.maksim.sample_app.MyTextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/text1"
        android:background="#acf"
        android:tag="text2"
        android:text="Text 2" />

    <ru.maksim.sample_app.MyTextView
        android:id="@+id/text3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/text1"
        android:layout_toRightOf="@id/text2"
        android:background="#fac"
        android:tag="text3"
        android:text="text 3" />

</ru.maksim.sample_app.MyRelativeLayout>

MyTextView

public class MyTextView extends android.support.v7.widget.AppCompatTextView {

    private static final String TAG = "MyTextView";

    public MyTextView(Context context) {
        super(context);
    }

    public MyTextView(Context context,
                      @Nullable AttributeSet attrs
    ) {
        super(context, attrs);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
        Log.d(TAG,
              "onMeasure, "
                      + getTag()
                      + " widthMeasureSpec=" + MeasureSpecMap.getName(widthMode)
                      + " heightMeasureSpec=" + MeasureSpecMap.getName(heightMode)
        );
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.d(TAG, getTag() + " onLayout");
    }
}

MyRelativeLayout

public class MyRelativeLayout extends RelativeLayout {

    public static final String TAG = "MyRelativeLayout";

    public MyRelativeLayout(Context context) {
        super(context);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
        Log.d(TAG,
              "onMeasure, "
                      + getTag()
                      + " widthMeasureSpec=" + MeasureSpecMap.getName(widthMode)
                      + " heightMeasureSpec=" + MeasureSpecMap.getName(heightMode)
        );
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.d(TAG, getTag() + " onLayout");
    }
}

Logcat:

09-11 19:25:40.077 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text2 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
09-11 19:25:40.078 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text3 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
09-11 19:25:40.078 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.078 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.078 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text3 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.078 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text2 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.078 7732-7732/ru.maksim.sample_app D/MyRelativeLayout: onMeasure, null widthMeasureSpec=EXACTLY heightMeasureSpec=EXACTLY

                                                                      [ 09-11 19:25:40.098  7732: 7748 D/         ]
                                                                      HostConnection::get() New Host Connection established 0xa0a8fbc0, tid 7748
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text2 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text3 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text3 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: onMeasure, text2 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyRelativeLayout: onMeasure, null widthMeasureSpec=EXACTLY heightMeasureSpec=EXACTLY
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: text1 onLayout
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: text2 onLayout
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyTextView: text3 onLayout
09-11 19:25:40.132 7732-7732/ru.maksim.sample_app D/MyRelativeLayout: null onLayout

现在让我们将 MyRelativeLayout 替换为 LinearLayoiut 的 child,名为 MyLinearLayout:

<?xml version="1.0" encoding="utf-8"?>
<ru.maksim.sample_app.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="ru.maksim.sample_app.MainActivity">

    <ru.maksim.sample_app.MyTextView
        android:id="@+id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#fca"
        android:tag="text1"
        android:text="Text 1" />

    <ru.maksim.sample_app.MyTextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#acf"
        android:tag="text2"
        android:text="Text 2" />

    <ru.maksim.sample_app.MyTextView
        android:id="@+id/text3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#fac"
        android:tag="text3"
        android:text="text 3" />

</ru.maksim.sample_app.MyLinearLayout>

MyLinearLayout

public class MyLinearLayout extends LinearLayout {

    public static final String TAG = "MyLinearLayout";

    public MyLinearLayout(Context context) {
        super(context);
    }

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
        Log.d(TAG,
              "onMeasure, "
                      + getTag()
                      + " widthMeasureSpec=" + MeasureSpecMap.getName(widthMode)
                      + " heightMeasureSpec=" + MeasureSpecMap.getName(heightMode)
        );
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.d(TAG, getTag() + " onLayout");
    }
}

这是我现在在 logcat 中看到的内容:

09-11 19:50:57.974 2781-2781/? D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:50:57.974 2781-2781/? D/MyTextView: onMeasure, text2 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
09-11 19:50:57.974 2781-2781/? D/MyTextView: onMeasure, text3 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:50:57.974 2781-2781/? D/MyLinearLayout: onMeasure, null widthMeasureSpec=EXACTLY heightMeasureSpec=EXACTLY

                                                 [ 09-11 19:50:58.004  2781: 2817 D/         ]
                                                 HostConnection::get() New Host Connection established 0xa5ec1940, tid 2817
09-11 19:50:58.017 2781-2781/? D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:50:58.017 2781-2781/? D/MyTextView: onMeasure, text2 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
09-11 19:50:58.017 2781-2781/? D/MyTextView: onMeasure, text3 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
09-11 19:50:58.017 2781-2781/? D/MyLinearLayout: onMeasure, null widthMeasureSpec=EXACTLY heightMeasureSpec=EXACTLY
09-11 19:50:58.017 2781-2781/? D/MyTextView: text1 onLayout
09-11 19:50:58.017 2781-2781/? D/MyTextView: text2 onLayout
09-11 19:50:58.017 2781-2781/? D/MyTextView: text3 onLayout
09-11 19:50:58.017 2781-2781/? D/MyLinearLayout: null onLayout
上面两个例子中使用的

MeasureSpecMap 只包含以下 Map:

public class MeasureSpecMap {

    private static final Map<Integer, String> MAP = new HashMap<>();

    static {
        MAP.put(View.MeasureSpec.AT_MOST, "AT_MOST");
        MAP.put(View.MeasureSpec.EXACTLY, "EXACTLY");
        MAP.put(View.MeasureSpec.UNSPECIFIED, "UNSPECIFIED");
    }

    private MeasureSpecMap() {

    }

    public static String getName(int mode) {
        return MAP.get(mode);
    }

}

问题一

在使用MyRelativeLayout时,为什么系统需要在调用MyRelativeLayoutonMeasure之前对每个child调用两次onMeasureMyLinearLayout 在我的示例中,每个 child 都被测量一次,如您在上面的日志输出中所见。

问题二

这是 the Double taxation section 中我不明白的其他内容:

when you use the RelativeLayout container, which allows you to position View objects with respect to the positions of other View objects, the framework performs the following actions:

Executes a layout-and-measure pass, during which the framework calculates each child object’s position and size, based on each child’s request. Uses this data, also taking object weights into account, to figure out the proper position of correlated views.

Uses this data, also taking object weights into account, to figure out the proper position of correlated views.

但是等等...他们不是在谈论 android:layout_weight 这是 LinearLayout 的一个功能吗?


上面的代码也可以在 GitHub:

The approach with MyLinearLayout

The approach with MyRelativeLayout

Question 1.

When using MyRelativeLayout, why does the system need to call onMeasure on each child twice before onMeasure of MyRelativeLayout is called? With MyLinearLayout each child in my example is measured once as you can see in the log output above.

用(过于)简单的术语来说,就是一个视图的大小和位置会影响另一个视图的大小和位置。

考虑text3:它的宽度不仅取决于它持有的文本的长度,还取决于text2的宽度;如果 text2 占用 80% 的屏幕,那么 text3 只会(最多)获得 20% 的屏幕。

因此,系统会执行第一个测量传递以找出视图将相互施加的“约束”,然后执行第二个测量传递以找出要使用的最终值。

查看您的日志输出(为简洁起见省略了一些文本):

D/MyTextView: onMeasure, text2 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
D/MyTextView: onMeasure, text3 widthMeasureSpec=AT_MOST heightMeasureSpec=AT_MOST
D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST

D/MyTextView: onMeasure, text1 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
D/MyTextView: onMeasure, text3 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST
D/MyTextView: onMeasure, text2 widthMeasureSpec=EXACTLY heightMeasureSpec=AT_MOST

看看第一次text2是怎么测的,widthMeasureSpecAT_MOST模式,第二次是EXACTLY模式?第一遍让 text2 有机会使用最多 100% 的屏幕宽度,但第二遍将其限制为实际需要的大小。

而对于垂直 LinearLayout,系统可以在一次测量过程中获得它需要知道的一切。 text1 允许达到完整的 window 高度,text2 允许达到完整的 window 高度减去 text1 的高度,依此类推。 =34=]

Question 2.

...

But wait... Aren't they talking about android:layout_weight which is a feature of LinearLayout?

这部分我也不明白。我倾向于相信 CommonsWare 的猜测,即它只是一个文档错误。