Android 获取 View 宽高的常用正确方式,避免为零

18,523 阅读4分钟

相信有很多朋友都有过在 Activity 中通过 getWidth() 之类的方法获取 View 的宽高值,可能在 onCreate() 生命周期方法中,也可能在 onResume() 生命周期方法中。然而,不幸的是,并不能获取所要的结果,宽高值均为 0。

如果对 View 的绘制显示流程熟悉的话,就会知道问题所在。我们知道,在自定义 View 时,通常都要重写 onMeasure、onLayout、onDraw 这几个方法。同理,Activity 的内容显示到设备上时,这些 View 也要经历这些阶段。

所以,当我们在 Activity 的生命周期方法中直接获取 View 的宽高时,View 也许还没执行完 measure 阶段,那么自然获取到的宽高结果为 0。这也提醒我们一点,在 onCreate 方法中只适合做些一些初始化设置工作,使用 View 执行动画或者其他操作时,一定要注意考虑 View 绘制的耗时过程。

那么问题来了,怎么样才能在 Activity 代码中获取到 View 的实际宽高值呢?这里给大家总结一些常用方法。

addOnGlobalLayoutListener


view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        if (Build.VERSION.SDK_INT >= 16) {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }else {
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
        int width = view.getWidth();
    }
});

ViewTreeObserver,顾名思义,视图树的观察者,可以监听 View 的全局变化事件。比如,layout 变化,draw 事件等。可以阅读源码了解更多事件。

注意:使用时需要注意及时移除该事件的监听,避免后续每一次发生全局 View 变化均触发该事件,影响性能。这里用的是 OnGlobalLayoutListener,移除监听时注意 API 的版本兼容处理。

addOnPreDrawListener


view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        view.getViewTreeObserver().removeOnPreDrawListener(this);
        int width = view.getWidth();
        return false;
    }
});

这里同样是利用 ViewTreeObserver 观察者类,只不过这里监听的是 draw 事件。

view.post()


view.post(new Runnable() {
    @Override
    public void run() {
        int width = view.getWidth();
    }
});

利用 Handler 通信机制,添加一个 Runnable 到 message queue 中,当 view layout 处理完成时,自动发送消息,通知 UI 线程。借此机制,巧妙获取 View 的宽高属性。代码简洁,使用简单,相比 ViewTreeObserver 监听处理,还不需要手动移除观察者监听事件。

onLayout()


view = new View(this) {
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        int = view.getWidth(); 
    }
};

利用 View 绘制过程中的 onLayout() 方法获取宽高值,这也是为一个不需要借助其他类或者方法,仅靠自己就能完成获取宽高属性的手段。但是局限性在于,在 Activity 中使用代码创建 View 的场景较少,一般都是获取 layout 文件所加载 View 的宽高。

addOnLayoutChangeListener


view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        view.removeOnLayoutChangeListener(this);
        int width = view.getWidth();
    }
});

监听 View 的 onLayout() 绘制过程,一旦 layout 触发变化,立即回调 onLayoutChange 方法。注意,用完也要注意调用 remove 方法移除监听事件。

ViewCompat.isLaidOut(view)


if (ViewCompat.isLaidOut(view)) {
    int width = view.getWidth();
}

严格意义上来讲,这不能作为一个获取宽高的方式之一。充其量只能是一个判断条件。只有当 View 至少经历过一次 layout 时,isLaidOut() 方法才能返回 true,继而才能获取到 View 的真实宽高。所以,当我们的代码中有多次调用获取宽高时,才有可能使用这个方法判断处理。

getMeasuredWidth()


最后,借此地儿补充一点知识,getMeasuredWidth() 与 getWidth() 或者 getMeasuredHeight() 与 getHeight() 的区别。很多人容易对此产生混淆,不知道这两个方法到底有什么区别,使用时应该如何取舍。其实,官方文档介绍 View class 时,对于 Size 部分,有这么一段话:

The size of a view is expressed with a width and a height. A view actually possess two pairs of width and height values.

The first pair is known as measured width and measured height. These dimensions define how big a view wants to be within its parent (see Layout for more details.) The measured dimensions can be obtained by calling getMeasuredWidth() and getMeasuredHeight().

The second pair is simply known as width and height, or sometimes drawing width and drawing height. These dimensions define the actual size of the view on screen, at drawing time and after layout. These values may, but do not have to, be different from the measured width and height. The width and height can be obtained by calling getWidth() and getHeight().

这段话足以解释 getMeasuredXXX() 与 getXXX() 的区别和联系所在。说得直白一点,measuredWidth 与 width 分别对应于视图绘制 的 measure 与 layout 阶段。很重要的一点是,我们要明白,View 的宽高是由 View 本身和 parent 容器共同决定的,要知道有这个 MeasureSpec 类的存在。

比如,View 通过自身 measure() 方法向 parent 请求 100x100 的宽高,那么这个宽高就是 measuredWidth 和 measuredHeight 值。但是,在 parent 的 onLayout() 阶段,通过 childview.layout() 方法只分配给 childview 50x50 的宽高。那么,这个 50x50 宽高就是 childview 实际绘制并显示到屏幕的宽高,也就是 width 和 height 值。

如果你对自定义 View 过程很熟练的话,理解这部分内容就比较轻松一些。事实上,开发过程中,getWidth() 和 getHeight() 方法用的更多一些。

关于我:亦枫,博客地址:yifeng.studio/,新浪微博:IT亦枫

微信扫描二维码,欢迎关注我的个人公众号:安卓笔记侠

不仅分享我的原创技术文章,还有程序员的职场遐想