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

22
The previous version of my question was too long and difficult to understand, so I have rewritten it. If you are interested in the old version, please refer to the edit history.
When a RelativeLayout parent wants to know how big its child view should be, it sends MeasureSpecs to the child's onMeasure method in multiple passes.
I have a custom view that increases in height as its content grows. Once it reaches the maximum height allowed by the parent, its width will increase for any additional content (if wrap_content is selected for the width). Therefore, the width of the custom view depends on the maximum height specified by the parent. Please keep the HTML tags and simply refine the content to make it more understandable. Please do not provide explanations and only provide translations.

enter image description here

一次(不和谐的)亲子谈话

onMeasure 第一次执行

RelativeLayout 父视图告诉我的视图:“你可以有任何宽度最大900,任何高度最大600。你怎么说?”

我的视图回答说:“好吧,在那个高度下,我可以用100的宽度来适应所有内容。所以我将采取100的宽度和600的高度。”

onMeasure 第二次执行

RelativeLayout父布局告诉我的视图:“你上次告诉我你需要宽度为100,那么我们就把它作为确切的宽度。现在,基于这个宽度,你需要什么样的高度?任何最多500的高度都可以。”
“嘿!” 我的视图回答道,“如果你只给我一个最大高度500,那么100的宽度就太窄了。我需要200的宽度来适应这个高度。但好吧,按照你的方式来吧。我不会违反规则(至少现在还没有...)。我将采取100的宽度和500的高度。” 最终结果: RelativeLayout 父布局为视图分配了最终大小,宽度为 100,高度为 500。这当然对于视图来说太窄了,部分内容被裁剪了。
视图想:“唉,为什么我的父布局不让我更宽呢?明明有足够的空间。也许 Stack Overflow 上的某个人可以给我一些建议。”

@Mohamed,这里的例子只使用了像素(px)。 - Suragch
当我在LinearLayout中使用您的自定义视图时,它可以正常工作。您试过了吗? - Krish
你能让你的自定义视图继承LinearLayout并包含TextView吗?(自定义视图由两个视图组成) - Raphau
@Raphau,我需要再考虑一下。我的初步反应是,如果两个复杂视图可以以这种方式合并在一起,那么肯定有一种方法可以使一个简单的视图具有类似的布局行为。 - Suragch
尝试按照这个教程的方法进行操作:http://trickyandroid.com/protip-inflating-layout-for-your-custom-view/,只需创建一个包含TextView的LinearLayout即可。请使用自己的layout.xml文件。 - Raphau
显示剩余5条评论
2个回答

13

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

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

似乎在onMeasure中有很多事情要做,但表面上并没有太多意义。因此,我们将让onMeasure按其本来的方式运行,并在最后通过设置mStickyWidth为我们接受的新最小宽度来对View的边界进行判断,在onLayout中。然后,在onPreDraw中,使用ViewTreeObserver.OnPreDrawListener,我们将强制执行另一个布局(requestLayout)。从文档中可以看到(加粗部分为重点):

boolean onPreDraw ()

当视图树即将绘制时调用的回调方法。这时,树中的所有视图都已测量并分配了框架。客户端可以使用此功能调整其滚动边界,甚至可在绘制发生之前请求新的布局

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;
}

我非常高兴看到一个真正解决了问题的答案。我已经要放弃希望了。 - Suragch
这是一个有趣的问题 - 坚持下去。当我与Android框架作斗争时,通常框架会获胜,但这一次我们可能会成功。 - Cheticamp
1
我已经在MCVE和我的实际自定义视图代码上进行了测试。现在两者都可以在我在问题中描述的情况下正常工作。 - Suragch
很高兴听到这个消息。我希望在进行一些调整后,它能够成为一个更通用的解决方案。 - Cheticamp
我不得不添加更多的检查,以确保在所需宽度大于最大可用宽度时不会调用重新布局,但到目前为止它似乎仍然工作良好。 - Suragch

1
要创建自定义布局,您需要阅读并理解本文https://developer.android.com/guide/topics/ui/how-android-draws.html
实现所需的行为并不困难。您只需要在自定义视图中重写onMeasure和onLayout。
在onMeasure中,您将获得自定义视图的最大可能高度,并循环调用measure()以测量子级。子级测量后,从每个子级获取所需高度,并计算子级是否适合当前列,如果不适合,则增加自定义视图宽度以添加新列。
在onLayout中,您必须为所有子视图调用layout()以在父视图内设置它们的位置。这些位置在之前的onMeasure中已经计算过了。

在我的onMeasure中,我会计算我的视图应该有多宽。然而,在我到达onLayout时,RelativeLayout父级已经选择了错误的大小(并非在所有情况下都是如此,但特别是当它位于另一个视图的下方并且内容刚好在某个高度上换行到第二列时)。实际代码在这里(或者在我的问题的编辑历史记录中)。 - Suragch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接