Android中的绘制机制

我们知道,其实Android系统的绘制几乎都是在底层完成(调用Native的方法,可参考Canvas类),这里,我主要是想讲一讲我对于Android在framework这一层的绘制机制。不会涉及到太多底层的东西,这一块目前我也没做过多深入的研究。

一,View如何绘制

View#draw方法,提供了一个最基本的绘制机制,子类通常不需要重写这个方法。我们可以通过查看其源码,在View的draw里面,它通常需要做以下几件事情:

    1,绘制自己的背景,如果有的话,因为背景始终都在最后面,所以要先画。

    2,如果需要的话,保存canvas的layer来准备绘制渐变效果,比如说有alpha动画等。

    3,绘制View的内容,其实就是调用onDraw方法,让子类可以绘制自己的内容。

    4,绘制自己的child,具体怎么绘制孩子,ViewGroup会去重写相应的方法。基类的View只是把调用这绘制child的方法,当然这个方法在View里面,应该是什么都不做。

    5,如果需要的话,画渐变效果并还原保存的canvas层。

    6,绘制其他的元素,比如scrollbar等。

通常,View里面要做这些事情,而且顺序不能改变。

下面我们来分析一下代码:

[java]  view plain  copy
  1. final int privateFlags = mPrivateFlags;  
  2.         final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
  3.                 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
  4.         mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
这几句就是判断当前View是否是不透明或者是dirty(这个表示当前View是否需要重新绘制),它都是根据一些FLAG来判断的。如果dirtyOpaque为true,就会去绘制背景,也会调用onDraw这个方法,子类里面就可以重写这个方法。这里插一点题外话,如果是ViewGroup,默认情况下是不会去调用onDraw的,因为它默认是透明的,不需要去绘制。
[java]  view plain  copy
  1. if (!dirtyOpaque) {  
  2.             final Drawable background = mBGDrawable;  
  3.             if (background != null) {  
  4.                 final int scrollX = mScrollX;  
  5.                 final int scrollY = mScrollY;  
  6.   
  7.                 if (mBackgroundSizeChanged) {  
  8.                     background.setBounds(00,  mRight - mLeft, mBottom - mTop);  
  9.                     mBackgroundSizeChanged = false;  
  10.                 }  
  11.   
  12.                 if ((scrollX | scrollY) == 0) {  
  13.                     background.draw(canvas);  
  14.                 } else {  
  15.                     canvas.translate(scrollX, scrollY);  
  16.                     background.draw(canvas);  
  17.                     canvas.translate(-scrollX, -scrollY);  
  18.                 }  
  19.             }  
  20.         }  

这段代码就是绘制背景图了,代码没有什么特别之处。这里有一点需要注意,background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  这里设置了背景的大小,也就是说,当mBackgroundSizeChanged标志量为true时,就会设置其bound,我们可以看看源码,而mBackgroundSizeChanged是在setFrame方法里面调用的,而#setFrame()是在#layout()里面调用的,说白了,当View重新layout时,就会重新去设置背景的大小,当然了,第一次肯干是需要设置的,mBackgroundSizeChanged在调用了#mBackgroundSizeChanged()就会设置为true。

[java]  view plain  copy
  1. // skip step 2 & 5 if possible (common case)  
  2.         final int viewFlags = mViewFlags;  
  3.         boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  
  4.         boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  
  5.         if (!verticalEdges && !horizontalEdges) {  
  6.             // Step 3, draw the content  
  7.             if (!dirtyOpaque) onDraw(canvas);  
  8.   
  9.             // Step 4, draw the children  
  10.             dispatchDraw(canvas);  
  11.   
  12.             // Step 6, draw decorations (scrollbars)  
  13.             onDrawScrollBars(canvas);  
  14.   
  15.             // we're done...  
  16.             return;  
  17.         }  
这段代码就是跳过上述的第2和第5步。如果既不需要绘制垂直边缘,也不需要绘制水平边缘的话,那么就走正常的逻辑:第3,第4,和第6步。

    第3步:绘制自己的内容,if (!dirtyOpaque) onDraw(canvas);

    第4步:绘制子孩子, dispatchDraw(canvas);

    第6步,绘制滚动条,onDrawScrollBars(canvas);

