View的高宽是由View本身和Parent容器共同决定的。
getMeasuredWidth() 和 getWidth() 分别对应于视图绘制的 measure 和 layout 阶段。getMeasuredWidth()获取的是View原始的大小,也就是这个View在XML文件中配置或者是代码中设置的大小。getWidth()获取的是这个View最终显示的大小,这个大小有可能等于原始的大小,也有可能不相等。比如说,在父布局的onLayout()方法或者该View的onDraw()方法里调用measure(0, 0),二者的结果可能会不同(measure中的参数可以自己定义)。
getWidth()
/** * Return the width of the your view. * @return The width of your view, in pixels. */ @ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; }
从源码上看, getWidth() 是根据 mRight 和 mLeft 之间的差值计算出来的,需要在布局之后才能确定它们的坐标,也就是说布局后在onLayout()方法里才能调用getWidth()来获取。因此,getWidth()获取的宽度是在View设定好布局后整个View的宽度。
getMeasuredWidth()
/** * Like {@link #getMeasuredWidthAndState()}, but only returns the * raw width component (that is the result is masked by * {@link #MEASURED_SIZE_MASK}). * * @return The raw measured width of this view. */ public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; }
从源码上看, getMeasuredWidth() 获取的是 mMeasuredWidth 的这个值。这个值是一个8位的十六进制的数字,高两位表示的是这个measure阶段的Mode的值,具体可以查看MeasureSpec的原理。这里 mMeasuredWidth & MEASURED_SIZE_MASK 表示的是测量阶段结束之后,View真实的值。而且这个值会在调用了 setMeasuredDimensionRaw() 函数之后会被设置。所以 getMeasuredWidth() 的值是measure阶段结束之后得到的View的原始的值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); } private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
总结一下, getMeasuredWidth 是measure阶段获得的View的原始宽度, getWidth 是layout阶段完成后,其在父容器中所占的最终宽度。
什么时候不同呢?
首先自定义一个父布局: CustomView
package com.yongdaimi.android.androidapitest.view; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.LinearLayout; import androidx.annotation.Nullable; public class CustomView extends LinearLayout { public static final String TAG = "xp"; private View mFirstView; private int mFirstViewWidth; private int mFirstViewHeight; private int mFirstViewMeasureWidth; private int mFirstViewMeasureHeight; public CustomView(Context context) { this(context, null); } public CustomView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { setOrientation(LinearLayout.HORIZONTAL); } @Override protected void onFinishInflate() { super.onFinishInflate(); mFirstView = getChildAt(0); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mFirstViewWidth = mFirstView.getWidth(); mFirstViewHeight = mFirstView.getHeight(); mFirstViewMeasureWidth = mFirstView.getMeasuredWidth(); mFirstViewMeasureHeight = mFirstView.getMeasuredHeight(); Log.i(TAG, "onSizeChanged: mFirstViewWidth: " + mFirstViewWidth + ", mFirstViewHeight: " + mFirstViewHeight + ", mFirstViewMeasureWidth: " + mFirstViewMeasureWidth + ", mFirstViewMeasureHeight: " + mFirstViewMeasureHeight); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mFirstView.layout(-100, 0, -50, 50); mFirstViewWidth = mFirstView.getWidth(); mFirstViewHeight = mFirstView.getHeight(); mFirstViewMeasureWidth = mFirstView.getMeasuredWidth(); mFirstViewMeasureHeight = mFirstView.getMeasuredHeight(); Log.i(TAG, "onLayout: mFirstViewWidth: " + mFirstViewWidth + ", mFirstViewHeight: " + mFirstViewHeight + ", mFirstViewMeasureWidth: " + mFirstViewMeasureWidth + ", mFirstViewMeasureHeight: " + mFirstViewMeasureHeight); } }
然后在activity的布局文件使用这个布局并添加一个新的控件:
<?xml version="1.0" encoding="utf-8"?> <com.yongdaimi.android.androidapitest.view.CustomView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_first" android:layout_width="60dip" android:layout_height="60dip" android:background="@android:color/holo_green_light" /> </com.yongdaimi.android.androidapitest.view.CustomView>
运行:
2020-09-04 10:56:51.202 30999-30999/com.yongdaimi.android.androidapitest I/xp: onSizeChanged: mFirstViewWidth: 0, mFirstViewHeight: 0, mFirstViewMeasureWidth: 180, mFirstViewMeasureHeight: 180
2020-09-04 10:56:51.204 30999-30999/com.yongdaimi.android.androidapitest I/xp: onLayout: mFirstViewWidth: 50, mFirstViewHeight: 50, mFirstViewMeasureWidth: 180, mFirstViewMeasureHeight: 180
可以看到,只要在代码里重新修改了子控件的摆放位置,getWidth和getMeasureWidth的值就会不同。
如何在onCreate中拿到View的宽度和高度?
在onCreate()中获取View的高宽有三种方法:
1. View.post(runnable)
view.post(new Runnable() { @Override public void run() { int width = view.getWidth(); int measuredWidth = view.getMeasuredWidth(); Log.i(TAG, " " + width); Log.i(TAG, "measuredWidth: " + measuredWidth); } });
利用Handler通信机制,发送一个Runnable到MessageQueue中,当View布局处理完成时,自动发送消息,通知UI进程。借此机制,巧妙获取View的高宽属性,代码简洁,相比ViewTreeObserver监听处理,还不需要手动移除观察者监听事件。
2. ViewTreeObserver.addOnGlobalLayoutListener()
监听View的 onLayout() 绘制过程,一旦layout触发变化,立即回调 onLayoutChange 方法。
注意,使用完也要主要调用 removeOnGlobalListener() 方法移除监听事件。避免后续每一次发生全局View变化均触发该事件,影响性能。
ViewTreeObserver vto = view.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeGlobalOnLayoutListener(this); Log.i(TAG, " " + view.getWidth()); Log.i(TAG, "height: " + view.getHeight()); } });
3. View.measure(int widthMeasureSpec, int heightMeasureSpec)
除了在onCreate()中获得View的高宽,还可以在Activity的onWindowFocusChanged() 方法中获得高宽。