自定义视图的onMeasure:如何根据高度获取宽度
Custom view's onMeasure: how to get width based on height
我之前的问题太罗嗦了。人们无法理解它,所以下面是一个完整的重写。如果您对旧版本感兴趣,请参阅edit history。
A RelativeLayout
parent 将 MeasureSpec
s 发送到其 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
运行照原样,最后通过设置mStickyWidth
对View
在onLayout
的边界进行判断] 到我们将接受的新的最小宽度。在 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;
}
我之前的问题太罗嗦了。人们无法理解它,所以下面是一个完整的重写。如果您对旧版本感兴趣,请参阅edit history。
A RelativeLayout
parent 将 MeasureSpec
s 发送到其 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
运行照原样,最后通过设置mStickyWidth
对View
在onLayout
的边界进行判断] 到我们将接受的新的最小宽度。在 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;
}