经过这几步,绘制就完成了,直接return。

关于第5步的实现,这一步,代码很多,其核心就是保存canvas的layers,再绘制,再还原其layers,代码我就不贴出来了,有兴趣的可以去看源码。

二,ViewGroup如何绘制child

从第一节我们可以得知,View里面的实现绘制子孩子是调用了View#dispatchDraw,ViewGroup会实现这个方法,去按照一定的算法去绘制child。我们一起来看看dispatchDraw的实现。

[java]  view plain  copy
  1. if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {  
  2.             final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;  
  3.   
  4.             for (int i = 0; i < count; i++) {  
  5.                 final View child = children[i];  
  6.                 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {  
  7.                     final LayoutParams params = child.getLayoutParams();  
  8.                     attachLayoutAnimationParameters(child, params, i, count);  
  9.                     bindLayoutAnimation(child);  
  10.                     if (cache) {  
  11.                         child.setDrawingCacheEnabled(true);  
  12.                         child.buildDrawingCache(true);  
  13.                     }  
  14.                 }  
  15.             }  
  16.   
  17.             final LayoutAnimationController controller = mLayoutAnimationController;  
  18.             if (controller.willOverlap()) {  
  19.                 mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;  
  20.             }  
  21.   
  22.             controller.start();  
  23.   
  24.             mGroupFlags &= ~FLAG_RUN_ANIMATION;  
  25.             mGroupFlags &= ~FLAG_ANIMATION_DONE;  
  26.   
  27.             if (cache) {  
  28.                 mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;  
  29.             }  
  30.   
  31.             if (mAnimationListener != null) {  
  32.                 mAnimationListener.onAnimationStart(controller.getAnimation());  
  33.             }  
  34.         }  

这些都做完了,就要开始绘制它的child了。先看这段代码: 

[java]  view plain  copy
  1. if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {  
  2.             for (int i = 0; i < count; i++) {  
  3.                 final View child = children[i];  
  4.                 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
  5.                     more |= drawChild(canvas, child, drawingTime);  
  6.                 }  
  7.             }  
  8.         } else {  
  9.             for (int i = 0; i < count; i++) {  
  10.                 final View child = children[getChildDrawingOrder(count, i)];  
  11.                 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
  12.                     more |= drawChild(canvas, child, drawingTime);  
  13.                 }  
  14.             }  
  15.         }  

这个 (flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0 判断就是去check当前这个ViewGroup是否使用drawing order,这个drawing order是什么意思呢?默认情况下,后加入(后调用addView)的child,通常是最后绘制,因为它后加入,理应显示在最上面。但是,还有一种情况,gallery,listview,gridview这样的特殊的ViewGroup,你就不能按照这种方式去管理绘制,因为gallery,listview,gridview它们的child有可能是会被复用的,最先加进去的,有可能上面显示的是最后一条数据,所以说,此时他就需要显示在最上面。

上面的代码,if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) 如果成立的话,也就是说,当前ViewGroup不需要用drawing order,那么就按正常的从0 - count绘制child。这里调用了ViewGroup#drawChild,它的分析,后面会讲到。如果上面的条件不成立的话,就会用drawing order来绘制,它调用了ViewGroup#getChildDrawingOrder()方法来返回一个索引值。

最后,如果动画做完了的话,它会调用ViewGroup#ontifyAnimationListener()方法 

[java]  view plain  copy
  1. if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&  
  2.                 mLayoutAnimationController.isDone() && !more) {  
  3.             // We want to erase the drawing cache and notify the listener after the  
  4.             // next frame is drawn because one extra invalidate() is caused by  
  5.             // drawChild() after the animation is over  
  6.             mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;  
  7.             final Runnable end = new Runnable() {  
  8.                public void run() {  
  9.                    notifyAnimationListener();  
  10.                }  
  11.             };  
  12.             post(end);  
  13.         }  
这样,dispatchDraw就算完了,其实他的逻辑不算是复杂,最本质就是调用一些方法(比如说drawChild)去绘制child,然后会设置一些flag。

关于ViewGroup#drawChild的实现,最本质上需要处理以下两点:

    1,动画,从动画中取出一个变换矩阵,根据这个矩阵去绘制出child。

    2,Alpha值,因为存在AlphaAnimation,所以,需要给canvas设置alpha值。

