我们知道,其实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里面要做这些事情,而且顺序不能改变。
下面我们来分析一下代码:
- final int privateFlags = mPrivateFlags;
- final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
- (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
- mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
- if (!dirtyOpaque) {
- final Drawable background = mBGDrawable;
- if (background != null) {
- final int scrollX = mScrollX;
- final int scrollY = mScrollY;
- if (mBackgroundSizeChanged) {
- background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
- mBackgroundSizeChanged = false;
- }
- if ((scrollX | scrollY) == 0) {
- background.draw(canvas);
- } else {
- canvas.translate(scrollX, scrollY);
- background.draw(canvas);
- canvas.translate(-scrollX, -scrollY);
- }
- }
- }
这段代码就是绘制背景图了,代码没有什么特别之处。这里有一点需要注意,background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); 这里设置了背景的大小,也就是说,当mBackgroundSizeChanged标志量为true时,就会设置其bound,我们可以看看源码,而mBackgroundSizeChanged是在setFrame方法里面调用的,而#setFrame()是在#layout()里面调用的,说白了,当View重新layout时,就会重新去设置背景的大小,当然了,第一次肯干是需要设置的,mBackgroundSizeChanged在调用了#mBackgroundSizeChanged()就会设置为true。
- // 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);
- // Step 6, draw decorations (scrollbars)
- onDrawScrollBars(canvas);
- // we're done...
- return;
- }
第3步:绘制自己的内容,if (!dirtyOpaque) onDraw(canvas);
第4步:绘制子孩子, dispatchDraw(canvas);
第6步,绘制滚动条,onDrawScrollBars(canvas);
经过这几步,绘制就完成了,直接return。
关于第5步的实现,这一步,代码很多,其核心就是保存canvas的layers,再绘制,再还原其layers,代码我就不贴出来了,有兴趣的可以去看源码。
二,ViewGroup如何绘制child
从第一节我们可以得知,View里面的实现绘制子孩子是调用了View#dispatchDraw,ViewGroup会实现这个方法,去按照一定的算法去绘制child。我们一起来看看dispatchDraw的实现。
- if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
- final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
- for (int i = 0; i < count; i++) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
- final LayoutParams params = child.getLayoutParams();
- attachLayoutAnimationParameters(child, params, i, count);
- bindLayoutAnimation(child);
- if (cache) {
- child.setDrawingCacheEnabled(true);
- child.buildDrawingCache(true);
- }
- }
- }
- final LayoutAnimationController controller = mLayoutAnimationController;
- if (controller.willOverlap()) {
- mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
- }
- controller.start();
- mGroupFlags &= ~FLAG_RUN_ANIMATION;
- mGroupFlags &= ~FLAG_ANIMATION_DONE;
- if (cache) {
- mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
- }
- if (mAnimationListener != null) {
- mAnimationListener.onAnimationStart(controller.getAnimation());
- }
- }
这些都做完了,就要开始绘制它的child了。先看这段代码:
- if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
- for (int i = 0; i < count; i++) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- } else {
- for (int i = 0; i < count; i++) {
- final View child = children[getChildDrawingOrder(count, i)];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- }
这个 (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()方法
- if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
- mLayoutAnimationController.isDone() && !more) {
- // We want to erase the drawing cache and notify the listener after the
- // next frame is drawn because one extra invalidate() is caused by
- // drawChild() after the animation is over
- mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
- final Runnable end = new Runnable() {
- public void run() {
- notifyAnimationListener();
- }
- };
- post(end);
- }
关于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()方法。
- /**
- * Invalidate the whole view. If the view is visible, {@link #onDraw} will
- * be called at some point in the future. This must be called from a
- * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
- */
- public void invalidate() {
- if (ViewDebug.TRACE_HIERARCHY) {
- ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
- }
- if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
- mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
- final ViewParent p = mParent;
- final AttachInfo ai = mAttachInfo;
- if (p != null && ai != null) {
- final Rect r = ai.mTmpInvalRect;
- r.set(0, 0, mRight - mLeft, mBottom - mTop);
- // Don't call invalidate -- we don't want to internally scroll
- // our own bounds
- p.invalidateChild(this, r);
- }
- }
- }
- /**
- * Don't call or override this method. It is used for the implementation of
- * the view hierarchy.
- */
- public final void invalidateChild(View child, final Rect dirty) {
- if (ViewDebug.TRACE_HIERARCHY) {
- ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD);
- }
- ViewParent parent = this;
- final AttachInfo attachInfo = mAttachInfo;
- if (attachInfo != null) {
- final int[] location = attachInfo.mInvalidateChildLocation;
- location[CHILD_LEFT_INDEX] = child.mLeft;
- location[CHILD_TOP_INDEX] = child.mTop;
- // If the child is drawing an animation, we want to copy this flag onto
- // ourselves and the parent to make sure the invalidate request goes
- // through
- final boolean drawAnimation = (child.mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION;
- // Check whether the child that requests the invalidate is fully opaque
- final boolean isOpaque = child.isOpaque() && !drawAnimation &&
- child.getAnimation() != null;
- // Mark the child as dirty, using the appropriate flag
- // Make sure we do not set both flags at the same time
- final int opaqueFlag = isOpaque ? DIRTY_OPAQUE : DIRTY;
- do {
- View view = null;
- if (parent instanceof View) {
- view = (View) parent;
- }
- if (drawAnimation) {
- if (view != null) {
- view.mPrivateFlags |= DRAW_ANIMATION;
- } else if (parent instanceof ViewRoot) {
- ((ViewRoot) parent).mIsAnimating = true;
- }
- }
- // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
- // flag coming from the child that initiated the invalidate
- if (view != null && (view.mPrivateFlags & DIRTY_MASK) != DIRTY) {
- view.mPrivateFlags = (view.mPrivateFlags & ~DIRTY_MASK) | opaqueFlag;
- }
- parent = parent.invalidateChildInParent(location, dirty);
- } while (parent != null);
- }
- }
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方法的实现如下:
- /**
- * Don't call or override this method. It is used for the implementation of
- * the view hierarchy.
- *
- * This implementation returns null if this ViewGroup does not have a parent,
- * if this ViewGroup is already fully invalidated or if the dirty rectangle
- * does not intersect with this ViewGroup's bounds.
- */
- public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
- if (ViewDebug.TRACE_HIERARCHY) {
- ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD_IN_PARENT);
- }
- if ((mPrivateFlags & DRAWN) == DRAWN) {
- if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
- FLAG_OPTIMIZE_INVALIDATE) {
- dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
- location[CHILD_TOP_INDEX] - mScrollY);
- final int left = mLeft;
- final int top = mTop;
- if (dirty.intersect(0, 0, mRight - left, mBottom - top) ||
- (mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION) {
- mPrivateFlags &= ~DRAWING_CACHE_VALID;
- location[CHILD_LEFT_INDEX] = left;
- location[CHILD_TOP_INDEX] = top;
- return mParent;
- }
- } else {
- mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
- location[CHILD_LEFT_INDEX] = mLeft;
- location[CHILD_TOP_INDEX] = mTop;
- dirty.set(0, 0, mRight - location[CHILD_LEFT_INDEX],
- mBottom - location[CHILD_TOP_INDEX]);
- return mParent;
- }
- }
- return null;
- }
这个attachInfo是何时设置到View里面的呢?此时,我们想一想,在Activity创建的时候,我们会调用setContentView方法,你可以看其源码的实现:
- public void setContentView(View view) {
- getWindow().setContentView(view);
- }
以上就是我关于Android中的绘制机制的一些研究,可能很肤浅,有些问题还需要深入研究。