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

时间:2017-02-22 11:42:41

标签: android android-custom-view android-relativelayout onmeasure

我的问题的上一个版本过于冗长。人们无法理解它,所以以下是完全重写。如果您对旧版本感兴趣,请参阅edit history

RelativeLayout父母将MeasureSpecs发送到其子视图的onMeasure方法,以查看孩子想要的大小。这发生在几次通过中。

我的自定义视图

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

enter image description here

(不和谐的)父母对话

onMeasure传递1

RelativeLayout父母告诉我的观点,"您可以是任意宽度up to 900和任何身高up to 600。你怎么说?"

我的观点是,"嗯,在那个高度,我可以适应宽度为100的所有东西。所以我的宽度为100,高度为600。"

onMeasure传递2

RelativeLayout父母告诉我的观点,"您上次告诉我您希望宽度为100,所以让我们将其设为exact宽度。现在,基于那个宽度,你想要什么样的高度?任何up to 500都可以。"

"嘿&#34!;我的观点回复。 "如果你只给我一个500的最大高度,那么100就太窄了。我需要宽度为200的高度。但很好,按你自己的方式行事。我不会违反规则(还有......)。我的宽度为100,高度为500。"

最终结果

RelativeLayout父级为视图指定宽度为100的最终尺寸,为高度指定500。对于视图来说,这当然太窄了,部分内容会被剪裁。

"叹息,"认为我的看法。 "为什么我的父母会让我变宽?有足够的空间。也许Stack Overflow上的某个人可以给我一些建议。"

2 个答案:

答案 0 :(得分:9)

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

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

onMeasure开始,似乎有很多事情表面上没有多大意义。既然如此,我们会让onMeasure按原样运行,最后通过将View设置为onLayout来判断mStickyWidth的{​​{1}}界限我们接受的最小宽度。在onPreDraw中,使用ViewTreeObserver.OnPreDrawListener,我们将强制执行另一种布局(requestLayout)。从documentation(强调添加):

  

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

答案 1 :(得分:1)

要进行自定义布局,您需要阅读并理解本文https://developer.android.com/guide/topics/ui/how-android-draws.html

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

在onMeasure中,您将获得自定义视图的最大可能高度,并为周期中的子项调用measure()。儿童测量后从每个孩子获得所需的身高并计算出是否适合当前列,如果不为新列增加自定义视图范围。

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