二十、 View 的工作原理(4)--- 获取 View 的测量宽/高

    通过上一篇对 View 的 measure 过程学习,我们知道,当 measure 过程完成之后我们就可以获取到 View 的测量宽/高。但是有一个问题就是 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,所以我们并不能保证在 Activity 的某个生命周期中去准确获取到 View 的测量宽/高,下面学习一下刚哥提供的四种方法:

    方法一: Activity/View#onWindowFocusChanged()

    当系统回调这个方法时,说明 View 已经初始化完毕了,所以此时我们获取宽/高是没问题。这个方法会在 Activity 的窗口得到焦点和失去焦点时调用,具体来说就是当 Activity 回调 onResume()--得到焦点 和 onPause()--失去焦点时,此方法都会被调用。所以典型代码如下:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);

    if (hasFocus) {
        // Activity 的窗口得到焦点时调用
        int width = mCustomView.getMeasuredWidth();
        int height = mCustomView.getMeasuredHeight();
        Log.d("cfmtest", "customView 的宽度: " + width);
        Log.d("cfmtest", "customView 的高度: " + height);
    }
}

eg:

CustomView.java:

package com.cfm.viewtest;

public class CustomView extends View {

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(100, 100);
        }else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.EXACTLY){
            setMeasuredDimension(100, heightSpecSize);
        }else if(widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, 100);
        }
    }
}

MainActivity.java:

package com.cfm.viewtest;

public class MainActivity extends AppCompatActivity {

    private CustomView mCustomView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCustomView = findViewById(R.id.custom_view);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        if (hasFocus) {
            int width = mCustomView.getMeasuredWidth();
            int height = mCustomView.getMeasuredHeight();
            Log.d("cfmtest", "customView 的宽度: " + width);
            Log.d("cfmtest", "customView 的高度: " + height);
        }
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    tools:context=".MainActivity">

    <com.cfm.viewtest.CustomView
        android:id="@+id/custom_view"
        android:background="@color/colorPrimary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

Log打印信息:

2019-05-30 22:31:24.088 9057-9057/com.cfm.viewtest D/cfmtest: customView 的宽度: 100
2019-05-30 22:31:24.088 9057-9057/com.cfm.viewtest D/cfmtest: customView 的高度: 100

    方法二: view.post(runnable)

    通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候,View 也已经初始化好了。典型代码如下:

@Override
protected void onStart() {
    super.onStart();

    mCustomView.post(new Runnable() {
        @Override
        public void run() {
            int width = mCustomView.getMeasuredWidth();
            int height = mCustomView.getMeasuredHeight();
        }
    });
}

eg:

MainActivity.java:

package com.cfm.viewtest;

public class MainActivity extends AppCompatActivity {

    private CustomView mCustomView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCustomView = findViewById(R.id.custom_view);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mCustomView.post(new Runnable() {

            @Override
            public void run() {
                int width = mCustomView.getMeasuredWidth();
                int height = mCustomView.getMeasuredHeight();
                Log.d("cfmtest", "customView 的宽度: " + width);
                Log.d("cfmtest", "customView 的高度: " + height);
            }
        });
    }
}

Log 打印信息:

2019-05-30 22:42:18.147 11187-11187/com.cfm.viewtest D/cfmtest: customView 的宽度: 100
2019-05-30 22:42:18.147 11187-11187/com.cfm.viewtest D/cfmtest: customView 的高度: 100

    方法三: ViewTreeObserver

    使用 ViewTreeObserver 的众多回调可以完成。比如使用 onGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout() 都会被回调。需要注意的是,随着 View 树的状态发生改变等,onGlobalLayout() 可能会被调用多次。典型代码如下:

@Override
protected void onStart() {
    super.onStart();

    ViewTreeObserver observer = mCustomView.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

        @Override
        public void onGlobalLayout() {
            // 防止多次调用
            mCustomView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width = mCustomView.getMeasuredWidth();
            int height = mCustomView.getMeasuredHeight();
        }
    });
}

eg:

MainActivity.java:

package com.cfm.viewtest;

public class MainActivity extends AppCompatActivity {
    private CustomView mCustomView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCustomView = findViewById(R.id.custom_view);
    }

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = mCustomView.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {
                mCustomView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mCustomView.getMeasuredWidth();
                int height = mCustomView.getMeasuredHeight();
                Log.d("cfmtest", "customView 的宽度: " + width);
                Log.d("cfmtest", "customView 的高度: " + height);
            }
        });
    }
}

Log 打印信息:

2019-05-30 22:50:25.848 12869-12869/com.cfm.viewtest D/cfmtest: customView 的宽度: 100
2019-05-30 22:50:25.848 12869-12869/com.cfm.viewtest D/cfmtest: customView 的高度: 100

    方法四: view.measure(int widthMeasureSpec, int heightMeasureSpec)

    通过手动对 View 进行 measure 来得到 View 的宽高。参考 View 的 MeasureSpec,分情况考虑:

    情况  1:

    当 View 的 LayoutParams 为 match_parent 时:

    通过上面的表,可以知道,此时 View 的 SpecMode 可能为 EXACTLY 或者 AT_MOST(具体由父容器的 MeasureSpec 可以确定),而 SpecSize 为 parentSize,也就是父容器剩余的空间大小。而这个时候,我们无法知道 parentSize 的大小,所以理论上不能测量出 View 的大小。

    情况 2:

    当 View 的 LayoutParams 为具体数值 (dp/pc) 时:

    根据上面的表我们可以知道,View 的 SpecMode 为 EXACTLY 模式,SpecSize 为 View 的 LayoutParams 具体值。所以典型代码如下:

// eg: View 的 LayoutParams 中宽/高都为 100px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

    情况 3:

    当 View 的 LayoutParams 为 wrap_content 时:

    通过上表可以看到,此时 SpecMode 为 AT_MOST,而 SpecSize 最大值为 parentSize。那我们就用 SpecSize 的最大值来创建 MeasureSpec,典型代码如下:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

eg:

MainActivity.java:

package com.cfm.viewtest;

public class MainActivity extends AppCompatActivity {
    private CustomView mCustomView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCustomView = findViewById(R.id.custom_view);
    }

    @Override
    protected void onStart() {
        super.onStart();
        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY);
        int  heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
        mCustomView.measure(widthMeasureSpec, heightMeasureSpec);
        Log.d("cfmtest", "customView 的宽度: " + mCustomView.getMeasuredWidth());
        Log.d("cfmtest", "customView 的高度: " + mCustomView.getMeasuredHeight());
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    tools:context=".MainActivity">

    <com.cfm.viewtest.CustomView
        android:id="@+id/custom_view"
        android:background="@color/colorPrimary"
        android:layout_width="200dp"
        android:layout_height="wrap_content"/>

</LinearLayout>

Log 打印信息:

2019-05-31 19:29:58.122 9170-9170/com.cfm.viewtest D/cfmtest: customView 的宽度: 200
2019-05-31 19:29:58.122 9170-9170/com.cfm.viewtest D/cfmtest: customView 的高度: 100

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值