绘制过程主要是把View对象绘制到屏幕上,并且如果该View是一个ViewGroup,则需要递归绘 制该ViewGroup中所包含的所有子视图。
视图中可绘制的元素
在介绍视图绘制之前,先来了解一下一个视图中都包含哪些需要绘制的元素,比如一个TextView, 除了具体的文字外,还需要绘制文字的背景等。那么,视图中都包含哪些绘制元素呢? 总的来讲,绘制元素包括四个:
- View背景。
每个视图都有一个背景,比如LinearLayout、TextView,背景可以是一个颜色值,也可以是一幅图片,甚至可以是任务Drawable对象,比如一个shader、一个DrawableState等。应用程序可以使用 setBackgroundColor()、setBackgroundDrawable()、setBackgroundResouce()三个函数设置不同的背景。
- 视图本身的内容。
比如,对 于 TextView而言,内容就是具体的文字,对于一个ImageView而 言,内容就是一幅图片。应用程序会在视图的onDraw()函数中绘制具体的内容。
- 渐变边框。
渐变边框的作用是为了让视图的边框看起来更有层次感, 其本质是一个Shader对象
- 滚动条。
与 PC上的滚动条不同,Android中的滚动条仅仅是显示滚动的状态,而不能被用户直接下拉。
在以上四个元素中,应用程序一般只需要重载View的 0nDraw()函数,并绘制视图本身,其他的三 个元素都是由View系统自动完成的。并 且 View系统提供了相应的API接口,应用程序只需要设置元 素所使用的具体颜色或者Drawable对象即可。
绘制过程的设计思路
绘制的总体过程如图所示,图中虚线代表了重载关系。 绘制过程从ViewRoot的 performTraversals()函数中开始,首先调用,ViewRoot中的draw()函数,该函数中进行一定的前端处理后,再调用mView.draw()。mView对象就是窗口的根视图,对 于 Activity而言,就是 PhoneWindow.DecorView 对象。 在一般情况下,View对象不应该重载draw()函数,因此,mView.draw()就调用到了 View类的 draw()函数。该函数的内部过程也就是View系统绘制过程的核心过程,该函数中会依次绘制上一节所讲的四种绘制元素,其中绘制视图本身的具体实现就是回调onDraw()函数,应用程序一般会重载onDraw()函数以绘制所设计的View的真正界面内容。 绘制完界面内容后,如果该视图内部还包含子视图,贝U调 用 dispatchDraw()函数,ViewGroup重载了该函数。因此,实际调用的是ViewGroup中的dispatchDraw()函数,应用程序不应该再重载ViewGroup类中的dispatchDmwO函数,因为该函数内部已经有了默认的实现,并且该实现代表了 View系统的内部流程。 dispatchDraw()内部会在一个for()循环语句中,循 环 调 用 drawChild()分别绘制每一个子视图,而 drawChild()内部又会调用draw()函数完成子视图的内部绘制工作。当然,如果子视图本身也是一个 ViewGroup,就会递归执行以上流程。 从以上设计思路来看,它与measure及 layout的过程极其相似。 下面就具体来看以上主要函数的内部执行过程。
ViewRoot 中 draw ()的内部流程
ViewRoot中的draw()函数主要处理一些根视图中的特有属性,并且处理完毕后同样要调用View类 中的draw()进行具体的绘制。 Surface按照底层的驱动模式可以分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一 种是使用CPU及内存模拟的Surface。因此,根视图中将针对不同的Surface采用不同的方式从该Surface中获取一个Canvas对象,并将该Canvas对象派发到整个视图中,对于非根视图而言,它并不区分底层是使用显卡模式,还是使用CPU模式。
/**
* ViewRoot中的draw()函数主要处理一些根视图中的特有属性,并且处理完毕后同样要调用View类
* 中 的draw()进行具体的绘制。
* Surface按照底层的驱动模式可以分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一
* 种是使用CPU及内存模拟的Surface。因此,根视图中将针对不同的Surface采用不同的方式从该Surface
* 中获取一个Canvas对象,并 将 该Canvas对象派发到整个视图中,对于非根视图而言,它并不区分底层
* 是使用显卡模式,还是使用CPU模式。
* @param fullRedrawNeeded
*/
private void draw(boolean fullRedrawNeeded) {
/**
* 检 查 Surface是否无效。在正常情况下, Surface都是有效的,除 非WmS发生异常不能为该客
* 户端分配有效的Surface, isValide()才会返回false。如 果 Surface无效,则终止绘制过程
*/
Surface surface = mSurface;
if (surface == null || !surface.isValid()) {
return;
}
/**
* 执行注册过的Runnable对象。 ViewRoot中使用一个静态列表,可以向该静态列表中添加一些
* Runnable对象,本步骤则把这些Runnable对象调用post()发送 到Handler队列中,以便下次消息循环时
* 处理这些Runnable对象。这个变量的名称是sFirstDrawComplete,有些读者可能觉得变量名称有点奇怪,
* 为什么是Complete呢,明明还没有开始绘制?因为该变量中保存的Runnable对象会在下个消息循环中
* 执行,而执行前,接下来的绘制过程必须先被执行。
*/
if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
for (int i=0; i<sFirstDrawHandlers.size(); i++) {
post(sFirstDrawHandlers.get(i));
}
}
}
/**
* 调 用 scrollToRectOrFocusO。几乎在所有的情况下,该函数内部都不会执行什么,所以其内部
* 执行流程忽略。该函数本来的设计目的是对mScmllY变量进行调整,调整的依据是调整到第一个Focus
* 视图中。
*/
scrollToRectOrFocus(null, false);
/**
* 如果根视图内部包含Scroller对象,则调用该对象的computeScrollOffset()获取新的滚动值,并
* 赋值给局部变量yoff,关 于Scroller的详细意义将在后面小节中单独介绍。该对象的computeScrollOffset()
* 的意义是计算是否发生滚动,该函数返回值类型为boolean,比如,当用户在一个ListView上滑动手指
* 后 ,会在一小段时间内发生滚动,ListView内部有一个Scroller对象,在这个期间调用computeScrollOffsetO
* 将 返 回true。
*/
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
/**
* 判 断 该Surface是 否 有SurfaceHolder对象。如果有则意味着该Sb*face是应用程序创建的,因
* 此所有的绘制操作应该由应用程序自身去负责,于 是 View系统退出绘制。如果不是,才 开 始View绘
* 制的内部流程。
*/
int yoff;
final boolean scrolling = mScroller != null && mScroller.computeScrollOffset();
if (scrolling) {
yoff = mScroller.getCurrY();
} else {
yoff = mScrollY;
}
if (mCurScrollY != yoff) {
mCurScrollY = yoff;
fullRedrawNeeded = true;
}
float appScale = mAttachInfo.mApplicationScale;
boolean scalingRequired = mAttachInfo.mScalingRequired;
Rect dirty = mDirty;
if (mSurfaceHolder != null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();
return;
}
/**
* 如 果 Surface是 由 OpenGL实现的,则开始按照G L 的处理方式进行处理。
*/
if (mUseGL) {
if (!dirty.isEmpty()) {
/**
* (1 )以全局变量mGlCanvas作 为 canvas的值,并 调 用canvas.save()保 存 该Canvas内部的各种属性
* 及状态,因为接下来View树内部在绘制的过程中会修改Canvas对象的相关属性。
*/
Canvas canvas = mGlCanvas;
if (mGL != null && canvas != null) {
mGL.glDisable(GL_SCISSOR_TEST);
mGL.glClearColor(0, 0, 0, 0);
mGL.glClear(GL_COLOR_BUFFER_BIT);
mGL.glEnable(GL_SCISSOR_TEST);
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mAttachInfo.mIgnoreDirtyState = true;
mView.mPrivateFlags |= View.DRAWN;
/**
* (2 )如果根视图内部包含Translator对象,则需要先经过Translator对象对该canvas对象进行一定
* 的调整。 Translator的作用主要是根据设备的硬件参数对Canvas的相关绘制属性进行一定的调整,该函
* 数的内部一般由驱动设计者支持实现。调整的过程由三个函数调用组成,
*/
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
/**
* 调 用canvas的 setScreenDensity()设置屏幕密度。这里需要区分不同语境中density的含义的不同。
* 首先,定义屏幕密度(density) 概念的作用是为了让不同分辨率的屏幕显示的视图能够看起来大小
* 相同。在传统的编程方式中,如果用像素指定视图的大小,对于分辨率高的显示器而言,像素密度高,
* 所以相同像素的实际尺寸会变小,于是引入density的概念。 Android中 160dpi的屏幕的density值定义
* 为 1,如果屏幕分辨率增加,比如240dpi,那 么 density的值也就越高,为 240/160=1.5,而分辨率低的
* 屏幕,比 如 120dpi,其密度就低,为 120/160=0.75。有了 density后,应用程序可以设置视图的大小单
* 位 为 dip,即密度无关像素(density independent pixel),从而在绘制视图时, View系统会根据不同的屏
* 幕分辨率将其换算成不同的像素。
* 而本步设置的screenDensity,却是指屏幕的分辨率值。当 参 数scalingRequired为 true时,该值为
* DisplayMetric.DENSITY_DEVICE,该常量是在系统启动时调用getProp()函数获取的设备参数,比如240、
* 160、 120等;如 果 scalingRequired为 false,那 么 screenDensity将被赋值为0, 0 是一个特殊值,并不是
* 说屏幕分辨率为0 , 而是指视图绘制没有指定具体的分辨率,从而在绘制时一个dpi将对应一个真实的
* 物理像素。
*/
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
/**
*(4) 调 用 mView.draw(canvas)。该步骤才真正启动视图树的绘制过程,注意这里是将canvas作为
*参数,这也就是为什么应用程序中不能保存这个Canvas的原因,因为它是一个临时变量。 mView.draw()
* 实际调用的是View类 的 dmw()函数,关于其内部流程将在后面小节中介绍。
*/
mView.draw(canvas);
if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
}
} finally {
/**
* ( 5 ) 完成视图树的绘制后,绘制工作就算结束了,因为调用Canvas的 restoreToCount()将 Canvas
* 的内部状态恢复到绘制之前,该步骤与前面的canvas.saveO函数是对称调用的。
*/
canvas.restoreToCount(saveCount);
}
mAttachInfo.mIgnoreDirtyState = false;
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
checkEglErrors();
/**
* 如 果 是 Debug模式,并且模式中要求显示FPS,即CPU的使用率,则调用一个native函数
* nativeShowFPSO给屏幕上方绘制一个条状的统计图。
*/
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if (sDrawTime != 0) {
nativeShowFPS(canvas, now - sDrawTime);
}
sDrawTime = now;
}
}
}
/**
* 最后,如果屏幕正在滚动,则需要再次发起一个重绘命令scheduleTravasals(),以便接着绘制,
* 直到滚动结束,滚动的标志scrolling来 源 于Scroller对 象 的computeScrollOffset()函数返回值。
*/
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return;
}
/**
* 如 果 Surface不 是 OpenGL实现的,则开始按照非G L 的处理方式进行处理。该步骤内部与上
* 一步基本上是相同的,唯一的区别在于如何获得Canvas对象。 G L方式中,内部使用mGlCanvas全局
* 变 量 保 存canvas对象,该变量是在G L 的初始化时进行赋值的;而 非 G L 方式中, Canvas对象需要调
* 用 surface对 象 的lockCanvas()获取,其他过程完全相同,此处不再赘述。
* 至此, ViewRoot中 的 draw()函数就执行完毕,一次绘制过程也就结束了,下一次的绘制将在下一
* 个消息循环中执行。
*/
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.union(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Draw " + mView + "/"
+ mWindowAttributes.getTitle()
+ ": dirty={" + dirty.left + "," + dirty.top
+ "," + dirty.right + "," + dirty.bottom + "} surface="
+ surface + " surface.isValid()=" + surface.isValid() + ", appScale:" +
appScale + ", width=" + mWidth + ", height=" + mHeight);
}
if (!dirty.isEmpty() || mIsAnimating) {
Canvas canvas;
try {
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
canvas = surface.lockCanvas(dirty);
if (left != dirty.left || top != dirty.top || right != dirty.right ||
bottom != dirty.bottom) {
mAttachInfo.mIgnoreDirtyState = true;
}
// TODO: Do this in native
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
Log.e(TAG, "OutOfResourcesException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
} catch (IllegalArgumentException e) {
Log.e(TAG, "IllegalArgumentException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
}
try {
if (!dirty.isEmpty() || mIsAnimating) {
long startTime = 0L;
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
startTime = SystemClock.elapsedRealtime();
}
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
if (!canvas.isOpaque() || yoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mView.mPrivateFlags |= View.DRAWN;
if (DEBUG_DRAW) {
Context cxt = mView.getContext();
Log.i(TAG, "Drawing: package:" + cxt.getPackageName() +
", metrics=" + cxt.getResources().getDisplayMetrics() +
", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
}
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
} finally {
mAttachInfo.mIgnoreDirtyState = false;
canvas.restoreToCount(saveCount);
}
if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
}
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if (sDrawTime != 0) {
nativeShowFPS(canvas, now - sDrawTime);
}
sDrawTime = now;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime);
}
}
} finally {
surface.unlockCanvasAndPost(canvas);
}
}
if (LOCAL_LOGV) {
Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
}
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
View 类 中 draw()函数内部流程
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | 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
/**
* 绘制背景。变 量 dirtyOpaque表示dirty区是否是不透明,只有透明时才需要绘制背景。Android
* 中的视图几乎都是透明的,因为视图支持阿尔法通道,所以dirtyOpaque总 是为false,所以背景总是需
* 要绘制。如果View系统不支持阿尔法通道,那么则不需要绘制背景,因为视图本身会占满整个区域,
* 背景会完全被挡住。
*
*/
int saveCount;
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;
}
/**
* 绘制背景时,首先根据滚动值对canvas的坐标进行调整,然后再恢复坐标,为什么需要先调用translate()
* 平移Canvas的坐标呢?因为对每一个视图而言,Canvas的坐标原点(0 ,0 )对应的都是该视图的内部区域。
* 下图中外面是一个任意ViewGroup的实例,内部包含一个TextView对象,粗实线区域代表该TextView
* 在 ViewGroup中的位置, TextView中的文字由于滚动,一部分已经超出了粗实线区域,从而不可见。
* 此时,如果调用canvas.getClipBounds()返回的矩形区域是指粗实线所示的区域,该矩形的坐标是相对其
* 父视图ViewGroup的左上角,并且如果调用canvas的 getHeight()和 getWidth()方法将返回父视图的高度
* 和宽度,此处分别为200dip和 320dip。
* 如 果ViewGroup中包含多个子视图,那么每个子视图内部的onDraw()函数中参数canvas的大小都
* 是相同的,为父视图的大小。唯一不同的是“剪切区”,这个剪切区正是父视图分配给子视图的显示区
* 域 。
* canvas之所以被设计成这样正是为了 View树的绘制,对于任何一个View而言,绘制时都可以认
* 为原点坐标就是该View本身的原点坐标,从而 对 于View而言,当用户滚动屏幕时,应用程序只需要
* 调 用View类 的 scrollBy()函数即可,而不需要在onDraw()函数中做任何额外的处理,View的 onDraw()
* 函数内部可以完全忽略滚动值。
* 由于背景本身针对的是可视区域的背景,而 不 是 整 个 V iew 内部的背景,因此,本步中先调用
* translate()将原点移动到粗实线的左上角,从而使得背景Drawable对象内部绘制的是粗实线的区域。当
* 绘制完背景后,还需要重新调用transalte()将原点坐标再移回到TextView本 身 的 (0 ,0 )坐标。
*/
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
/**
* 如果该程序员要求显示视图的渐变框,则需要先为该操作做一点准备,但是大多数情况下都不
* 需要显示渐变框,因此,源码中针对这种情况进行快速处理,即略过该准备。
*/
// 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) {
/**
* 绘制视图本身,实 际 上 回 调onDraw()函数即可, View 的设计者可以在onDraw()函数中调用
* canvas的各种绘制函数进行绘制。
*/
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
/**
* 调 用 dispatchDmw()绘制子视图。如果该视图内部不包含子视图,则不需要重载该函数,而对
* 所有 的ViewGroup实例而言,都必须重载该函数,否则它也就不是ViewGroup 了
*/
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
/**
* 回调onDrawScrollbars()绘制滚动条。
*/
onDrawScrollBars(canvas);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
/**
* 下面继续分析需要绘制渐变框(Fading Edge)的情况,应用程序可以调用setVerticalFadingEdge()
* 和 setHorizontalFadingEdge()告诉系统绘制View对象的垂直方向渐变框及水平方向渐变框。
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
int paddingTop = mPaddingTop;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
paddingTop += getTopPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + paddingTop;
int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop;
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
/**
* 源码中处理渐变框的逻辑中,定义了以下相关变量
* mScrollCache:该变量的类型是ScrollabilityCache,该类中的作用是保存一个缓存对象,并且该
* 缓存内部定义了一个Matrix、一 个 Shader、一 个 Paint,这三个对象联合起来可以使用Paint绘
* 制 一 个Shader,并且可以在绘制时使用Matrix对 该 Shader进行缩放、平移、旋转、扭拉四种操
* 作,具体见Matrix的介绍。
*/
final ScrollabilityCache scrollabilityCache = mScrollCache;
/**
* length:对于垂直方向上的渐变框, length指的是该Shader的高度,对于水平方向,是 指 Shader
* 的宽度。
*/
int length = scrollabilityCache.fadingEdgeLength;
/**
* xxxFadeStrength: xxx 代表 left、 top、 right、 bottom, 该变量将作为后面 matrix.setScale()的参数,
* 表 面 意 思 是“渐变强度”,但从其效果来看应该是“渐变拉伸度”,因 为 Shader对象内部的原始
* 图像仅仅是一个像素宽,所以才调用matrix.SetSCale()对该像素的Shader进 行 缩 放 (拉伸),以产
* 生一个矩形。xxxFadeStrength的范围是0? 1,源码中使用该变量乘以fadeHeight或 者fadeLength。
* 理解了以上变量的含义后,剩下的过程就变得简单了,具体流程如下。
* 得到渐变框的length值 ,如 果 length的值大于视图本身的高度,则需要缩小length的值,否则
* 会出现上下渐变重影或者左右渐变重影,影响视觉效果。
*
*/
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
/**
* 回 调getXXXFadingEdgeStrengthO。该函数一般由View的设计者重载,如前对xxxFadeStrength
* 变量的解释,如果该函数返回为0,意味着不对Shader拉伸,那么也就不会绘制Shader 了。因此,本
* 步骤正是通过回调这些函数,从而决定都要绘制上下左右哪些渐变框,并用四个变量drawXXX表示,
* xxx 代表 Left、 Top、 Right、 Bottom。
*/
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength >= 0.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength >= 0.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength >= 0.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength >= 0.0f;
}
/**
* 获得Shader渐变的色调。这段代码内部有点差强,源码设计者的本意是想调用canvas.saveLayerO
* 对后面的绘制进行缓存,然而却仅仅设计成当颜色值为0 时才缓存。在笔者看来,如果要缓存,则无论
* 什么颜色都可以缓存,因此,那 段 canvas.saveLayer()实际上没有什么意义。在一般情况下,如果颜色不
* 为 0,则 调 用 setFadeColor()将该颜色设置到mScrollCache内 部 的 画 笔CPaint)中,应用程序可以重载
* View类 的 getSolideColor()用于设置渐变色。
*/
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
/**
* 绘制视图本身
*/
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
/**
* 调 用 dispatchDmwO绘制子视图。如果该视图内部不包含子视图,则不需要重载该函数,而对
* 所有 的ViewGroup实例而言,都必须重载该函数,否则它也就不是ViewGroup了
*/
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
/**
* 此时,才开始真正绘制渐变框,根据第二步中保存的变量drawXXX分别绘制不同的渐变框,
* 绘制中主要是对Matrix进行变换,然 后 调 用canvas.draw()进行绘制。读者可能觉得奇怪,这 个 Matrix
* 对 象 是 ScrollAbilityCache对 象 内 部 的 ,为 什 么 设 置 后 影 响 的 却 是 Canvas对 象 呢 ?原因就在于
* canvas.draw()中最后一个参数p,它是一个Paint对象,该对象正是来源于ScrollAbilityCache中的Paint,
* 而 该 Paint内部已经和该Matrix关联了。对 Matrix的变换包含以下四点。
// * ? matrix.setScale():该函数的作用正是把只有一个像素宽度的Shader缩放成一个真正的矩形渐
* 变 框 。
* ? matrix.postTranslate():对坐标进行平移。
* ? matrix.postRotate():对图形进行旋转
* ? fade.setLocalMatrix():该调用正是把该Matrix和 该 Shader关联起来。
*/
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
/**最后调用onScmllBar()绘制滚动条,
*/
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
}
源码中绘制上渐变框和下渐变框调用以上函数(matrix.setScale()、matrix.postTranslate()、matrix.postRotate()、fade.setLocalMatrix()) 时的执行效果如图1、图2所示,绘制左右 边框与此类似。
ViewGroup类中绘制子视图dispatchDrawO内部流程
/**
* dispatchDraw()的作用是绘制父视图中包含的子视图,该函数的本质作用是给不同的子视图分配合
* 适 的 画 布 (Canvas),至于子视图如何绘制,则又递归到View类 的 draw()函数中。应用程序一般不需要
* 重 载 dispatchDraw()函数,而只需要在onLayout()中为子视图分配合适的大小, dispatchDraw()将根据前
* 面分配的大小调整Canvas的内部剪切区,并作为绘制子视图的画布。所有的ViewGroup实例的内部绘
* 制基本上都是如此,这就是为什么具体的ViewGroup实例不需要重载dispatchDraw()的原因。
*/
@Override
protected void dispatchDraw(Canvas canvas) {
final int count = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
/**
* 判 断 mGroupFlags中是否设置FLAG—RUN—ANIMATION标识,该标识并不是该ViewGroup的
* “动画标识”,而是 该ViewGroup “布局动画标识”。动画标识指的是一个View自身的动画,而布局动
* 画只存在于ViewGroup对象中,指的是该ViewGroup在显示内部的子视图时,为内部子视图整体设置
* 的 动 画 。 典 型 的 例 子 就 是 , 应 用 程 序 可 以 在 X M L 文 件 中 的 LinearLayout标 签 中 设 置
* android:layoutAnimation属性,从而使 该LinearLayout的子视图在显示时出现逐行显示、随机显示、落
* 下等不同的动画效果,而这些效果正是在本步骤实现的。关于动画的详细过程见后面小节,本节只分析
* 没有动画的情况。
*/
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());
}
}
/**
* 处理padding属性。该属性是ViewGroup特有的,程序员只能给一个ViewGroup设 置padding,
* 而不能给一个View设 置padding。如 果ViewGroup包 含padding值 ,则 CLIP_PADDINT—MASK标识将
* 存在。对 于 View系统而言,当绘制到某个View时, View系统并不区分该View是一个具体的Veiw还
* 是一个ViewGroup实例,都会在View.draw()函数中调用dispatchDraw(canvas),参 数 Canvas的绘制区原
* 点坐标是该View内部区域的左上角, Canvas的剪切区仅仅是根据scroll值进行了剪切。由于padding
* 是 ViewGroup所特有的属性,因此ViewGroup的 dispatchDraw()需要对该属性进行自身的处理。
* 源码中首先调用canvas.save()保 存 当 前Canvas内部状态,然 后 调 用canvas.clipRect()进行剪切。在
* 执 行 dispatchDraw()函数前, Canvas的剪切区已经根据scroll值进行了剪切,剪切坐标的原点是View自
* 身的左上角,所以此处仅仅需要从左边加paddingLeft,从上边加paddingTop,从右边减paddingRight,
* 从下边减paddingBottom。
* 执行后,就会根据padding的值缩小剪切区。这里需要注意,缩小的仅仅是剪切区,也就是用户在
* 屏幕上看到的区域,而 ViewGmup本身的大小没有变化。本步骤执行前后的位置如图13-40和 图 13-41所示。
*/
int saveCount = 0;
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
saveCount = canvas.save();
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
/**
* 清 除 mPrivateFlags的 DRAW_ANIMATION标 识 ,因为接下来就会绘制视图了;同时清除
* mGroupFlags的 FLAG—INVALIDATED_REQUJRIED标 识 , 因 为 接 来 绘 制 后 就 意 味 着 已 经 满 足
* "RECURIED” 这个需求了。
*/
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
/**
* 使 用 for()循环,针 对 该ViewGroup的子视图逐个调用drawChild()函数。在一般情况下,绘制
* 子 视 图 的 顺 序 是 按 照 子 视 图 被 添 加 的 顺 序 逐 个 绘 制 , 但 应 用 程 序 可 以 重 载 ViewGmup的
* getChildDrawingOrder()函数,提供不同的顺序。关 于 drawChild()的内部过程见后面小节
*/
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);
}
}
}
// Draw any disappearing views that have animations
/**
* 绘 制 mDisappearingChildren列表中的子视图。这个变量需要着重解释一下,当 从 ViewGroup
* 中 removeView()时,指 定 的View对象会从mChildren变量中移除,因此,当进行消息派发时,被删除
* 的 View就绝不会获得用户消息。当被删除的View对象包含一个移除动画时,则 该 View会被添加到
* mDisappearingChildren列表中,从而使得在进行dispatchDraw()时,该 View依然会被绘制到屏幕上,直
* 到动画结束,在动画期间,用户虽然能够看到该视图,但却无法点击该视图,因为它已经从mChildren
* 列表中被删除,消息处理时会认为没有该View的存在。
*/
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
if (clipToPadding) {
canvas.restoreToCount(saveCount);
}
// mGroupFlags might have been updated by drawChild()
flags = mGroupFlags;
/**
* 6、 重新检查 mGroupFlags 中是否包含 FLAG_INVALIDATED_REQURIED 标识,因为 drawChild()
* 调用后,可能需要重绘该ViewGroup,如果需要,则调 用 invalidate()发起一个重绘请求。
*/
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate();
}
/**
* 7 、本步骤与第1 步是对称的,第1步中会先处理“布局动画”,而本步骤则处理布局动画是否完
* 成,如果完成,发 送 一 个 Handler消息。该 消 息 是 一 个Runnable对象,其 作 用 是 回 调ViewGroup中
* AnimationListener接 口 的onAnimationEnd()函数,通知应用程序布局动画完成了。
*/
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()过程
/**
* drawChild()的核心过程是为子视图分配合适的Canvas剪切区,剪切区的大小取决于child的布局大
* 小,剪切区的位置取决于child的内部滚动值及child内部的当前动画。
*/
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean more = false;
final int cl = child.mLeft;
final int ct = child.mTop;
final int cr = child.mRight;
final int cb = child.mBottom;
final int flags = mGroupFlags;
if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) {
if (mChildTransformation != null) {
mChildTransformation.clear();
}
mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION;
}
/**
* 1 、判断该子View是 否 存 在“变换矩阵”。所 谓 的 “变换矩阵” 是 指 该child在绘制时将通过一个
* 矩 阵 (Matrix)进行变换,变换的元素包括四点,分别为平移、旋转、缩放、扭曲,该矩阵就称之为变
* 换矩阵。存在变换矩阵意味着,该图像变换后会改变边框的大小。可以通过两种方式为视图设置变换矩
* 阵,一种是动画,另一种是静态回调。
* 所谓动画是指,应用程序可以调用View类 的 setAnimation()为 该View设置一个动画。动画的本质
* 是 对 View视图在指定的时间内进行某种变换(Transformation),变换包括图像平移、旋转、缩放、扭
* 曲及图像颜色阿尔法通道变化,然后将变换后的图像绘制到屏幕上,系统会在指定的时间内连续进行绘
* 制 ,并在不同时间得到不同的变换参数,从 而 使 其 看 起 来 就 像 是 一 个“ 动 画 ”。动画中又使用一个
* Transformation类来保存这些变换参数, Transaformation类是一个数据类,内部包含变换的相关参数,
* 变换按照类型分为四种:
* ? TYPEJDENTIFY: identify的 意 思 是“相同的”,即该变换实际上不会引起任何变换。
* ? TYPE_ALPAH:将引起图像颜色阿尔法通道变换。
* ? TYPE—MATRIX:将引起矩阵变换,矩阵变换包括平移、旋转、缩放、扭曲四种。
* ? TYPE_BOTH: both的含义是同时包含ALPHA和 MATRIX。
* 所 谓 的 静 态 回 调 是 指 , ViewGroup实 例 的 设 计 者 可 以 重 载 ViewGroup类 的
* getChildStaticTransformation()函数,从而为其包含的子视图指定一个静态的变换对象。
* 本步骤中包含两个重要局部变量。
* ? Transformation transformToApply:保存了子视图的变换对象,可来源于动画,也可静态指定。
* 源码中首先判断是否存在动画,如果存在动画,则 调 用Animation对 象 的 getTransformation()获
* 取变换对象;如果没有动画,则回 调getChildStaticTransformation()获取变换对象。
* ? boolean concatMatrix:该变量代表是否存在变换矩阵。对于动画而言,调 用 Animation对象的
* willChangeTransformationMatrix()判断是否存在变换矩阵;而 对 于“静态回调”,则直接判断变换
* 的类型,只有当变换类型是TYPE_MATRIX或 者 TYPE_BOTH时,该变量才为true。
*/
Transformation transformToApply = null;
final Animation a = child.getAnimation();
boolean concatMatrix = false;
if (a != null) {
if (mInvalidateRegion == null) {
mInvalidateRegion = new RectF();
}
final RectF region = mInvalidateRegion;
final boolean initialized = a.isInitialized();
if (!initialized) {
a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);
child.onAnimationStart();
}
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
more = a.getTransformation(drawingTime, mChildTransformation);
transformToApply = mChildTransformation;
concatMatrix = a.willChangeTransformationMatrix();
if (more) {
if (!a.willChangeBounds()) {
if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
FLAG_OPTIMIZE_INVALIDATE) {
mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
} else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
mPrivateFlags |= DRAW_ANIMATION;
invalidate(cl, ct, cr, cb);
}
} else {
a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
mPrivateFlags |= DRAW_ANIMATION;
final int left = cl + (int) region.left;
final int top = ct + (int) region.top;
invalidate(left, top, left + (int) region.width(), top + (int) region.height());
}
}
} else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
if (hasTransform) {
final int transformType = mChildTransformation.getTransformationType();
transformToApply = transformType != Transformation.TYPE_IDENTITY ?
mChildTransformation : null;
concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
}
}
// Sets the flag as early as possible to allow draw() implementations
// to call invalidate() successfully when doing animations
child.mPrivateFlags |= DRAWN;
/**
* 如果以上变换不会改变边框大小,即没有变换矩阵时,调 用Canvas对 象 的quickReject()函数快
* 速判断该子视图对应的剪切区是否超出了父视图的剪切区,超出意味着该子视图不能显示到屏幕上,所
* 以就不用绘制了,因为绘制了用户也看不见。 quickReject()调用时参数代表该子视图在父视图中的布局
* (layout)位置。注意本步成立的条件包含三个,这三个条件最终的意义是指,该 View 内部没有进行
* 动画,并且不存在“静态回调” 变换,并且剪切区不在父视图的剪切区中。该意义的反义是指,如果当
* 前正在进行动画或者存在静态回调变化,那么就算当前视图的剪切区不在父视图的剪切区中,都要进行
* 绘制操作。为什么呢?因为存在矩阵变换后,会引起子视图边框位置改变,而改变后的区域有可能又落
* 到了父视图的剪切区中,从 而 变 成“可看得见” 的。
*/
if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
(child.mPrivateFlags & DRAW_ANIMATION) == 0) {
return more;
}
/**
* 回 调child.computeScroll(),重新计算子视图的当前滚动值,因为子视图的滚动值在每次绘制之
* 后都有可能变化。应用程序一般会重载View对 象 的computeScroll()函数,比如对于ListView,当用户
* 手指在屏幕上滑动时,将导致屏幕做惯性滚轮运动,而在这个运动的过程中, computeScroll()函数会不
* 断改变该View的滚动值。
*/
child.computeScroll();
final int sx = child.mScrollX;
final int sy = child.mScrollY;
/**
* 上一步得到了子视图的滚动值,本步就要根据该滚动值设置子视图Canvas坐标的原点。对当
* 前 的 Canvas而言,其坐标原点是该ViewGroup布局区域的左上角①点,如 图 13-43所示,而本步正是
* 要将这个坐标原点移动到指定子视图的自身显示区域的左上角②点。
*
* 源码中,针对是否有cache的情况分别处理。在一般情况下,没 有 cache,所 以 translate()参数中水
* 平方向是先向右移动c l ,然后再向左移动sx,垂直方向类似;而对于有cache的情况,则忽略滚动值,
* 因为有cache时,视图本身需要处理滚动值,实际上如果有cahce,视图的滚动值都会设置为0,因此水
* 平方向仅平移cl,垂直方向仅平移ct。
*/
boolean scalingRequired = false;
Bitmap cache = null;
if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE ||
(flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) {
cache = child.getDrawingCache(true);
if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired;
}
final boolean hasNoCache = cache == null;
final int restoreTo = canvas.save();
if (hasNoCache) {
canvas.translate(cl - sx, ct - sy);
} else {
canvas.translate(cl, ct);
if (scalingRequired) {
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
float alpha = 1.0f;
/**
* 上面第一步得到了变换对象transformToApply,本步就要将该变换对象应用于子视图。首先判
* 断是否存在变换矩阵concatMatrix,并应用变换矩阵,然后再应用视图颜色阿尔法变换。
* 变换矩阵针对的是视图本身的整个区域,而不仅是在屏幕上的显示区域,但是变换矩阵中的数据却
* 是相对子视图可视区域的左上角,因此,在变换前需要先将坐标原点调整到视图本身的左上角,然后再
* 应用变换,最后再将原点调整回子视图本身的左上角,
*/
if (transformToApply != null) {
if (concatMatrix) {
int transX = 0;
int transY = 0;
if (hasNoCache) {
transX = -sx;
transY = -sy;
}
// 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);
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
alpha = transformToApply.getAlpha();
if (alpha < 1.0f) {
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
/**
* 针对视图颜色的阿尔法变换主要是调用canvas.saveLayoutAlpha()函数完成,在调用该函数前先回调
* 子视图的 child.onSetAlpha()。
*/
if (alpha < 1.0f && hasNoCache) {
final int multipliedAlpha = (int) (255 * alpha);
if (!child.onSetAlpha(multipliedAlpha)) {
canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
} else {
child.mPrivateFlags |= ALPHA_SET;
}
}
} else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
child.onSetAlpha(255);
}
/**
* 此 时 Canvas内部的变换参数已经确定,子视图的滚动值也确定了,因此也就可以确定子视图
* 中剪切区的位置了。同样,剪切区也分是否有cache的情况,因为如果有cache的话,系统将认为子视
* 图没有滚动值,滚动的处理完全由子视图内部控制,所以剪切区将忽略滚动值。
* scalingRequired变量是指是否进行了缩放。如果没有缩放,则边框没有变化。边框
* 的位置就等于该子视图的布局位置,即 child.mLeft、 child.mTop、 child.mRight及 child.mBottom,此时
* 滚动值一般都为0,所以实际的剪切区就等于子视图布局的大小。如果有缩放,比如缩放大于1,则子
* 视图的显示边框将大于布局边框,因此,剪切区的大小应该使用cache的大小,即 cache.getWidth()及
* cache.getHeight()。
* 该算法的结果实际上就是子视图的布局大小,只是考虑了滚动而已。
*/
if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (hasNoCache) {
/**而在一般情况下视图都没有cache,因此需要根据滚动的位置确定剪切区,
*
*/
canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));
} else {
if (!scalingRequired) {
canvas.clipRect(0, 0, cr - cl, cb - ct);
} else {
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
}
/**
* 剪切区设置好后,就可以调用子视图的dmw()函数进行具体的绘制了。同样,源码中分别针对
* 是 否 有cache的情况做了不同处理。
* 一般情况下没有cache,那么最简单的过程就是直接调用child.dmw()即可,只是在调用之前先判断
* 子视图的mPrivateFlags是否包含SKIP_DRAW标识,该标识告知View系统暂时跳过对该子视图的绘制。
* 如果需要跳过,贝U仅 调 用child.dispatchDmw(),即跳过视图本身的绘制,但要绘制视图可能包含的子视
* 图。
* 如 果 有cache,则只需要把视图对应的cache绘制到屏幕上即可,即调用canvas.drawBitmap()函数,
* 参数中包含缓冲区cache,它实际上是一个Bitmap对象
*/
if (hasNoCache) {
// Fast path for layouts with no backgrounds
if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
child.mPrivateFlags &= ~DIRTY_MASK;
child.dispatchDraw(canvas);
} else {
child.draw(canvas);
}
} else {
final Paint cachePaint = mCachePaint;
if (alpha < 1.0f) {
cachePaint.setAlpha((int) (alpha * 255));
mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE;
} else if ((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) {
cachePaint.setAlpha(255);
mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60003, hashCode());
}
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
}
/**
* 最后,绘制到此就结束,结束前,需要先 恢 复Canvas绘制前的状态,并且如果该绘制是一次
* 动画绘制,那么当该动画结束,则调 用 finishAnimatingView()通知应用程序该子视图动画绘制完成了。
*/
canvas.restoreToCount(restoreTo);
if (a != null && !more) {
child.onSetAlpha(255);
finishAnimatingView(child, a);
}
return more;
}
绘制滚动条
绘制滚动条是通过在View类 的 draw()函数中调用onDrawScrollBar()完成的。每个视图都可以有滚动条,请注意是“每个”,因为滚动条是视图中包含的基本元素,就像视图的背景一样。举个例子,一个 按 钮 Button)也可以有滚动条,读者可能觉得奇怪,按钮怎么会有滚动条呢?但的确是这样,而且让按钮显示滚动条是件非常容易的事情,因为按钮本身也是一个视图,只是从用户的角度来讲,按钮不需要显示这个滚动条而已。 滚动条包含垂直滚动条和水平滚动条,滚动条本身是一个Drawable对象,View类内部使用ScrollBarDrawable 类表示滚动条,View可以同时绘制水平滚动条和垂直滚动条。 在 ScrollBarDrawable类中,包含三个基本尺寸,分别是range、 offset、 extent。
-
range:代表该滚动条从头到尾滚动中所跨越的范围有多大。比如想用一个滚动条标识一万行代码,那 么 range可以设为10000。 -
offset:代表滚动条当前的偏移量。比如当前在看第600行代码,那 么 offset就 是 600。 -
extent:代表该滚动条在屏幕上的实际高度,比如200,单位是dip。 有了以上三个尺寸后,ScrollBarDrawable内部就可以计算出滚动条的高度及滚动条的位置。 除了以上尺寸外,ScrollBarDrawable类内部还包含两个Drawable对象,一个标识滚动条的背景,另一个标识滚动条自身。这两个Drawable对象分别是track和 thumble,水平方向和垂直方向分别有两个该Drawable对象。
protected final void onDrawScrollBars(Canvas canvas) {
// scrollbars are drawn only when the animation is running
/**
* onDrawScrollBarQ函数内部的执行流程如下。
* 1) 判断该View对象内部是否存在ScrollabilityCache对象,即变量mScrollCache是否为空。对应
* 用程序而言,当需要视图显示滚动条时,可以在XML文件中使用android:scrollbars属性为视图指定滚
* 动条,属性值可以是vertical、 horzontal、 none,从而在View的构造函数中将会调用initializeScrollbars()
* 创建一个ScrollablilityCache对象。如果View对象不是从XML文件中产生,而是通过程序动态产生,
* 则可以调用View类 的 setScrollbarFaddingEnable(boolean)函数设置滚动条是否“ 自动隐藏(fadding)”,该
* 函数内部则会调用initializeScrollbars()初 始 化 ScrollabilityCache对象。什么是“自动隐藏”?Android
* 中的滚动条和PC上的滚动条有所不同,对 于 PC上的滚动条而言,在一般情况下滚动条会一直存在,
* 在默认情况下滚动条并不显示,只有当用户做滚动操作时才会显示,滚动完毕后,滚动条就会自动藏起
* 来,这就叫自动隐藏。应用程序可以调用setScrollbarFaddingEnable()设置是否自动隐藏,在默认情况下
* 是自动隐藏。
*/
final ScrollabilityCache cache = mScrollCache;
if (cache != null) {
/**
* 2)如 果 cache存在,则继续判断cache的 状 态 (state)。 ScrollablilityCache内部有两种状态,分别
* 为 ON和 OFF,ON意味着滚动条处于显示状态,OFF意味着滚动条处于隐藏状态。因此,本步骤判断
* 如果是OFF状态的话,就直接返回。
* 那么,什么时候是ON,什么时候是OFF呢?如果滚动条是自动隐藏,那么,在
* setScrollbarFaddingEnable()函数中会将该cache的状态置为OFF,否则置为ON。如果是自动隐藏,那么
* 当应用程序调用scrollBy()函数时,该函数内部会间接调用awakenScrollBars(),该函数中会把cache的
* 状 态 “暂时置” 为 ON。为什么这里是暂时呢?因为该函数将cache状态置为ON后,紧接着会发送一
* 个异步延迟消息,在指定的延迟时间后,消息的处理函数又会重新将cache的状态置为OFF,从而使得
* 在下次绘制滚动条时会在本步骤中直接返回,这也就是“ 自动隐藏” 的具体过程。
* awakenScrollBars()函数的类型是protected,意味着该函数只能被重载,而不能被应用程序直接调用,
* 应用程序一般只能调用scrollBy()函数。而对于自定义View而言,比如ListView,其内部实际上并没有
* 滚动值,所以也就不能调用 scrollBy()函数,而它实现滚动条自动隐藏的效果正是借助于
* awakenScrollBars()函数。
*/
int state = cache.state;
if (state == ScrollabilityCache.OFF) {
return;
}
boolean invalidate = false;
/**
* 3 判 断 cache的状态是否为FADING。FADDING的本质上依然是ON,只 是 它 “正在隐藏”,
* 所 谓 “正在隐藏” 的效果一般就是滚动条“逐渐消失”,其实现方法是逐渐减少滚动条的阿尔法通道值。
* 如果不是FADDING,则将阿尔法值设为Oxff,也就是完全不透明。
*/
if (state == ScrollabilityCache.FADING) {
// We're fading -- get our fade interpolation
if (cache.interpolatorValues == null) {
cache.interpolatorValues = new float[1];
}
float[] values = cache.interpolatorValues;
// Stops the animation if we're done
if (cache.scrollBarInterpolator.timeToValues(values) ==
Interpolator.Result.FREEZE_END) {
cache.state = ScrollabilityCache.OFF;
} else {
cache.scrollBar.setAlpha(Math.round(values[0]));
}
// This will make the scroll bars inval themselves after
// drawing. We only want this when we're fading so that
// we prevent excessive redraws
invalidate = true;
} else {
// We're just on -- but we may have been fading before so
// reset alpha
cache.scrollBar.setAlpha(255);
}
final int viewFlags = mViewFlags;
/**
* 4 如果存在水平或者垂直滚动条,则逐个进行绘制。
* ( 1 ) 首先调用scrollBar.getSize()获得滚动条的大小。对于垂直滚动条而言,大小就是指track或者thumb的宽度;
* 水平方向是指track或者thumb的高度,只有当 size小于 0 时,才会使用XML中android:scrollbarSize 属性的值。
* ( 2 ) 绘制水平滚动条。
* ( 3 ) 绘制垂直滚动条。绘制时,首先回调computeVerticalRange()等三个函数,获得当前的range、
* offset及 extent值,然后将这三个值设置到scrollBar中。这就是为什么滚动条会“滚动” 的原因。自定
* 义视图的设计者应该重载这三个computeXXX()函数,并在函数实现中根据自定义的滚动情况返回相应
* 的值,从而使得View系统能够绘制出具有正确位置的滚动条。源码中有一段注释是关于RTL语言的,
* RTL是指从右向左阅读的语言。设置完这三个值后,回 调 onDrawVerticalScrollBar()函数,
*
* 即首先设置滚动条对应的剪切区,然后调用scrollBar的 draw()函数将其绘制到剪切区中。
*/
final boolean drawHorizontalScrollBar =
(viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
final boolean drawVerticalScrollBar =
(viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL
&& !isVerticalScrollBarHidden();
if (drawVerticalScrollBar || drawHorizontalScrollBar) {
final int width = mRight - mLeft;
final int height = mBottom - mTop;
final ScrollBarDrawable scrollBar = cache.scrollBar;
int size = scrollBar.getSize(false);
if (size <= 0) {
size = cache.scrollBarSize;
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
int left, top, right, bottom;
if (drawHorizontalScrollBar) {
scrollBar.setParameters(computeHorizontalScrollRange(),
computeHorizontalScrollOffset(),
computeHorizontalScrollExtent(), false);
final int verticalScrollBarGap = drawVerticalScrollBar ?
getVerticalScrollbarWidth() : 0;
top = scrollY + height - size - (mUserPaddingBottom & inside);
left = scrollX + (mPaddingLeft & inside);
right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap;
bottom = top + size;
onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom);
if (invalidate) {
invalidate(left, top, right, bottom);
}
}
/**
* 以上步骤已经完成了一次绘制,但如果当前滚动条正处于滚动状态,则需要继续调用invalidate()
* 发起一次重绘消息。而判断是否处于滚动状态的变量是invalidate,其值正是当cache状态为FADDING
* 时被赋值为true。调 用 invalidate()时,参数对应的矩形区仅仅是滚动条所在的区域。
* 至此,滚动条绘制就完成了。
*/
if (drawVerticalScrollBar) {
scrollBar.setParameters(computeVerticalScrollRange(),
computeVerticalScrollOffset(),
computeVerticalScrollExtent(), true);
// TODO: Deal with RTL languages to position scrollbar on left
left = scrollX + width - size - (mUserPaddingRight & inside);
top = scrollY + (mPaddingTop & inside);
right = left + size;
bottom = scrollY + height - (mUserPaddingBottom & inside);
onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom);
if (invalidate) {
invalidate(left, top, right, bottom);
}
}
}
}
}
动画的绘制
动画就是让“画” 动起来,其原理就像电影的胶片,通过不断在荧屏上绘制不同静态图像,从而达到动画的效果。GUI系统中动画的本质也是这样,在 View类 的 draw()函数中,会判断当前视图是否包含动画,如果包含,就根据动画的参数对当前View做一定的图形变换,比如缩放、平移等,然后将变换后的图像绘制到屏幕上。绘制完后,再发起一个重绘的消息,就这样连续绘制,直到动画参数指示动画结束。 从效果的角度看,View系统中包含的动画可以分为三类,分别是窗口动画、视图动画、布局动画。
指窗口对应的动画。窗口可以是一个Activity对应的窗口,也可以是一个对话框对应的窗口,还可以是应用程序调用WindowManager类的addView()函数添加的任意窗口。窗口动画一般定义了窗口在显示、消失时的动画。
指 View对象在显示及消失时对应的动画,它影响的是视图自身的动画效果。
指 ViewGroup对象包含的动画。该动画定义了 ViewGroup中子视图第一次显示时的动画,它影响的是ViewGroup中子视图的整体动画效果,其本质过程是根据布局动画为子视图分别设置不同的动画,从而使得整体上看来像是一个布局动画的效果。所以说,布局动画仅仅是一个概念。
动画的设计思路
对 于 View系统而言,所能提供的动画更是有限,Android中 View系统仅支持基本的五种动画,分别为平移、缩放、旋转、扭曲及颜色阿尔法通道变化,这 称之为“动画参数” 或 “动画类型”。应用程序也可以将这五种基本动画进行组合以产生新的动画,但也仅限于此。 View系统中动画的设计思路如下。 首先要有一个动画的主体,实际上就是一个View对象,然后可以为该View对象指定一个动画。动画使用一个Animation类来表示,当View要开始动画时,从从 Animation类中获取动画的参数,并根据这些参数对View进行图形变换, 然后将变换后的图形绘制到屏幕上。 Animation类中会保存动画的起始时间,并且在动画开始后,Animation会在不同的时间返回不同的动画参数,从而使得View在随后的时间中会变换出不同的图像。View系统会连续从Animation中取出动画参数,并将变换后的图像绘制到屏幕上,直到动画结束。而对用户来讲,这个过程感觉上就是View在变换,也 就 是 “动画”。 Animation类是一个abstract类型,它仅仅定义了动画和View类 的 API接口,至于要提供具体什么样的动画参数,则需要继承Animation类,并实现所定义的API。不同动画的设计者可以在定义的API中返回不同的动画参数,从而产生不同的动画效果。
除了 Animation类,与动画相关的还有一个重要类Interpolator。该类是一个interface,它的作用是什么呢?如上所述,假设给一个View指定了一个移动动画,动画的参数中包括起始位置X, 目标位置 Y ,起始时间t0 ,终止时间t 1,在默认情况下,动画开始后,会在t0到t1时间段上做匀速运动。再举一个例子,假设给一个View指定了一个旋转动画,动画的参数中包含起始角度A0,终止角度A1,起 始时间t0,终止时间t 1,在默认情况下,动画开始后,会 在 t0到 t 1 时间段上做匀速旋转。这两个例子 中,无论是哪种动画类型,从时间轴上来看都是匀速的,而从应用的角度来看,往往需要非匀速的变换, 而如何实现非勻速变换呢?这正是Interpolator类的作用,程序员可以实现Interpolator接口中定义的唯一一个函数getInterpolation() ,该函数的原型如下:
float getInterpolation(float input);
参 数 input代表的是时间轴上的t0到 t1, input的范围是0?1,即把t0到 t 1 时间段进行归夂化。比 如 当 t0为 2s,t 1 为 12s,当前时间为3s,input的值 就 是 ( t-t1) /t1-t0 = (3-2) /10,即 0.1。
至于 getlnterpolation() 的返回值应该是多少,则取决于该函数的实现者了。对于匀速变换而言,该函数直接返回input即可;而对于非匀速移动,比如加速运动,则返回值和input的关系如图13-46所示,时间点t 上对应的返回 值 等 于 (y—y0) / ( y1-y0 )。不同的加速曲线会产生不同的返回值,程序员甚至可以在不同的时间段使用不同的加速曲线,从而达到特别的动画效果,这实际上是一个数学模型。
另外,在动画设计中还包含一个重要数据类Transformation ,正如其名称所指,该数据类保存了 Animation中 的 “动画参数”。针对前面所讲的五种动画参数,Transformation 类分别用以下变量进行保存。
- Matrix mMatrix:该矩阵变量中保存了旋转、缩放、移动、扭曲相关的变换参数。
- float alpha:该变量保存了颜色阿尔法通道的值。
Animation类中至少需要重载的API有三个,如 表 13-8所示。 Animation的具体实现类名称一般为 XXXAnimation,其 中 X X X代 表 变 换 的 名 称 , 比如AlphaAnimation类可以实现Alpha变换,RotateAnimation可以实现旋转变换,等等。
ViewGroup类中drawChildO函数中视图动画绘制过程
视图动画是在ViewGroup 类 中 drawChild() 函数中完成的。drawChild() 函数首先会判断该视图是否包含 Animation对象,如果包含,则利用Animation中的动画参数对该视图进行变换,然后把变换后的图像绘制到canvas中。
应用程序可以使用res/anim/xxx.xml文件描述一个动画,对于视图动画而言,一般使用如下的xml标签描述一个动画。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<alpha />
<scale />
<translate />
<rotate />
</set>
< set >标签用于包含一组动画参数,具体的动画参数分为四种,分别如下。 ? < scale >标签:表示缩放动画,标签中可以使用fromXScale等属性指定缩放的相关值。 ? < alpha >标签:表示颜色阿尔法通道的变换。 ? < rotate >标签:表示旋转变换。 ? < translate >标签:表示平移变换。
描述好了动画后,程序中可以调用AnimationUtils.loadAnimation() 函数从这个XM L文件中产生一 个 Animation 对象,然后可以调用View类 的 setAnimation() 把这个动画设置给该视图。以后,当需要该视图开始指定的动画时,就可以调用View类的 startAnimation() 。startAnimation() 函数内部实际上只做了一件事情,即调用 invalidate() ,这就发起了一次重绘请求,剩下的过程就是在ViewGroup 类中的drawChild() 函数中完成的。
drawChild()中和动画相关的代码从第1496行 的 if( a != null)开始到第1552行 的 if条件结束,其具体 流程如下。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean more = false;
final int cl = child.mLeft;
final int ct = child.mTop;
final int cr = child.mRight;
final int cb = child.mBottom;
final int flags = mGroupFlags;
if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) {
if (mChildTransformation != null) {
mChildTransformation.clear();
}
mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION;
}
Transformation transformToApply = null;
final Animation a = child.getAnimation();
boolean concatMatrix = false;
if (a != null) {
if (mInvalidateRegion == null) {
mInvalidateRegion = new RectF();
}
// (1) 创建一个矩形区region,该矩形区将保存视图经过动画变换后的大小。
final RectF region = mInvalidateRegion;
/*
*(2) 查看动画对象是否已经初始化。这里所谓的“初始化” 的本质是,在 Animation类内部有一个boolean类型的mlnitialized变量,
* 当调用Animation的 reset()方法后,该变量将被重新置为false,而是否初始化实际上是指该变量是否为true。
* 如果还没有初始化,则:
* - 先 调 用 a.initialize()初始化该动画对象,初始化的参数中包含了子视图的宽度、高度,以及父视图的宽度和高度
* - 接着调用a.initializeInvalidateRegion()设置初始无效区,初始值即为子视图的大小。
* - 接着调用child.onAnimationStarted()通 知 应 用 程 序 “视图动画就要开始了 ”
*/
final boolean initialized = a.isInitialized();
if (!initialized) {
a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);
child.onAnimationStart();
}
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
// (3) 如前所述,Animation类内部使用一个Transformation对象来保存动画参数,因此本步调用 a.getTransformation()获得该对象,
并赋值给局部变量transformToApply,变量的语义是“将要应用给子视图的变换”。注意,该调用是通过参数传递的,而并非通过返回值。
getTransformation()的返回值仅仅是一个boolean类型,指明该动画是否结束,返 回 true代表着还没有结束,没有结束的话,执行完本次 绘制后,还需要再调用invalidate()发起一个绘制消息。
more = a.getTransformation(drawingTime, mChildTransformation);
transformToApply = mChildTransformation;
/**
* 判断本次变换是否会改变变换中Matrix的值,并赋值给局部变量concatMatrix。什么变换会改变 Matrix呢?平移、缩放、旋转、扭曲等
* 变换都会改变Matrix的值。由于这些变换针对的是子视图中 整个区域,包括在屏幕上看得见的以及由于剪切区限制而看不见的,因
* 此,在后面的对子视图的变换中 将根据是否改变Matrix而做不同的处理。
*/
concatMatrix = a.willChangeTransformationMatrix();
if (more) {
if (!a.willChangeBounds()) {
if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
FLAG_OPTIMIZE_INVALIDATE) {
mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
} else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
mPrivateFlags |= DRAW_ANIMATION;
invalidate(cl, ct, cr, cb);
}
} else {
a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
mPrivateFlags |= DRAW_ANIMATION;
final int left = cl + (int) region.left;
final int top = ct + (int) region.top;
// invalidate()函数的参数代表了需要重绘的区域,如果动画变换不改变子视图边框大小的话,这个区域就是子视图在父视图中的大
//小,而如果动画变换超出了子视图本来的大小,则重绘的区域就需要扩大。
//源码中先调用 a.willChangeBounds()判断是否本次变换会改变边框大小,然后再调用a.getInvalidateRegion()计算出本次动画变换后
//的区域region。
//该 region相对的坐标原点是子视图的左上 角,因此,当变换后的区域增加时,region的 left就是一个负值。得 到 region后,就可以根据region的值重新设定需要重绘的区域。
invalidate(left, top, left + (int) region.width(), top + (int) region.height());
}
}
}
// (5)如 果 第 3 步调用getTransformation()返 回 false,则意味着动画己经结束了,于是就不需要再调
用 invalidated发起重绘请求,否则,就要发起重绘请求。
//如果FLAG_SUPPORT_STATIC_TRANSFORMATIONS == 1,调用getChildStaticTransformation()方法检查子视图是否被设置一个
//变换矩阵,如果设置了,即hasTransform == true,则mChildTransformation就是子视图需要的变换矩阵
else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
if (hasTransform) {
final int transformType = mChildTransformation.getTransformationType();
transformToApply = transformType != Transformation.TYPE_IDENTITY ?
mChildTransformation : null;
concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
}
}
//设置mPrivateFlags的DRAWN标志位为1,标明它要开始绘制了。
// Sets the flag as early as possible to allow draw() implementations
// to call invalidate() successfully when doing animations
child.mPrivateFlags |= DRAWN;
if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
(child.mPrivateFlags & DRAW_ANIMATION) == 0) {
return more;
}
//调用computeScroll()计算子视图的滑动位置
child.computeScroll();
final int sx = child.mScrollX;
final int sy = child.mScrollY;
boolean scalingRequired = false;
Bitmap cache = null;
//如果FLAG_CHILDREN_DRAWN_WITH_CACHE或者FLAG_CHILDREN_DRAWN_WITH_CACHE为1,则表示它采用缓冲的方式进行
//绘制,它将自己的UI缓冲在一个Bitmap里,可以调用getDrawingCache()方法来获得这个Bitmap。
if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE ||
(flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) {
cache = child.getDrawingCache(true);
if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired;
}
final boolean hasNoCache = cache == null;
//设置子视图child的偏移、Alpha通道以及裁剪区域
final int restoreTo = canvas.save();
if (hasNoCache) {
canvas.translate(cl - sx, ct - sy);
} else {
canvas.translate(cl, ct);
if (scalingRequired) {
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
float alpha = 1.0f;
//至此,动画的参数设置就结束了,drawChild()函数内部之后就要使用这些动画参数对canvas进行一定的变换,即判断transformToApply是否为空。
//如果不为空,则用该对象中的Matrix对 canvas进行变换,如以下代码所示:
if (transformToApply != null) {
if (concatMatrix) {
int transX = 0;
int transY = 0;
if (hasNoCache) {
transX = -sx;
transY = -sy;
}
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
// 这段代码首先将canvas的坐标原点移动到当前显示区,因为变换仅针对子视图的显示区,然后调用 concat()进行变换,变换后再将坐标原点重新移动到滚动后的状态。
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
alpha = transformToApply.getAlpha();
if (alpha < 1.0f) {
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
if (alpha < 1.0f && hasNoCache) {
final int multipliedAlpha = (int) (255 * alpha);
if (!child.onSetAlpha(multipliedAlpha)) {
canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
} else {
child.mPrivateFlags |= ALPHA_SET;
}
}
} else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
child.onSetAlpha(255);
}
//如果FLAG_CLIP_CHILDREN == 1,则需要设置子视图的裁剪区域
// 进行动画参数变换后,接下来就需要设置子视图的剪切区
/**
* 这段代码初看起来有点让人费解,大家都知道,当某个子视图进行放大动画时,该视图在屏幕上的显示区域会大于原来的区域,而在以
* 上代码中,canvas.clipRect()参数中指定的大小却始终是子视图的原始大小,这岂不怪哉?事实上,读者需要注意,clipRect()最终剪切
* 的区域取决于两点,第一点是clipRect()参数中指定的矩形区域,而第二点却是当前 canvas内部的 Matrix值 。这就是为什么前面使用
* canvas.concat()的原因,concat()函数能够用指定的Matrix和 canvas原有的Matrix进行矩阵相乘,所得Matrix中的scale属性将用于最终决
* 定剪切区的大小。比如,以下代码clipRect()指定的矩形高度为20, 宽度为100,而在调用clipRect()前却设置了 canvas中的scale属 性
* 为 (1,2),因此最终获得的剪切区矩形高度将为40,宽度为100。
* Matrix m = new Matrix();
* m.setScale(1,2);
* canvas.concat(m);
* canvas.clipRect(20,0,120,20);
/
if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (hasNoCache) {
canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));
} else {
if (!scalingRequired) {
canvas.clipRect(0, 0, cr - cl, cb - ct);
} else {
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
}
// 计算出剪切区后,剩下的具体绘制就和绘制普通视图完全相同了。
//绘制子视图的UI
if (hasNoCache) {
// Fast path for layouts with no backgrounds
if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
child.mPrivateFlags &= ~DIRTY_MASK;
child.dispatchDraw(canvas);
} else {
child.draw(canvas);
}
} else {
final Paint cachePaint = mCachePaint;
if (alpha < 1.0f) {
cachePaint.setAlpha((int) (alpha * 255));
mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE;
} else if ((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) {
cachePaint.setAlpha(255);
mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60003, hashCode());
}
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
}
//恢复画布的堆栈状态,以便在绘制完当前子视图的UI后,可以继续绘制其他子视图的UI
canvas.restoreToCount(restoreTo);
if (a != null && !more) {
child.onSetAlpha(255);
finishAnimatingView(child, a);
}
return more;
}
}
动画执行过程中的消息处理逻辑有以下特点。 第一,动画被变换后尽管显示的大小发生了变换,但其在消息处理过程中所占据的窗口大小却并没有改变,因此,在动画的运行过程中,该视图依然会获取用户消息。比如当对一个Button对象进行缩放动画时,在动画的过程中,用户依然可以点击该Button,但是点击的区域并不是看到的区域,而是Button在父视图中的布局区域,一般为动画前或动画后看到的Button所占的区域,如 图 13-47所示。
第二,由于动画过程是逐个绘制子视图,因此,后面子视图的动画绘制区域可能覆盖或部分覆盖前面子视图动画绘制区域,如 图 13-48所示。
ViewGroup中 dispatchDraw()中布局动画绘制流程
布 局 动 画 是 在 ViewGroup类 中 dispatchDraw()函 数 中 完 成 的 。 dispatchDraw( ) 函 数 首 先 会 获 得 该ViewGroup中 包 含 的 布 局 动 画 参 数 ,然 后 根 据 该 参 数 逐 个 设 置 子 视 图 的 动 画 参 数 ,最 后 调 用 drawChild()函 数 将 子 视 图 绘 制 到 屏 幕 上 。 与 视 图 动 画 相 似 , 应 用 程 序 也 可 以 使 用 res/anim/xxx.xml文 件 描 述 一 个 布 局 动 画 , 所 不 同 的 是 布 局动 画 所 使 用 的 标 签 及 属 性 与 视 图 动 画 不 同 , 如 以 下 代 码 所 示 :
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="50%"
android:animationOrder="normal"
android:animation="@anim/item_anim"/>
在以上代码中 , layoutAnimation标签 是 Framework内 部 定 义 的 , 所 有 的 ViewGroup实例都识别该标签 。android:animation 属 性 指 定 了 具 体 的 动 画 文 件 , 该 动 画 对 应 的 是 视 图 动 画 , 它 将 应 用 到 每 一 个 子视 图 中。 android: delay属 性 指 定 每 一 个 视 图 动 画 的 时 间 间 隔 , 比 如 当 animation指 定 的 动 画 持 续 时 间 为 3 秒 时, 那 么 delay为 50%的 意 思 就 是 每 当 上 一 个 动 画 开 始 1.5秒 后 , 下 一 个 子 视 图 就 开 始 动 画 。
下 面 来 分 析 以 上 XM L 文 件 是 如 何 被 读 取 的 , 以 及 ViewGroup中 的 dispatchDraw()如 何 使 用 这 些 参数 。
首 先 , 假 设 上 面 这 段 代 码 对 应 的 XM L 文 件 名 称 为 anim_tab.xml, 那 么 , 应 用 程 序 可 以 在 layout文件中的具 体 ViewGroup实 例 的 标 签 中 使 用 android: layoutAnimation设 置 该 ViewGroup实 例 的 布 局 动 画 文件 , 如 以 下 代 码 所 示 :
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/anim_tab">
</ListView>
源 码 中 , ViewGroup类 的 构 造 函 数 将 负 责 解 析 layoutAnimation属 性 , 如 以 下 代 码 所 示 :
case R.styleable.ViewGroup_layoutAnimation:
int id = a.getResourceId(attr, -1);
if (id > 0) {
setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, id));
}
break;
该段代码中,当遇到layoutAnimation属性时,首先调用a.getResourceId()获得对应的动画文件,本 例 中 动 画 文 件 就 是 anim_tab.xml 。 然 后 调 用 AnimationUitls类 的 静 态 功 能 函 数 loadLayoutAnimation() 装 载 该 anim_tab.xml 文 件 , 该 函 数 将 返 回 一 个LayoutAnimationController 对 象 , 接 着 再 调 用 setLayoutAnimation() 函 数 将 该 Controller对 象 赋 值 给 viewGroup 中 的 全 局 变 量 mLayout AnimationController 。 以 上 就 是 ViewGroup类 构 造 函 数 中 为 布 局 动 画 所 做 的 准 备 , 其 关 键 是 要 产 生 setLayoutAnimaion()函 数 设 置 Controller对 象 , XM L 文 件 的 作 用 仅 仅 是 用 它 产 生 这 个 Controller对 象 。接 下 来 , 就要 在 dispatchDraw() 函 数中 使 用 这 个 Controller对 象 了 , 具 体 从 处 理 以 下 代 码 处 开 始 :
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()){
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
}
/*
* (1)判 断 mGroupFlags中 是 否 包 含 FLAG_RUN_ANIMATION标 识 。 注 意 , 该 标 识 并 不 是 视 图 动 画标 识 , 而 是 ViewGroup
* 中 布 局 动 画 的 标 识 , 它 是 在 ViewGroup类 中 定 乂 的 。 如 果 存 在 该 标 识 , 说 明 需要 启 动 布 局 动 画 , 布 局 动 画 只 需
* 要 启 动 一 次 。 该 标 识 是 在 setLayoutAnimation()函 数 中 被 置 位 , 本 次 dispatchDraw()函 数 执 行 完 毕 后 , 会清除该标识 。
*/
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
/**
* (2)判 断 mGroupFlags中 是 否 包 含 FLAG_ANIMATION_CACHE标 识 ,在 默 认 情 况 下 ,mGroupFlags中 都 会 包 含 该标 识 。该标
* 识 的 意 义 是 说 , 在 布 局 动 画 期 间 , 为 该 ViewGroup中 的 每 一 个 子 视 图 创 建 一 个 显 示 缓 冲 区 ,这样的好处是提高绘图效率 。
*/
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
/**
* (3)使 用 for循 环 , 为 每 一 个 子 视 图 设 置 视 图 动 画 , 注 意 , 是 视 图 动 画 。 因 为 布 局 动 画 最 终 都 是 通 过 视 图 动 画 绘 制
* 到 屏 幕 上 的 , 这 就 是 为 什 么 layoutAnimation标 签 中 都 包 含 一 个 android: animation属 性的 原 因 , 该 属 性 值 必 须 对 应 一
* 个 具 体 的 视 图 动 画 。
*/
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
/**
* 调 用 attachLayoutAnimationParams()设 置 子 视 图 的 LayoutParams 属 性 中 的 LayoutAnimationParams变 量 , 该 变 量 默 认
* 为 空 , 其 类 型 是 LayoutAnimationController.AnimationParameters类 。 该 类 中 有 两 个 成员 变 量 , 分 别 是 count和 index,
* count代 表该 布 局 动 画 中 一 共 有 多 少 个 兄 弟 视 图 , index代 表 当 前 子 视图 在 父 视 图 中 的 序 号 。
*/
attachLayoutAnimationParameters(child, params, i, count);
/**
* 调 用 bindLayoutAnimation()设 置 具 体 的 子 视 图 动 画 。 该 函 数 中 , 调 用 布 局 动 画 控 制 器mLayout AnimationController
* 的 getAnimationForView ( child)获 得 指 定 子 视 图 的 动 画 , 该 函 数 的 内 部 原理 如 下 。
*/
bindLayoutAnimation(child);
// 如 果 需 要 使 用 绘 制 缓 存 , 贝U调 用 child.buildDrawingCache(tme)为 子 视 图 创 建 一 个 缓 存 。
if (cache) {
child.setDrawingCacheEnabled(true);
child.buildDrawingCache(true);
}
}
}
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
/**
* 以 上 就 完 成 了 将 布 局 动 画 转 换 为 具 体 的 子 视 图 所 需 的 视 图 动 画 , 由 于 以 上 过 程 可 能 会 花 费 一 点 时 间 , 因 此 ,
* 在 设 置 好 视 图 动 画 后 , 默 认 是 并 不 启 动 动 画 中 的 计 时 , 而 此 时 完 成 了 以 上 转 换 工 作 , 因 此 , 可 以 调 用
* controller.start()开 始 动 画 了 。
*/
controller.start();
/**
* 清 除 mGroupFlags 中 的 FLAG_RUN_ANIMATION 标 识 及 FLAG_ANIMATION_DONE 标 识 。
*前 者 代 表 是 否 需 要 启 动 布 局 动 画 , 由 于 以 上 几 步 已 经 完 成 了 布 局 动 画 的 设 置 , 因 此 ,接 下 来 就 不 需 要 了 , 所 *
*以 要 清 除 该 标 识 。 换 句 话 说 , 在 一 般 情 况 下 , 当 一 个 ViewGroup被 绘 制 后 , 布 局 动 画 只 出 现 一 次 , 除 非 应 用 程
*序 再 次 调 用 setLayoutAnimation()或 者 再 次 调 用 startLayoutAnimation()。
*FLAG_ANIMATION_DONE代 表 布 局 动 画 是 否 完 成 , 由 于 动 画 才 刚 刚 开 始 , 肯 定 还 没 有 完 成 , 所以需要清除该标识 。
*/
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (cache) {
mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
}
/**
* 回 调 mAnimationListener.onAnimationStartO函 数 。 应 用 程 序 可 以 调 用 setLayoutAnimationListener()设置 一 个 监 听 器 , 从 而
* 当 布 局 动 画 开 始 时 能 够 有 机 会 执 行 其 他 操 作 。 与 此 类 似 的 还 有 当 布 局 动 画 结 束 后 , 会 回 调 监 听 者 的
* onAnimationEnd()。
*/
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
前 面 讲 过 , Controller是 根 据 布 局 动 画 的 XM L 文 件 产 生 的 , 该 文 件 中 包 含 两 个 重 要 属 性 , 一 个 是 delay,另 一 个 是 Animation。 delay表 示 子 视 图 动 画 之 间 的 延 迟 , Animation标 识 应 该 应 用 给 每 个 子 视 图 的 具 体 动 画 。 因 为 在 第 (1) 步 中 已 经 设 置 了 每 一 个 子 视 图 中 AnimationParams对 象 的 count和 index值 , 并 且 可 以 知 道 animation对 应 的 动 画 持 续 时 间 ,所 以 理 论 上 就 可 以 计 算 出 每 个 子 视 图 动 画 的 开 始 时 间 。Controller对 象的 getAnimationForView(child)的 核 心 作 用 就 是 计 算 每 个 子 视 图 动 画 的 开 始 时 间 ,获 得 时 间 后 ,再 使 用 Animation的 clone()函 数 克 隆 一 个 Animation对 象 ,因 为 每 个 子 视 图 都 必 须 对 应 一 个 独 立 的 Animation对 象 。 最 后 再 将 计 算 出 的 起 始 时 间 赋 值 到 新 的 Animation对 象 , 并 把 该 Animation赋 值 给 子 视 图 即 可 。
private void bindLayoutAnimation(View child) {
Animation a = mLayoutAnimationController.getAnimationForView(child);
child.setAnimation(a);
}
public final Animation getAnimationForView(View view) {
final long delay = getDelayForView(view) + mAnimation.getStartOffset();
mMaxDelay = Math.max(mMaxDelay, delay);
try {
final Animation animation = mAnimation.clone();
animation.setStartOffset(delay);
return animation;
} catch (CloneNotSupportedException e) {
return null;
}
}
对 于 应 用 程 序 而 言 , 如 果 需 要 更 为 特 别 的 布 局 动 画 , 则 需 编 写 自 定 义 的 LayoutAnimationController对 象 , 并 且 新 的 Controller必 须 继 承 于 LayoutAnimationController类 , 其 内 部 逻 辑 就 是 要 根 据 你 想 要 的 布 局 动 画 效 果 设 计 合 适 的 子 视 图 动 画 起 始 时 间 、 起 始 位 置 等 , 最 后 再 调 用 ViewGroup类 的 setLayoutAnimation()将 新 的 Controller 设 置 给 该 ViewGroup 对 象 。
比 如 ,除 了 LayoutAnimation 标 签 外 ,Framework 中 还 为 GridVeiw 定 义 了 特 别 的 gridLayoutAnimation标 签 , 在 该 标 签 中 , 应 用 程 序 可 以 使 用 以 下 属 性 设 置 特 别 的 效 果 。
- rowDelay: 设 置 每 行 的 延 迟 。
- columnDelay: 设 置 每 列 的 延 迟 。
- directionPriority: 其 值 可 以 是 “row” 或 者 “column”。
- direction: 设 置 动 画 的 方 向 , 其 值 可 以 是 right_to_left, top_to_bottom等 。
|