天天看点

如何在onCreate中测量View的实际宽高

通常在实际开发中,为了适配,我们会把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中测量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

继续阅读