控件的测量和绘制
控件架构
控件分为两大类,即ViewGroup和View
ViewGroup又可包含View,形成控件树,常用的fingViewById()方法依据深度优先遍历查找控件
上层控件负责下层子控件的测量和绘制,并传递交互事件
setContentView()
Android界面框架图如下
- 每个Activity都包含一个PhoneWindow对象
- PhoneWindow将一个DecorView设置为根View,其分为TitleView和ContentView(Framelayout)
- setContentView()将activity_main.xml设置到ContentView,ActivityManagerService回调onResume()将DecorView添加到PhoneWindow显示出来
- DecorView中的View监听事件,通过WindowManagerService接收并回调到Activity的onClickListener
将上图转为视图树如下
- DecoreView以LinearLayout上下布局
- 上面为ActionBar显示标题,下面为Content显示内容
- 若设置requestWindowFeature(Window.FEATURE_NO_TITLE)全屏显示,则只存在Content,故其需要在setContView()之前调用才能生效
MeasureSpec
MeasureSpec是一个32位int值,高2位为测量模式,低30位为测量大小,模式分为
- EXACTLY:layout_width、layout_height 为具体数值或match_parent
- AT_MOST:layout_width、layout_height 为 wrap_content,在父控件宽高范围内,控件大小随子控件或内容变化而变化
- UNSPECIFIED,不指定其大小和测量模式,内部使用无需关注
measure
调用过程为:
- Activity创建时通过ViewRootImpl的setView()设置DecorView,调用performTraversals()、performMeasure()
- 调用DecorView的measure(),会调用到父类View的measure()
- 随后调用View的onMeasure(),这里会根据多态,会调用到DecorView的onMeasure()
- 再调用父类FrameLayout的onMeasure(),其通过getChildMeasureSpec()获取每一个child的MeasureSpec,并调child的measure()、onMeasure()
- 若child是ViewGroup则重复上述过程,否则调用child的setMeasuredDimension()设置mMeasuredWidth、mMeasuredHeight
View的measure
通过getChildMeasureSpec()获取child的MeasureSpec,其与下面属性有关
- 父容器的MeasureSpec
- child的LayoutParams
- child的Margin和Padding
可以总结为下图
由View的getDefaultSize()方法和上表可知,当View为wrap_content时,默认大小就为父容器中剩余空间的大小,如下图
继承View的自定义控件,需要重写onMeasure()并设置wrap_content下的大小,否则其相当于match_parent
public class MyView extends View {
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidthOrHeight(widthMeasureSpec), measureWidthOrHeight(heightMeasureSpec));
}
private int measureWidthOrHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
}
如上自定义View,其测量过程如下
- 从MeasureSpec获取具体的模式specMode和大小specSize
- 当若specMode为EXACTLY,使用指定的大小specSize
- 当specMode为AT_MOST取(specSize或默认值)中的最小值
如下设置为wrap_content时,默认为200,而不是填充整个父布局
ViewGroup的measure
ViewGroup继承自View,但其是抽象类,并没有实现onMeasure()方法,需要其子类去具体实现,接下来以LinearLayout的onMeasure()为例
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
纵向布局的测量过程如下
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
.....
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
......
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
......
//下面测量child,判断是否使用weight分割
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
//1.当前LinearLayout高度为EXACTLY ,先加上child的margin,但不测量child,稍后会将多余空间分配给child
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
//2. 当前LinearLayout高度为UNSPECIFIED或AT_MOST,child.height设置为WRAP_CONTENT,先测量出原始高度,否则测量高度会为0
lp.height = LayoutParams.WRAP_CONTENT;
}
//下面调用measureChildWithMargins()测量child
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
//恢复child.height,记录weight消耗的高度
lp.height = 0;
consumedExcessSpace += childHeight;
}
//加上child的margin
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
......
}
......
}
......
//加上自己的Padding,将计算出的宽高和LinearLayout的MeasureSpec传到resolveSizeAndState()
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
......
maxWidth += mPaddingLeft + mPaddingRight;
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
......
}
resolveSizeAndState()代码如下
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL; //若计算出来的宽高超过LinearLayout,仍使用LinearLayout宽高
} else {
result = size; //若计算出来的宽高不超过LinearLayout,则用计算出来的宽高
}
break;
case MeasureSpec.EXACTLY:
result = specSize; //宽高固定为LinearLayout宽高
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
获取宽高
宽高需要在measure结束后,通过getMeasuredWidth() / getMeasuredHeight()获取,但在某些情况(如多次measure)无法获取正确的宽高
onWindowFocusChanged
Activity / View的回调方法,当其被调用时,View已经初始化完毕,但需要注意,当窗口每次失去/获取焦点时,这个方法都会被调用
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
}
}
View.post
当Looper调用此runnable时,View已经初始化好了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
}
});
}
ViewTreeObserver
当View树状态改变或内部View可见性改变时,onGlobalLayout()会被回调,但此方法可能也会被调用多次
@Override
protected void onStart() {
super.onStart();
View view = new View(this);
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
}
});
}
View.measure
可手动对View进行measure以获取宽高,但要根据LayoutParams分具体情况
当为match_parent时,无法获取,因为measure时需要知道父容器的剩余空间
当为具体数值时,可以获取,如100px时
int widthMeasureSpc = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int HeightMeasureSpc = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpc, HeightMeasureSpc);
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
当为wrap_content时,可以获取,利用最大的SepcSize去构造MeasureSpec
int widthMeasureSpc = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
int HeightMeasureSpc = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpc, HeightMeasureSpc);
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
layout
调用过程为:
- 调用ViewRootImpl的performTraversals()、performLayout()
- 调用DecorView的layout(),故会调用到父类ViewGroup和View的layout()
- 随后调用View的onLayout(),这里会根据多态,会调用到DecorView的onLayout()
- 再调用父类FrameLayout的onLayout()、layoutChildren()处理上下左右边距
- 调用child的layout()
- 若child是ViewGroup则重复上述过程,否则调用自身的onLayout()完成布局
View的layout
layout()通过setFrame()设置mLeft、mRight、mTop、mBottom确认自身在父容器的位置,随后调用onLayout(),View的onLayout是个protected空方法,需要子类具体实现
public void layout(int l, int t, int r, int b) {
......
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
.....
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
......
}
ViewGroup的layout
ViewGroup继承自View,但其是抽象类,并没有实现onLayout()方法,需要其子类去具体实现,接下来以LinearLayout的onLayout()为例
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
layoutVertical代码如下,通过不断增加childTop让child下移,在setChildFrame()调用child的layout()方法
void layoutVertical(int left, int top, int right, int bottom) {
......
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
......
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
getMeasuredWidth()和getWidth()区别
从源码来看,View的默认实现中,测量宽高和最终宽高是相等的,只不过测量宽高在measure中赋值,最终宽高在layout中赋值
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
但在某些情况可能会导致两者不同,如下重写了layout()方法会导致最终宽高+100,还有就是当View需要多次measure时,前几次measure和最终的宽高可能不一致
@Override
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b + 100);
}
draw
调用流程为
- 调用ViewRootImpl的performTraversals()、perforemDraw()
- 调用ViewRootImpl的draw()、drawSoftware()
- 调用DecorView的draw(),调用父类View的draw()、onDraw()、dispatchDraw()
- 根据多态调用ViewGroup的dispatchDraw()、drawChild()
- 再调用child的draw(),若是Viewgroup则重复上面过程,否则调用自身的onDraw()完成绘制
View的draw
View的绘制由onDraw()方法完成,并在其参数canvas上调用drawXXX()方法绘制图像
canvas通过bitmap用于存储所有绘制信息,可让onDraw()中的canvas绑定bitmap并暴露出去,实现通过修改共享bitmap的方法更新UI
public class MyView extends View {
public static Bitmap bitmap;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); //这里未转换单位
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidthOrHeight(widthMeasureSpec), measureWidthOrHeight(heightMeasureSpec));
}
private int measureWidthOrHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmap, 0, 0, null);
super.onDraw(canvas);
}
}
如上,修改MyView,构造函数中创建bitmap,onDraw方法中绑定bitmap
<?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:orientation="horizontal"
tools:context=".MainActivity">
<Button
android:id="@+id/test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="替换背景" />
<com.demo.demo0.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0000" />
</LinearLayout>
如上,添加按钮和自定义View
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button test = findViewById(R.id.test);
test.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Canvas canvas = new Canvas(MyView.bitmap);
canvas.drawColor(Color.GREEN);
}
});
}
}
如上,在点击事件中,根据MyView.bitmap初始化一个Canvas,执行drawColor()方法,通过改变bitmap让View重绘实现更新颜色
ViewGroup的绘制
ViewGroup通常情况下不需要绘制,只有在指定背景色时其onDraw()方法才会调用,使用dispatchDraw()方法遍历并绘制child
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
ViewGroup会默认开启WILL_NOT_DRAW 标志位进行优化(View不默认开启),当ViewGroup需要onDraw()时需要手动关闭此标志位
实例
自定义View需要注意如下事项
- 让View支持wrap_content,否则其相当于match_parent
- 让View支持padding,如果不在ondraw()处理padding会导致其无效
- 让ViewGroup支持padding和child的margin,同上
- 不要在View中使用Handle,内部有post()等方法
- View中的线程或动画需要及时停止,可利用onDetachedFromWindow()方法
- 滑动嵌套需要处理滑动冲突