通常在实际开发中,为了适配,我们会把View设置为match_parent或者wrap_content、又或者是设置weight权重来分配宽高,而不是使用具体值。那么就出现一个问题了,如果动态测量View的实际宽高。
其实这个问题网上有很多解决方法,无奈给出的方法众多,却没有解析,实际可能根本不管用。所以详细记录下这个问题。
测量宽高的API
我们以高度为例子:
android提供了两个API来动态获取View的高:
- view.getMeasuredHeight()
- view.getHeight()
那么这两个API的区别是什么呢?
简单来说就是:
- view.getMeasuredHeight() 是由view中的测量方法赋值的,这个值包含了隐藏的高度(比如一个view部分超出屏幕,他也会计算出来)。
- view.getHeight() 由view的底部位置减去顶部位置,即实际显示的View的高度,不包含隐藏了的高度。
如果是在View的onMeasure方法后执行上面两个方法,会发现可以得到正确的高度,但是如果是在onCreate等方法中执行就会发现返回的值不正确或者为0,这是为什么呢?
Activity和View的混合生命周期
我画了一个图来表明两者的生命周期先后问题:
可以看到的是onCreate()和onResume()是在onMeasure方法之前。
说明此时还没有测量出实际的宽高,还没有进行绘制,所以调用上述的两个API会出现值为0或者数值错误的情况。
经过验证的解决方法
下面的方法都是进过验证的,但是有使用的条件限制,务必注意。
1.主动测量
代码:
view.measure(, );
view.getMeasuredWidth();
view.getMeasuredHeight();
说明:
onMeasure传入的两个参数是由父控件的大小,
也可以使用
View.MeasureSpec.makeMeasureSpec(0,mode);
设置值
其中mode可以选择
- MeasureSpec.UNSPECIFIED 未指定尺寸,比如listview中尺寸由父控件决定
- MeasureSpec.EXACTLY 适合match_parent或者具体值
- MeasureSpec.AT_MOST 适合wrap_content不确定值
注意:
这种方法不一定能测出正确的值,因为onMesure会多次调用(由于onMesure自上而下,父控件如果对于子控件的宽高不满意,即如子控件没有限制宽高,父控件会重新调用onMesure重新测量),所以测量结果不一定正确。
2.使用OnPreDrawListener
代码:
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();
return true;
}
});
说明:
这个是异步解决方法,在onDraw之前执行,onDraw方法执行在onMeasure之后,所以这个监听器可以解决这个问题。
注意:
onPreDraw方法只有return true才会生效,由于onDraw会在view绘制时调用,所以可能会调用多次,务必调用removeOnPreDrawListener方法,防止执行效率问题。onDraw会多次执行,每次view出现都会调用,但onLayout不会,测量后会被记录下来。
3.使用OnGlobalLayoutListener
代码:
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int height = img.getWidth();
int width = img.getHeight();
}
});
说明:
这个也是异步解决方法,在全局onLayout触发时执行。
注意:
这个方法有一定不确定性,而且onLayout执行第一次的数值不一定正确。
4.使用OnLayoutChangeListener
代码:
view.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
}
});
说明:
这个方法OnLayout时调用,而且只有改变时才会触发。
注意:
这个方法要求API level 11
5.低版本API使用OnLayoutChangeListener兼容方法
低版本的可以通过自定义控件,这里以ImageView为例,个人设计的自定义ImageView控件:
/**
* 封装OnLayoutChangeListener的ImageView,为兼容低版本
*/
public class LayoutImageView extends ImageView {
public LayoutImageView(Context context) {
super(context);
}
public LayoutImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 兼容低版本的监听器集合
*/
private ArrayList<SupportOnLayoutChangeListener> onLayoutChangeListeners;
/**
* 添加布局改变监听器
* @param listener 监听器
*/
public void addOnLayoutChangeListener(SupportOnLayoutChangeListener listener) {
if (onLayoutChangeListeners == null) {
onLayoutChangeListeners = new ArrayList<SupportOnLayoutChangeListener>();
}
if (!onLayoutChangeListeners.contains(listener)) {// 不重复添加
onLayoutChangeListeners.add(listener);
}
}
/**
* 移除监听器
* @param listener 监听器
*/
public void removeOnLayoutChangeListener(
SupportOnLayoutChangeListener listener) {
if (onLayoutChangeListeners == null || listener == null) {
return;
}
onLayoutChangeListeners.remove(listener);;// 先不删除
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (onLayoutChangeListeners != null) {
@SuppressWarnings("unchecked")
ArrayList<SupportOnLayoutChangeListener> listenersCopy = (ArrayList<SupportOnLayoutChangeListener>) onLayoutChangeListeners
.clone();//使用克隆集合,防止多线程操作同一集合问题
int numListeners = listenersCopy.size();
for (int i = ; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this);
}
}
}
/**
* 兼容低版本的布局改变监听器
*/
public interface SupportOnLayoutChangeListener {
/**
* 布局改变
* @param thisView 改变的View
*/
public void onLayoutChange(LayoutImageView thisView);
}
}
上述代码是参考了谷歌View上的addOnLayoutChangeListener等方法的写法,其中clone集合这种设计方法非常值得借鉴,可以解决多线程访问同一个集合,又不用考虑同步问题。
这样就可以在低版本中使用兼容的addOnLayoutChangeListener,当然下面的imageView是自定义的LayoutImageView类型:
imageView.addOnLayoutChangeListener(
new SupportOnLayoutChangeListener() {
@Override
public void onLayoutChange(LayoutImageView thisView) {
thisView.removeOnLayoutChangeListener(this);
}
});
声明
原创文章,欢迎转载,请保留出处。
有任何错误、疑问或者建议,欢迎指出。
我的邮箱:[email protected]
参考文章
http://blog.csdn.net/crazy1235/article/details/41806079