通过上一篇对 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:
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:
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