自定义视图的onMeasure:如何根据高度获取宽度

Custom view's onMeasure: how to get width based on height

我之前的问题太罗嗦了。人们无法理解它,所以下面是一个完整的重写。如果您对旧版本感兴趣,请参阅edit history

A RelativeLayout parent 将 MeasureSpecs 发送到其 child 视图的 onMeasure 方法,以查看 child 需要多大成为。这发生在几次传递中。

我的自定义视图

我有一个custom view。随着视图内容的增加,视图的高度也会增加。当视图达到 parent 允许的最大高度时,视图的宽度会随着任何其他内容而增加(只要为宽度选择了 wrap_content)。因此,自定义视图的宽度直接取决于parent所说的最大高度。

一次(不和谐)parent child 对话

onMeasure传1

RelativeLayout parent 告诉我的观点,“你可以任意宽度 up to 900 and any height up to 600。你说呢?”

我的观点是,“好吧,在那个高度,我可以容纳宽度为 100 的所有东西。所以我将采用 100 的宽度和 [=17] 的高度=]."

onMeasure传2

RelativeLayout parent 告诉我的观点,“你上次告诉我你想要 100 的宽度,所以让我们将其设置为 exact width. Now, based on that width, what kind of height would you like? Anything up to 500 没问题。"

“嘿!”我的观点回复。 “如果您只给我 500 的最大高度,那么 100 就太窄了。对于那个高度,我需要 200 的宽度。但是没关系,随你便。我不会违反规则 (yet...)。我将采用 100 的宽度和 500 的高度。“[=39] =]

最终结果

RelativeLayout parent 为视图分配最终尺寸 100 宽度和 500 高度。这当然对于视图来说太窄了,部分内容被剪掉了。

“叹息”,我认为。 “为什么我的 parent 不让我变宽?有足够的空间。也许 Stack Overflow 上的人可以给我一些建议。”

要制作自定义布局,您需要阅读并理解这篇文章https://developer.android.com/guide/topics/ui/how-android-draws.html

实现您想要的行为并不困难。您只需要在自定义视图中覆盖 onMeasure 和 onLayout。

在 onMeasure 中,您将获得自定义视图的最大可能高度,并在循环中调用 measure() childs。在 child 测量后从每个 child 获得所需的高度并计算 child 是否适合当前列,如果不增加新列的自定义视图宽度。

在 onLayout 中,您必须为所有 child 视图调用 layout() 以设置它们在 parent 中的位置。这个位置你之前在onMeasure里计算过。

更新:修改代码以修复一些问题。

首先,让我说你问了一个很好的问题并且很好地阐述了问题(两次!)这是我的解决方案:

似乎 onMeasure 发生了很多事情,从表面上看,这并没有多大意义。既然如此,我们就让onMeasure运行照原样,最后通过设置mStickyWidthViewonLayout的边界进行判断] 到我们将接受的新的最小宽度。在 onPreDraw 中,使用 ViewTreeObserver.OnPreDrawListener,我们将强制使用另一个布局 (requestLayout)。来自 documentation(强调已添加):

boolean onPreDraw ()

Callback method to be invoked when the view tree is about to be drawn. At this point, all views in the tree have been measured and given a frame. Clients can use this to adjust their scroll bounds or even to request a new layout before drawing occurs.

onLayout 中设置的新最小宽度现在将由 onMeasure 强制执行,后者现在更智能地了解可能的情况。

我已经用您的示例代码对此进行了测试,它似乎工作正常。它将需要更多的测试。可能还有其他方法可以做到这一点,但这是方法的要点。

CustomView.java

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;

public class CustomView extends View
        implements ViewTreeObserver.OnPreDrawListener {
    private int mStickyWidth = STICKY_WIDTH_UNDEFINED;

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        logMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
        int desiredHeight = 10000; // some value that is too high for the screen
        int desiredWidth;

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        // Height
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(desiredHeight, heightSize);
        } else {
            height = desiredHeight;
        }

        // Width
        if (mStickyWidth != STICKY_WIDTH_UNDEFINED) {
            // This is the second time through layout and we are trying renogitiate a greater
            // width (mStickyWidth) without breaking the contract with the View.
            desiredWidth = mStickyWidth;
        } else if (height > BREAK_HEIGHT) { // a number between onMeasure's two final height requirements
            desiredWidth = ARBITRARY_WIDTH_LESSER; // arbitrary number
        } else {
            desiredWidth = ARBITRARY_WIDTH_GREATER; // arbitrary number
        }

        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(desiredWidth, widthSize);
        } else {
            width = desiredWidth;
        }

        Log.d(TAG, "setMeasuredDimension(" + width + ", " + height + ")");
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int w = right - left;
        int h = bottom - top;

        super.onLayout(changed, left, top, right, bottom);
        // Here we need to determine if the width has been unnecessarily constrained.
        // We will try for a re-fit only once. If the sticky width is defined, we have
        // already tried to re-fit once, so we are not going to have another go at it since it
        // will (probably) have the same result.
        if (h <= BREAK_HEIGHT && (w < ARBITRARY_WIDTH_GREATER)
                && (mStickyWidth == STICKY_WIDTH_UNDEFINED)) {
            mStickyWidth = ARBITRARY_WIDTH_GREATER;
            getViewTreeObserver().addOnPreDrawListener(this);
        } else {
            mStickyWidth = STICKY_WIDTH_UNDEFINED;
        }
        Log.d(TAG, ">>>>onLayout: w=" + w + " h=" + h + " mStickyWidth=" + mStickyWidth);
    }

    @Override
    public boolean onPreDraw() {
        getViewTreeObserver().removeOnPreDrawListener(this);
        if (mStickyWidth == STICKY_WIDTH_UNDEFINED) { // Happy with the selected width.
            return true;
        }

        Log.d(TAG, ">>>>onPreDraw() requesting new layout");
        requestLayout();
        return false;
    }

    protected void logMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        String measureSpecHeight;
        String measureSpecWidth;

        if (heightMode == MeasureSpec.EXACTLY) {
            measureSpecHeight = "EXACTLY";
        } else if (heightMode == MeasureSpec.AT_MOST) {
            measureSpecHeight = "AT_MOST";
        } else {
            measureSpecHeight = "UNSPECIFIED";
        }

        if (widthMode == MeasureSpec.EXACTLY) {
            measureSpecWidth = "EXACTLY";
        } else if (widthMode == MeasureSpec.AT_MOST) {
            measureSpecWidth = "AT_MOST";
        } else {
            measureSpecWidth = "UNSPECIFIED";
        }

        Log.d(TAG, "Width: " + measureSpecWidth + ", " + widthSize + " Height: "
                + measureSpecHeight + ", " + heightSize);
    }

    private static final String TAG = "CustomView";
    private static final int STICKY_WIDTH_UNDEFINED = -1;
    private static final int BREAK_HEIGHT = 1950;
    private static final int ARBITRARY_WIDTH_LESSER = 200;
    private static final int ARBITRARY_WIDTH_GREATER = 800;
}