View 系列 —— 解锁正确获取 View 的宽高的姿势

987 阅读3分钟

前言

获取 View 的宽高是我们开发中很常见的一种操作,View 本身也提供了获取宽高的方法,比如说 getWidthgetHeightgetMeasuredWidthgetMeasuredHeight。但是在使用中我们会发现,在 onCreate 或者 onResume 中获取到的宽高竟然是 0。那么这两种方式获取的宽高有什么不同呢?为什么在使用的时候会获取到 0 呢?本篇文章对这个常见的问题做一个记录。

1. getWidth() 和 getMeasuredWidth 的区别

首先这两个方法都是用来获取 View 的宽高的,区别在于 getWidth/getHeight 返回的是最终的宽高,这个值是在 View 布局过程 layout 中计算出来的,因此只有在 layout 之后才能被正确的获取。

getMeasuredWidth/getMeasuredHeight 返回的是测量过程的宽高,也就是在 measure 过程被计算出来的。

因此,如果需要获取 View 的实际宽度,应该使用 getWidth() 方法;如果需要获取 View 的测量宽度,应该使用 getMeasuredWidth() 方法。

2. 为什么 getWidth() 和 getMeasuredWidth 会获取到 0?

我们可以自己尝试下,在 Activity 的 onCreate 或者 onResume 中获取 View 的宽高,发现值是 0。通过在第一小节的讲解,相信你已经发现了,getWidth/getHeight 的值只能在 layout 之后才能被获取到,getMeasuredWidth/getMeasuredHeight 只能在 measure 后被获取到,那 onCreate 或者 onResume 获取不到,很显然就是因为此时 View 还没有测量或布局完成。

事实也确实如此,View 的 measure 过程和 Activity 的生命周期不是同步执行的,因此无法保证在某个生命周期中 View 已经完成了测量。

那么有什么方法可以正确的获取到 View 的宽高吗?下面来介绍几种。

3. 正确获取 View 的宽高

3.1 view.post(runnable)

view.post()是 View 中的一个方法,用于在 UI 线程中异步地执行一个 Runnable 对象。当调用 view.post() 方法时,系统会将 Runnable 对象加入到 View 的消息队列中,等待 UI 线程空闲时执行。此时 View 已经初始化好了,可以获取到正确的宽高。

override fun onResume() {
    super.onResume()
    val circle = findViewById<CircleView>(R.id.circle_view)
    circle.post {
        val width = circle.width
        val height = circle.height
    }
}

3.2 onWindowFocusChanged()

当 View 所在的 Window 获得或失去焦点时,系统会调用 View 的 onWindowFocusChanged() 方法。在这个方法中,可以获取 View 的宽高。因为根据 View 的生命周期,onWindowFocusChanged 是在 View 的三大工作流程之后才执行的,这时候就可以去获取到 View 的宽高了。

override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    val circle = findViewById<CircleView>(R.id.circle_view)
    circle.post {
        val width = circle.width
        val height = circle.height
    }
}

3.3 ViewTreeObserver

ViewTreeObserver 是一个观察者模式的类,用于监听 View 树的变化,通过 ViewTreeObserver 可以获取 View 的宽高。

ViewTreeObserver 中,可以通过 addOnGlobalLayoutListener() 方法添加一个监听器,用于监听 View 树的全局事件。当 View 树的布局发生变化时,系统会调用监听器的onGlobalLayout() 方法。需要注意的是,View 树有改变,onGlobalLayout 就会被调用,为了防止多次调用该方法,需要及时移除监听。

override fun onResume() {
    super.onResume()

    val circle = findViewById<CircleView>(R.id.circle_view)
    circle.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            circle.viewTreeObserver.removeGlobalOnLayoutListener(this)
            val width = circle.width
            val height = circle.height
        }

    })
}

上面这几种方式就是我平时比较常用的获取 View 的宽高的方法,其实还可以通过手动调用 measure 方法来获取宽高,但是个人不喜欢用,因为 match_parent 的使用不了这种方法,而且有时候测量的也不准确。所以个人觉得上面的几种方法就足够用啦,欢迎交流!