7.3 Activity Décorview布局(layout)
Measure确定Décor View以及child views的大小,layout则是确定child view在其parent view中的显示区域,只有layout结束,view的left,right,top,bottom值才会被设置,getWidth和getHeight两个函数才会返回view最终的宽高值
对FrameLayout来说,由于child view的大小确定了,那么再确定它们在parentview中的显示区域其实通过parent view的padding值,child view的margin值以及宽高就可以计算出
当然RelativeLayout和LinearLayout会相对复杂点,因为childviews之间会存在布局关联,本文只对FrameLayout的实现做简单介绍,至于RelativeLayout和LinearLayout大家可自行看代码分析
在ViewRootImpl中调用performMeasure完成对Décor View的measure后,接着调用
PerformLayout触发布局操作,其内部主要调用:
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); |
Host变量保存的就是Décor View,layout的四个变量依次对应left,top,right,bottom
FrameLayout和ViewGroup都没有对layout做处理,接着直接看View中的默认实现:
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); 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; } |
这个函数先调用setFrame保存位置数据,然后调用onLayout进行布局操作,最后看这个view是否有设置layoutchangelistener,如果有,调用回调通知布局已经发生改变
先看setFrame:
//View.java protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position invalidate(sizeChanged);
mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS; if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); }
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) { mPrivateFlags |= PFLAG_DRAWN; invalidate(sizeChanged); invalidateParentCaches(); } // Reset drawn bit to original value (invalidate turns it off) mPrivateFlags |= drawn; mBackgroundSizeChanged = true; notifySubtreeAccessibilityStateChangedIfNeeded(); } return changed; } |
如果left,right,top,bottom值都没变,那就说明布局没变,返回changed为false,如果变了,则将对应的值都保存到mLeft,mRight,mTop, mBottom,然后调用invalidate通知view进行重绘
接着看onLayout的实现:
//FrameLayout.java protected void onLayout(boolean changed, int left, int top, int right, int bottom) { layoutChildren(left, top, right, bottom, false /* no force left gravity */); } |
直接调用layoutChildren:
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) { final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground(); final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground(); final int parentBottom = bottom - top - getPaddingBottomWithForeground();
mForegroundBoundsChanged = true;
for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight();
int childLeft; int childTop;
int gravity = lp.gravity; if (gravity == -1) { gravity = DEFAULT_CHILD_GRAVITY; }
final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: if (!forceLeftGravity) { childLeft = parentRight - width - lp.rightMargin; break; } case Gravity.LEFT: default: childLeft = parentLeft + lp.leftMargin; }
switch (verticalGravity) { case Gravity.TOP: childTop = parentTop + lp.topMargin; break; case Gravity.CENTER_VERTICAL: childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin; break; case Gravity.BOTTOM: childTop = parentBottom - height - lp.bottomMargin; break; default: childTop = parentTop + lp.topMargin; }
child.layout(childLeft, childTop, childLeft + width, childTop + height); } } } |
逻辑很简单,通过拿到child view的measure width和measure height,然后根据gravity和margin值计算出child view在parent view中的left,right,top,bottom值,最后调用
child.layout
如果child view是ViewGroup,则重走本节流程,依次反复执行,直到全部child view都layout完成为止
7.4 Activity Décorview绘制(draw)
Layout结束后,每一个child view在其parent view的位置都已经固定,接下去就可以开始绘制child view的图形数据了
在ViewRootImpl中调用performLayout完成布局后,接着调用performDraw,其最终会调用drawSoftware,该函数会通过mSurface拿到Canvas,接着调用mView.draw()并传入canvas开始Décor view的绘制
由于Decor view的所有child views是共用Decor view这一块top canvas的,对每一个view来说,它们在绘制其child view或者content data时,最简单的当然是使用相对坐标,也就是将其左上角设置为坐标原点,这样就可以让view只需专注于child view或者content data的绘制,而无需关心坐标原点的调整以及恢复
如果把Décor view作为世界地图,那top canvas肯定拥有初始的单位矩阵, childview或者content data就是世界中的显示元素,元素显示位置和方式的调整,都是通过矩阵(matrix)的配置来实现的
后续View绘制代码的分析,为了简化逻辑以便于理解,我们假定系统不支持硬件加速,也就是mAttachInfo.mHardwareAccelerated为false
Draw(Canvas canvas)是View绘制的入口函数,它跟measure和layout一样,基于view tree做完整的递归调用,它会按顺序做如下事情
1) 绘制view的background
2) 调用onDraw绘制view的content
3) 调用dispatchDraw绘制view的children
4) 绘制scrollbar等
其中1, 2, 4都是基于传入的canvas做界面绘制,那canvas的坐标切换肯定是由第3步
dispatchDraw来完成了
dispatchDraw的默认实现是空的,也就是啥也没做,只有ViewGroup重新实现了改方法,也就是说,这个方法,只会ViewGroup有效
接着我们看其在ViewGroup中的实现:
//ViewGroup.java protected void dispatchDraw(Canvas canvas) { boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); final int childrenCount = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; …… int clipSaveCount = 0; final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; if (clipToPadding) { clipSaveCount = canvas.save(); canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, mScrollX + mRight - mLeft - mPaddingRight, mScrollY + mBottom - mTop - mPaddingBottom); }
// We will draw our child's animation, let's reset the flag mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
……
for (int i = 0; i < childrenCount; i++) { int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } …… } |
由于mGroupFlags在initViewGroup时被设置了FLAG_CLIP_TO_PADDING,所以这里
clipToPadding肯定为true,接着调用canvas .clipRect根据padding和scroll值来对canvas的操作区域进行裁剪,接着遍历所有childview,依次调用drawChild
接着看drawChild的实现:
//ViewGroup.java protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); } |
直接调用child.draw函数,这个函数也在View.java默认被实现,跟上头提过的draw(canvas)函数不同的是,它有三个参数,除了canvas外,还传入了parent viewgroup和drawing time:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { boolean usingRenderNodeProperties = mAttachInfo != null && mAttachInfo.mHardwareAccelerated; boolean more = false; final boolean childHasIdentityMatrix = hasIdentityMatrix(); final int flags = parent.mGroupFlags; …… final boolean hasNoCache = cache == null || hasDisplayList; final boolean offsetForScroll = cache == null && !hasDisplayList && layerType != LAYER_TYPE_HARDWARE;
int restoreTo = -1; if (!usingRenderNodeProperties || transformToApply != null) { restoreTo = canvas.save(); } if (offsetForScroll) { canvas.translate(mLeft - sx, mTop - sy); } else { if (!usingRenderNodeProperties) { canvas.translate(mLeft, mTop); } if (scalingRequired) { if (usingRenderNodeProperties) { // TODO: Might not need this if we put everything inside the DL restoreTo = canvas.save(); } // mAttachInfo cannot be null, otherwise scalingRequired == false final float scale = 1.0f / mAttachInfo.mApplicationScale; canvas.scale(scale, scale); } } …… if (!usingRenderNodeProperties) { // apply clips directly, since RenderNode won't do it for this draw if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN && cache == null) { if (offsetForScroll) { canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop)); } else { if (!scalingRequired || cache == null) { canvas.clipRect(0, 0, mRight - mLeft, mBottom - mTop); } else { canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight()); } } }
if (mClipBounds != null) { // clip bounds ignore scroll canvas.clipRect(mClipBounds); } } …… if (!layerRendered) { if (!hasDisplayList) { // Fast path for layouts with no backgrounds if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); } else { draw(canvas); } } else { mPrivateFlags &= ~PFLAG_DIRTY_MASK; ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags); } } } else if (cache != null) { …… }
if (restoreTo >= 0) { canvas.restoreToCount(restoreTo); } …… mRecreateDisplayList = false; return more; } |
drawingTime和parent viewgroup的作用在下一节再介绍
由于在layout的时候,view在其parent view中的top-left坐标已经确定,所以这里的坐标切换相对来说就非常简单,首先调用canvas.save()备份canvas当前的matrix/clip数据,接着调用canvas.translate(mLeft,mTop)进行原点切换,最后调用draw(canvas)让view在当前canvas上进行内容绘制,绘制成功后,调用canvas.restoreToCount(restoreTo)恢复之前canvas备份的matrix/clip数据
7.5 ViewAnimation原理介绍
通过上一节我们知道,View在parent view中的位置或者展示方式是通过matrix来完成的,
那如果我在view每一次绘制的时候,按照一定的规律更改matrix的值,这样就可以达到view在parent view的一个序列图形显示效果,也就是说一个针对view的动画效果就出来了,这个好像是废话,动态视图不都是这么来的么^_^
Android提供了TranslateAnimation等类用来封装对每一帧对应matrix的计算
接下去通过代码做下简单介绍,为view设置并开始播放animation:
TranslateAnimation tAnim = new TranslateAnimation(0, 400, 0, 0); tAnim.setDuration(2000);
view.startAnimation(tAnim); |
创建TranslateAnimation,然后调用startAnimation开发播放
//View.java public void startAnimation(Animation animation) { animation.setStartTime(Animation.START_ON_FIRST_FRAME); setAnimation(animation); invalidateParentCaches(); invalidate(true); } |
先通过animation.setStartTime(Animation.START_ON_FIRST_FRAME)设置animation的启动时间为Animation.START_ON_FIRST_FRAME,也就是说,animation的第一帧被绘制的时间,即为start time
然后调用setAnimation(animation)将该animation设置为view的mCurrentAnimation中
最后调用invalidate(true)强制view重画。
View重画后,上一节说过,带有parent和drawingTime的draw函数会被调用:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { …… Transformation transformToApply = null; boolean concatMatrix = false; …… final Animation a = getAnimation(); if (a != null) { more = drawAnimation(parent, drawingTime, a, scalingRequired); concatMatrix = a.willChangeTransformationMatrix(); if (concatMatrix) { mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM; } transformToApply = parent.getChildTransformation(); } else { …… } concatMatrix |= !childHasIdentityMatrix; …… float alpha = usingRenderNodeProperties ? 1 : (getAlpha() * getTransitionAlpha()); if (transformToApply != null || alpha < 1 || !hasIdentityMatrix() || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) == PFLAG3_VIEW_IS_ANIMATING_ALPHA) { if (transformToApply != null || !childHasIdentityMatrix) { int transX = 0; int transY = 0;
if (offsetForScroll) { transX = -sx; transY = -sy; }
if (transformToApply != null) { if (concatMatrix) { if (usingRenderNodeProperties) { renderNode.setAnimationMatrix(transformToApply.getMatrix()); } else { // Undo the scroll translation, apply the transformation matrix, // then redo the scroll translate to get the correct result. canvas.translate(-transX, -transY); canvas.concat(transformToApply.getMatrix()); canvas.translate(transX, transY); } parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION; }
…… } …… if (a != null && !more) { if (!hardwareAccelerated && !a.getFillAfter()) { onSetAlpha(255); } parent.finishAnimatingView(this, a); } …… return more; } |
drawingTime,其实就是指这次draw操作的触发时间,也就是ViewRootImpl调用
drawSoftware的时间:
//ViewRootImpl.java private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { …… attachInfo.mDrawingTime = SystemClock.uptimeMillis(); …… return true; } |
至于parent ViewGroup,这边看到,其内部会有一个变量mChildTransformation保存当前某一child view的animation对应的transformation片段数据
在draw函数中,先通过调用drawAnimation并传入parent,animation和drawingTime来来得到当前的时间下对应的transformation片段数据,并保存到parent ViewGroup的
mChildTransformation中,然后drawAnimation内部还会判断在当前transformation片段数据执行完后,是否还存在animation片段数据?如果有,则会调用invalidate触发view的下一次绘制,这样才能保持animation不会中断
在draw函数,接着调用parent.getChildTransformation()获取当前的transformation数据,然后获取其Matrix并设置到canvas中
最后判断more是否为false,如果是false,说明animation已经结束,调用
parent.finishAnimatingView(this,a)执行animation结束相关扫尾代码
由于android View都是在主线程的完成绘制的,而主线程的负载又不是均衡的,所以会导致View animation的帧率无法得到保障,当然,对于短时的动画来说影响不大,但是对于长时间并且比较复杂的动画,建议还是使用Surface来绘制,然后开一条线程来按照你想要的帧率来触发重绘