View的三个流程:measure、layout、draw
View常见的回调方法:构造方法、onAttach、onVisibilityChanged、onDetach等
对于DecorView来说:
对于普通的View,其MeasureSpec由父容器MeasureSpec和自身的LayoutParams来共同决定。
getChildMeasureSpec
getChildMeasureSpecView 的工作流程
View的工作流程主要是指measure、layout、draw三大流程,即测量、布局和绘制。
measure:确定View的测量宽/高。
layout:确定View的最终宽/高 和 四个顶点的位置。
draw:将View绘制到屏幕上
measure过程
分情况:
- View
- ViewGroup,除了自己测量外,还需要遍历子元素的measure方法
View 的 measure 过程
View 的 measure 过程由 measure 方法来完成。
measure 方法是一个 final 方法。意味着子类不能重新写此方法。
在 View 的 measure 方法中会调用 View 的 onMeasure 方法,如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上述代码简洁,当并不代表简单。
ViewGroup 的 measure 过程
对于ViewGroup来说,除了完成自己 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个过程。
和 View 不同的是,ViewGroup 是一个抽象类,因此它没有重写View的onMeasure方法,但是它会提供一个叫measureChildren的方法,如下:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
从上述代码来看,ViewGroup 在 measure 时,会对每一个子元素进行measure,measureChild方法如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
很显然,measureChild 的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建MeasureSpec,然后将Measure直接传递给View的measure方法来进行测量。getChildMeasureSpec的工作过程已经在上面进行了详细的分析,通过上表可以清楚的了解它的逻辑。
ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure 方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等。不同的ViewGroup由不同的布局特性,这导致了它们的测量细节各不相同,比如LinearLayout和RelativeLayout布局特性显然不同,所以ViewGroup无法做统一实现。
分析LinearLayout的onMeasure方法
获取View宽/高的方式
View 的 measure 过程是三大流程中最负责的一个,measure完成后可以通过 getMeasureWidth/Height方法就可以正确地获取到View的测量宽/高。
注意,某些情况,系统需要多次measure才能确定最终的测量宽高,在这种情形下,在onMeasure中拿到测量宽高很可能是不准确的。一个好的习惯是在onLayout中获取View的测量宽高或最终宽高。
- Activity/View#onWindowFocusChanged
View 初始化完毕,宽/高已经准备好。
注意:会被多次调用,当Activity的窗口得到焦点和失去焦点时均会被调用一次。(onResume和onPause频繁会被调用)
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
}
}
- view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
@Override
protected void onStart() {
super.onStart();
textView.post(new Runnable() {
@Override
public void run() {
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
}
});
}
- ViewTreeObserver
使用ViewTreeObserver的中国回调可以完成这个功能,比如使用onGlobalLayoutListener接口,当View树的状态发生改变或者View树内部的View可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。
ViewTreeObserver observer = textView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
textView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
}
});
- view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对View进行measure来得到View的宽高。
这种方法比较复杂,需要分情况处理,根据LayoutParams来分:match_parent,dp/px,wrap_content。
match_parent
直接放弃,无法measure出具体的宽高。
原因很简单,根据View的measure过程,构造此MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。
dp/px
比如宽和高都是100px
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
textView.measure(widthMeasureSpec, heightMeasureSpec);
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
wrap_content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30 )- 1, View.MeasureSpec.AT_MOST);
textView.measure(widthMeasureSpec, heightMeasureSpec);
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
注意到 (1 << 30) -1
,通过分析MeasureSpec的实现可以知道,View的尺寸使用30位二进制表示,也就是说最大是30个1(即 2^30 - 1),也就是(1 << 30) -1 ,在最大化模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的。
两个错误的用法:
- 违背了系统的内部实现规范,无法通过错误的MeasureSpec去得出合法的SpecMode,从而导致measure过程出错。
- 不能保证一定能measure出正确的结果,可能凑巧正确
错误用法一
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
错误用法二
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTNET);
layout 过程
作用:ViewGroup 用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,layout方法中调用onLayout方法,layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,先看View的layout方法:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
layout 方法的大致流程:通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop、mBottom四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有段,所以View和ViewGroup均没有真正实现onLayout方法。
测量的宽高和最终的宽高可能不一致。
draw过程
作用:将View绘制到屏幕上面。
步骤:
- 绘制背景 background.draw(canvas)
- 绘制自己 onDraw
- 绘制children dispatchDraw
- 绘制装饰 onDrawScrollBars
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
}
View 绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有的子元素的draw方法。
View有一个特殊的方法 setWillNotDraw,先看一下源码:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从setWillNotDraw方法的注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启动这个优化标记位,但是ViewGroup会默认启用这个标记位。
这个标记位对实际开发的意义:当我们的自定义控件继承于ViewGroup并且自身不具备绘制功能时,就可以开启这个标记位,从而让系统进行后续的优化。如果明确ViewGroup自身需要绘制内容时,我们需要显示关闭WILL_NOT_DRAW这个标记位。
自定义View
自定义View的分类
分类标准不唯一
- 继承 View 重写 onDraw 方法
实现一些不规则的效果,不方便通过布局的组合来达到,需要重写onDraw方法。
这种方式需要自己支持 wrap_content,并且padding也需要自己处理。 - 继承 ViewGroup 派生特殊的Layout
重新定义一种新布局,类似LinearLayout或RelativeLayout。
当某种效果看起来像几种View组合在一起时,可以采用这种方式实现。
采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。 - 继承特定的View(比如TextView)
用于扩展已有View的功能,比如TextView
这种实现比较容易,不需要自己支持wrap_content和padding等 - 继承特定的ViewGroup(比如LinearLayout)
当某种效果看起来很像几种View组合在一起的时候,可以采用这种方式实现。
这种方式不需要自己处理ViewGroup的测量和布局过程。
需要注意和方式2的区别,方法2更接近View的底层
margin 属性是父容器控制的,padding需要自己处理
自定义View须知
一些自定义View过程中的注意事项,如果这些问题处理不好,有些会影响View的正常使用,而有些则会导致内存泄露等。
- 让View支持wrap_content
在onMeasure中对wrap_content做特殊处理,否则当外界在布局中使用wrap_content时就无法达到预期的效果,和match_parent效果类似 - 让View支持padding
在draw中对padding做处理,否则padding属性无法起作用。另直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,都这将导致padding和子元素的margin失效。 - 尽量不在View中使用Handler,没必要
View内部本身提供了post系列的方法,完全可以替代Handler的作用,当然除非很明确要使用Handler来发送消息。 - View中有线程或动画,需要及时停止 onDetachedFromWindow:当包含此View的Activity退出或者当前View被remove时,该方法会被调用。
和此方法对应的时onAttachedToWindow。
当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏 - View带有滑动嵌套的情形,需要处理好滑动冲突
另附示例:
- 自定义View
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int defaultWidth = 200;
private int defaultHeight = 200;
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_color, Color.RED);
a.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
if (wMode == MeasureSpec.AT_MOST && hMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, defaultHeight);
} else if (wMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, hSize);
} else if (hMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSize, defaultHeight);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();
int radius = Math.min(width, height) / 2;
canvas.drawCircle(getPaddingLeft() + width / 2, getPaddingRight() + height / 2, radius, mPaint);
}
}
- 自定义ViewGroup
假定了每个子元素的宽高相同(以第一个为准)
示例不规范的地方:
- 没有子元素的时候不应该直接把宽高设置为0,而应该根据LayoutParams中的宽高来做相应的处理
- 在测量HorizontalScrollViewEx宽高时,没有考虑到它的padding以及子元素的margin,因为它自身的padding以及子元素的margin会影响到HorizontalScrollViewEx的宽高。
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
private int mLastX = 0;
private int mLastY = 0;
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastXIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
Log.d(TAG, "intercepted = " + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (wMode == MeasureSpec.AT_MOST && hMode == MeasureSpec.AT_MOST) {
View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (hMode == MeasureSpec.AT_MOST) {
View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(wSize, measuredHeight);
} else if (wMode == MeasureSpec.AT_MOST) {
View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, hSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
参考资料
感谢以下文章作者
《Android开发艺术探索》第4章 View的工作原理
网友评论