自定义基础之Draw过程
时间:2022-09-27 05:30:01
绘制过程主要是把手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
显示器而言,像素密度高, * 所以相同像素的实际尺寸会变小,于是引入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 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