我们看代码会发现,有这么几句核心代码:

final Animation a = child.getAnimation();

more = a.getTransformation(drawingTime, mChildTransformation);

这里,先去拿动画的对象(如果有的话),然后,去取到动画中当前时间所对应的变换(Transformation),这里面包含了矩阵信息和Alpha值。这ViewGroup#drawChild()方法里面,它还会调用View#onAnimationStart()方法,还会调用View#draw()方法去绘制child。

三,invalidate

关于invalidate,我的理解就是把当前的view标记成无效,然后发送一个绘制消息,而这人消息会触发绘制。所以通常我们要更新UI,就调用这个方法,简而言之,就是要重新绘制UI,就调用invalidate()方法。

[java]  view plain  copy
  1. /** 
  2.      * Invalidate the whole view. If the view is visible, {@link #onDraw} will 
  3.      * be called at some point in the future. This must be called from a 
  4.      * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}. 
  5.      */  
  6.     public void invalidate() {  
  7.         if (ViewDebug.TRACE_HIERARCHY) {  
  8.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);  
  9.         }  
  10.   
  11.         if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {  
  12.             mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;  
  13.             final ViewParent p = mParent;  
  14.             final AttachInfo ai = mAttachInfo;  
  15.             if (p != null && ai != null) {  
  16.                 final Rect r = ai.mTmpInvalRect;  
  17.                 r.set(00, mRight - mLeft, mBottom - mTop);  
  18.                 // Don't call invalidate -- we don't want to internally scroll  
  19.                 // our own bounds  
  20.                 p.invalidateChild(this, r);  
  21.             }  
  22.         }  
  23.     }  
这里面,它调用了parent的ViewParent#invalidateChild()方法,ViewGroup实现了这个方法。 
[java]  view plain  copy
  1. /** 
  2.      * Don't call or override this method. It is used for the implementation of 
  3.      * the view hierarchy. 
  4.      */  
  5.     public final void invalidateChild(View child, final Rect dirty) {  
  6.         if (ViewDebug.TRACE_HIERARCHY) {  
  7.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD);  
  8.         }  
  9.   
  10.         ViewParent parent = this;  
  11.   
  12.         final AttachInfo attachInfo = mAttachInfo;  
  13.         if (attachInfo != null) {  
  14.             final int[] location = attachInfo.mInvalidateChildLocation;  
  15.             location[CHILD_LEFT_INDEX] = child.mLeft;  
  16.             location[CHILD_TOP_INDEX] = child.mTop;  
  17.   
  18.             // If the child is drawing an animation, we want to copy this flag onto  
  19.             // ourselves and the parent to make sure the invalidate request goes  
  20.             // through  
  21.             final boolean drawAnimation = (child.mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION;  
  22.   
  23.             // Check whether the child that requests the invalidate is fully opaque  
  24.             final boolean isOpaque = child.isOpaque() && !drawAnimation &&  
  25.                     child.getAnimation() != null;  
  26.             // Mark the child as dirty, using the appropriate flag  
  27.             // Make sure we do not set both flags at the same time  
  28.             final int opaqueFlag = isOpaque ? DIRTY_OPAQUE : DIRTY;  
  29.   
  30.             do {  
  31.                 View view = null;  
  32.                 if (parent instanceof View) {  
  33.                     view = (View) parent;  
  34.                 }  
  35.   
  36.                 if (drawAnimation) {  
  37.                     if (view != null) {  
  38.                         view.mPrivateFlags |= DRAW_ANIMATION;  
  39.                     } else if (parent instanceof ViewRoot) {  
  40.                         ((ViewRoot) parent).mIsAnimating = true;  
  41.                     }  
  42.                 }  
  43.   
  44.                 // If the parent is dirty opaque or not dirty, mark it dirty with the opaque  
  45.                 // flag coming from the child that initiated the invalidate  
  46.                 if (view != null && (view.mPrivateFlags & DIRTY_MASK) != DIRTY) {  
  47.                     view.mPrivateFlags = (view.mPrivateFlags & ~DIRTY_MASK) | opaqueFlag;  
  48.                 }  
  49.   
  50.                 parent = parent.invalidateChildInParent(location, dirty);  
  51.             } while (parent != null);  
  52.         }  
  53.     }  

ViewGroup#invalidateChild()方法里面有一个do while 循环,它是干什么的呢,简而言之,就是去确定一个dirty的区域和location,这个dirty的Rect和int[] 的location都是AttachInfo的成员变量,也就是说,它把do while 循环执行完后,AttachInfo里面的dirty Rect已经计算好了,有了dirty区域,底层就可以绘制指定的区域。

这里的do - while循环,就是从当前view(调用invalidate的那个View)一层一层地向上找其parent,然后调用parent的ViewParent#invalidateChildInParent(),直到找到根View为止(Root view 没有parent)。

那么为什么要计算dirty区域呢?我们知道,当由当你调用View#invalidate()方法,它默认的区域是整个View的大小,而这个区域的大小可能与其parent的区域有交叉,也有可能与其parent的parent的区域有交叉,所以需要一层一层地向上,这样才能确定出最终需要绘制的区域大小。

ViewParent#invalidateChildInParent方法的实现如下: 

[java]  view plain  copy
  1. /** 
  2.      * Don't call or override this method. It is used for the implementation of 
  3.      * the view hierarchy. 
  4.      * 
  5.      * This implementation returns null if this ViewGroup does not have a parent, 
  6.      * if this ViewGroup is already fully invalidated or if the dirty rectangle 
  7.      * does not intersect with this ViewGroup's bounds. 
  8.      */  
  9.     public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {  
  10.         if (ViewDebug.TRACE_HIERARCHY) {  
  11.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD_IN_PARENT);  
  12.         }  
  13.   
  14.         if ((mPrivateFlags & DRAWN) == DRAWN) {  
  15.             if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=  
  16.                         FLAG_OPTIMIZE_INVALIDATE) {  
  17.                 dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,  
  18.                         location[CHILD_TOP_INDEX] - mScrollY);  
  19.   
  20.                 final int left = mLeft;  
  21.                 final int top = mTop;  
  22.   
  23.                 if (dirty.intersect(00, mRight - left, mBottom - top) ||  
  24.                         (mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION) {  
  25.                     mPrivateFlags &= ~DRAWING_CACHE_VALID;  
  26.   
  27.                     location[CHILD_LEFT_INDEX] = left;  
  28.                     location[CHILD_TOP_INDEX] = top;  
  29.   
  30.                     return mParent;  
  31.                 }  
  32.             } else {  
  33.                 mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;  
  34.   
  35.                 location[CHILD_LEFT_INDEX] = mLeft;  
  36.                 location[CHILD_TOP_INDEX] = mTop;  
  37.   
  38.                 dirty.set(00, mRight - location[CHILD_LEFT_INDEX],  
  39.                         mBottom - location[CHILD_TOP_INDEX]);  
  40.   
  41.                 return mParent;  
  42.             }  
  43.         }  
  44.   
  45.         return null;  
  46.     }  
从上面的代码可以看出,它返回的始终是 mParent ,也就是说,返回自己的parent。这个方法里面,主要就是计算了dirty和location的值,前面已经说过了,这两个值都存在attachInfo里面。AttachInfo是View的一个内部类,它就是当前View和Window的一系列信息,如Window, Handler, RootView,还有刚才说的dirty rect (mTmpInvalRect), location (mInvalidateChildLocation)。

这个attachInfo是何时设置到View里面的呢?此时,我们想一想,在Activity创建的时候,我们会调用setContentView方法,你可以看其源码的实现:

[java]  view plain  copy
  1. public void setContentView(View view) {  
  2.         getWindow().setContentView(view);  
  3.     }  
它得到当前的window,调用setContentView,如果你有兴趣可以继续研究,最终代码是在 frameworks\base\policy\src\com\android\internal\policy\impl\PhoneWindow 中实现的。 View#dispatchAttachedToWindow 方法,会设置一个attachInfo。ViewGroup重写了这个方法,它里面,也就是依次调用child的 dispatchAttachedToWindow 方法。 这个方法还会在ViewRoot中调用。

以上就是我关于Android中的绘制机制的一些研究,可能很肤浅,有些问题还需要深入研究。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值