Android View的绘制流程
源码版本为 Android 10(Api 29),不同Android版本可能有一些差别
View 的绘制从哪里开始
在《Activity常见问题》的 Activity 在 onResume 之后才显示的原因是什么? 部分中我们知道了View是在 onResume()
回调之后才显示出来的,显示过程主要是通过 WindowManagerImpl#addView()
-> WindowManagerGlobal#addView()
-> ViewRootImpl#setView()
这个过程,我们再次看一下 ViewRootImpl#setView()
的代码(核心代码):
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
// 调用 requestLayout() 方法,进行布局(包括measue、layout、draw)
requestLayout();
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
// 通过调用 Session 的 addToDisplay() 方法
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
}
}
}
有这样一行 requestLayout()
,表示请求布局,我们界面的绘制也是从这一行代码开始的,接下来,我们就来看一下跟踪一下这段代码(ViewRootImpl#requestLayout()
):
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
调用 ViewRootImpl#scheduleTraversals()
方法:
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
在 scheduleTraversals()
方法中通过 mChoreographer.postCallback()
方法发送一个要执行的实现了 Runnable
的 TraversalRunnable
的对象 mTraversalRunnable
:
-
mChoreographer.postCallback()
方法内部就是通过Handler
机制 -
TraversalRunnable
为ViewRootImpl
的内部类final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
调用 ViewRootImpl#doTraversal()
方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
调用 ViewRootImpl#performTraversals()
方法,该方法中关于绘制的代码
private void performTraversals(){
……
// 方法测量组件的大小
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
……
// 方法用于子组件的定位(放在窗口的什么地方)
performLayout(lp,desiredWindowWidth,desiredWindowHeight);
……
// 绘制组件内容
performDraw();
……
}
而在performMeasure()
、performLayout()
和performDraw()
方法的调用过程可以用下面的图来表示:
从图中可以看出系统的View类已经写好了measure()
、layout()
和draw()
方法:
- 在系统View类中
measure()
方法用了final
修饰,不能被重写(我觉得这应该是google不想让开发者更改measure()
方法里面的逻辑而设计的,但是开发者有时又有需求需要自己测量,所以提供了onMeasure()
方法可以重写);
代码 - 在系统View类中的
layout()
方法在调用onLayout()
方法前调用了setFrame()
方法,这个方法作用是判断View的位置是否发生改变,如果没有发生改变,就不调用onLayout()
方法,主要是为了提高性能,在View类中onLayout()
方法是空实现,这是因为view没有子类,而当在自定义的控件如果是直接继承ViewGroup时就必须重写onLayout()
方法;
代码 - 在系统View类中的
draw()
方法,开发者一般不会重写,因为当我们如果重写draw()
时,就需要按照系统定义好的步骤一步一步的画,否则会显示不出来,相对来说比较麻烦。而如果我们实现onDraw()
方法,我们只要关注我们画的内容即可(画出来的内容就是显示到界面的内容);
代码 - 当开发者在自定义控件时一般只需重写
onMeasure()
、onLayout()
和onDraw()
方法就可以了。
测量 -- measure
源码追踪
ViewRootImpl
中的performMeasure()
方法:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
调用了mView
的measure()
方法,mView
就是 DecorView
,是通过 ViewRootImpl#setView()
传入进来的,也就是调用了FrameLayout#measure()
方法,FrameLayout
继承ViewGroup
,ViewGroup
没有重写也不能重写measure()
方法,所以最终调用的是View
类中的measure()
方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
}
在View类中的measure()
中系统帮我们做了很多的处理并且不想让开发者重写measure的逻辑,所以使用了final
修饰符进行修饰,并且调用了onMeasure()
方法,所以在Activity
中View树的测量过程中,最终是从FrameLayout#onMeasure()
方法开始的,FrameLayout
的onMeasure()
方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
......
// 判断孩子控件的Visibility属性,如果为gone时,就跳过,因为gone属性不占用空间
if (count > 1) {// 判断是否有孩子控件
for (int i = 0; i < count; i++) {
// 通过LayoutParams参数获取孩子控件的margin、padding值
...
// 调用孩子控件的measure()方法测量自身大小
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
在FrameLayout
的onMeasure()
方法中,先是获取了孩子控件的个数,然后获取每一个孩子控件并判断visibility
属性;最终调用孩子View#measure()
方法测量自身大小。
当开发者有需要重新测量控件时,只需要重写onMeasure()
方法即可,系统在View的measure()
方法中会回调onMeasure()
方法,使测量值生效。
下面是继承自View时重写的onMeasure()
方法,我就只是简单的将宽和高都设置成500:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(500,500);
}
下面是继承ViewGroup
时重写的onMeasure()
方法,将所有孩子控件的宽和高的和计算出来作为父控件的宽和高,最终调用setMeasuredDimension()
方法设置值:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMeasure = 0,heightMeasure = 0;
// 直接调用系统方法测量每一个孩子控件的宽和高
measureChildren(widthMeasureSpec,heightMeasureSpec);
/**
* 系统在调用measureChildren(widthMeasureSpec,heightMeasureSpec)的过程中,
* 如果孩子控件依然是ViewGroup类型的,那么又会调用measureChildren()方法,否则会调用
* child.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法测量每一个孩子控件
* 的宽和高,直到所有的孩子控件都测量完成。
* 这就可以说明measure的过程可以看成是一个递归的过程。
*/
// 获取孩子控件的个数
int childCount = getChildCount();
// 循环测量每一个孩子控件的宽和高,得到的和就作为控件的宽和高
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
// 获取每一个孩子的宽和高
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
// 把每一个孩子控件的宽和高加上
widthMeasure += width;
heightMeasure += height;
}
// 调用setMeasuredDimension()方法保存宽和高,表示measure的结束
setMeasuredDimension(widthMeasure,heightMeasure);
}
另外在measure的过程中还可能会用到MeasureSpec类(View的内部类):
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 父控件不没有对子施加任何约束,子可以是任意大小(也就是未指定)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 父控件决定子的确切大小,表示width和height属性设置成match_parent或具体值
public static final int EXACTLY = 1 << MODE_SHIFT;
// 子最大可以达到的指定大小,当设置为wrap_content时,模式为AT_MOST
public static final int AT_MOST = 2 << MODE_SHIFT;
/*
* 通过模式和大小创建一个测量规范
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/*
* 获取模式
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/*
* 获取大小
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
}
MeasureSpecs
使用了二进制去减少对象的分配,用最高的两位数来表示模式(Mode),剩下的30位表示大小(size)。
Mode有三种:UNSPECIFIED
(未指定,没有约束,可以任意大小)、EXACTLY
(精确,表示match_parent或者具体的大小值)、AT_MOST
(最大值,表示wrap_content)
measure总结:
-
View的
measure()
方法被final
修饰,子类不可以重写,但可以通过重写onMeasure()
方法来测量大小,当然也可以不重写onMeasure()
方法使用系统默认测量大小; -
如果想要让自己设置的值生效,就必须调用
setMeasuredDimension()
方法设置宽和高; -
如果在
Activity
的onCreate()
方法或onResume(
)方法里面直接调用getWidth()/getHeight()
、getMeasureWidth()/getMeasureHeight()
获取控件的大小得到的结果很可能是0,因为在onCreate()
或onResume()
的时候系统还没有调用measure()
方法(getMeasureWidth()
和getMeasureHeight()
的赋值在View的setMeasuredDimension()
方法中,所以在调用完View的setMeasuredDimension()
方法之后getMeasuredWidth()
和getMeasuredHeight()
就已经有值了。而getWidth()
和getHeight()
要在onLayout()
方法完成之后才会被赋值),如果一定要在onCreate()
方法或onResume()
方法里面获取控件的大小,可以通过以下方法得到:view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int width = view.getWidth(); int height = view.getHeight(); view.getViewTreeObserver().removeOnGlobalLayoutListener(this); } });
-
通过
setMeasuredDimension()
方法设置的值并不一定就是控件的最终大小,组件真正的大小最终是由setFrame()
方法决定的,该方法一般情况下会参考measure出来的尺寸值; -
子视图View的大小是由父容器View和子视图View布局共同决定的;
-
如果控件是
ViewGroup
的子类,那就必须测量每一个孩子控件的大小,可以调用系统的measureChildren()
方法测量,也可以自己测量; -
Android系统对控件的测量过程可以看做是一个递归的过程。
摆放 -- layout
源码追踪
ViewRootImpl
中的performLayout()
方法:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during layout: running second layout pass");
view.requestLayout();
}
}
代码中的host
是View树中的根视图(DecroView
),也就是最外层容器,容器的位置安排在左上角(0,0),其大小默认会填满 mContentParent
容器。该方法作用是确定孩子控件的位置,所以该方法只针对ViewGroup
容器类,最终调用了View
的layout()
方法确定每一个孩子控件的位置:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
在layout()
中确定位置之前会判断是否需要重新测量控件的大小,如果需要,就会调用onMeasure()
方法重新测量控件,接下来执行 setOpticalFrame()
或 setFrame()
方法确定自身的位置与大小,这一步并不会绘制出来,只是将控件位子和大小值保存起来;接着调用onLayout()
方法,在View中onLayout()
方法是空实现。onLayout()
方法的作用是当当前控件是容器控件时,那就必须重写onLayout()
方法确定每一个孩子控件的位置,而当孩子控件还是ViewGroup
的子类时,继续调用onLayout()
方法,直到所有的孩子控件都有了确定的位置和大小,这个过程和measure一样,也可以看做是一个递归的过程。下面是FrameLayout#onLayout()
方法源码:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
...// 遍历所有的孩子控件
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
... // 判断控件的visible属性书否为gone,如果为gone就不占用位置
... // 计算childLeft、childTop、childRight、childBottom,确定位置
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
layout总结:
- View的布局逻辑是由父View,也就是ViewGroup容器布局来实现的。因此,我们如果自定义View一般都无需重写onLayout()方法,但是如果自定义一个ViewGroup容器的话,就必须实现onLayout()方法,因为该方法在ViewGroup类中是抽象的,ViewGroup的所有子类必须实现onLayout()方法(如果我们定义的容器控件是继承FrameLayout或其他已经继承了ViewGroup类的容器控件时,如果没有必要可以不用实现onLayout()方法,因为FrameLayout类中已经实现了);
- 如果view控件使用了gone属性时,在onLayout()方法遍历中就会跳过当前的View,因为gone属性表示不占用位置;
- 当layout()方法执行完成之后,调用getHeight()/getWidth()方法就能够得到控件的宽和高的值了;
- View的layout过程和measure过程类似,都可以看做是一个递归的过程;
- 在Activity中,layout的过程是从DecorView控件开始的。
绘制 -- draw
源码追踪
ViewRootImpl
中的performDraw()
方法:
private void performDraw() {
...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...
}
private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
return;
}
...
}
/**
* @return true if drawing was succesfull, false if an error occurred
*/
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
boolean scalingRequired, Rect dirty) {
Canvas canvas;
...
try {
...
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
canvas = mSurface.lockCanvas(dirty);
if (!canvas.isOpaque() || yoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
mView.draw(canvas);
...
} finally {
surface.unlockCanvasAndPost(canvas);
}
...
return true;
}
canvas
对象是从surface
中获取到的,surface
是中提供了一套双缓存机制,这样就提高了绘图的效率.通过代码可以看到最后调用了mView
的draw()
方法,mView
是ecorView
,也就是FrameLayou
t,在FrameLayout
和ViewGroup
中都是没有重写draw()
方法的,所以最终调用的是View
中的draw()
方法:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background 绘制视图View的背景
* 2. If necessary, save the canvas' layers to prepare for fading 保存画布canvas的边框参数
* 3. Draw view's content 绘制视图View的内容(调用了onDraw()方法)
* 4. Draw children 绘制当前视图View的子视图(调用dispatchDraw()方法)
* 5. If necessary, draw the fading edges and restore layers 绘制边框的渐变效果并重置画布
* 6. Draw decorations (scrollbars for instance) 绘制前景、滚动条等修饰
*/
// Step 1, draw the background, if needed 绘制视图View的背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
...
// Step 2, save the canvas' layers 保存画布canvas的边框参数
saveCount = canvas.getSaveCount();
// Step 3, draw the content 绘制视图View的内容(调用了onDraw()方法)
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children 绘制当前视图View的子视图(调用dispatchDraw()方法)
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers 绘制边框的渐变效果并重置画布
...
canvas.restoreToCount(saveCount);
// Step 6, draw decorations (foreground, scrollbars) 绘制前景、滚动条等修饰
onDrawForeground(canvas);
}
由以上代码可以看得到,系统在View
类中的draw()
方法已经将背景、边框、修饰等都绘制出来了,而且在第三步和第四步的时候调用了onDraw()
方法和dispatchDraw()
方法,这样开发者在自定义控件的时候就只需要重写onDraw()
方法或者dispatchDraw()
方法,也就是只需要关注最终显示的内容就可以了,而不需要去绘制其他的修饰。
在View
里面的onDraw()
方法和dispatchDraw()
方法都是空实现,也就是留给开发者去实现里面的具体逻辑。同时,在开发者实现onDraw()
方法和dispatchDraw()
方法时也可以不用去绘制其他的修饰了。需要说明一点,View
中的draw()
方法并不是和measure()
方法一样被final
修饰,draw()
方法没有被final
修饰,所以是可以重写的,但是当我们重写draw()
方法时,必须和系统中View
的draw()
方法一样,一步一步的实现,否则就不能将控件绘制出来,所以在自定义控件的时候,一般都是重写onDraw()
或dispatchDraw()
方法。
如果自定义控件是继承至View
时,就重写onDraw()
方法,在onDraw()
方法中绘制的结果就是最终显示的结果。
以下代码就是在界面上以坐标(150,150)绘制了一个半径为35的红色实心圆:
public class CircleView extends View {
Paint paint;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(150,150,35,paint);
}
}
View 绘制流程_circle.png
如果自定义控件是继承至ViewGroup
时,就重写dispatchDraw()
方法,这里直接查看系统FrameLayout
的dispatchDraw()
方法:
@Override
protected void dispatchDraw(Canvas canvas) {
final int childrenCount = mChildrenCount;
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
}
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
在代码里面调用了drawChild(canvas, child, drawingTime)
方法用来绘制孩子:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
而在drawChild()
方法中就是调用了view
的draw(Canvas canvas, ViewGroup parent, long drawingTime)
方法(三个参数的方法)来绘制自己,如果孩子控件还是ViewGroup
的子类,又会重新调用drawChild()
方法递归处理,直到所有的孩子控件绘制完成,也就表示控件绘制完成了。其实这也和measure、layout的过程一样,可以看做是一个递归的过程。
看一下view
中三个参数的draw(Canvas canvas, ViewGroup parent, long drawingTime)
方法:
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
* 这个方法是在ViewGroup的drawChild()方法中调用,用来绘制每一个孩子控件自身。
* This draw() method is an implementation detail and is not intended to be overridden or
* to be called from anywhere else other than ViewGroup.drawChild().
* 这个方法除了在ViewGroup的drawChild()方法中被调用外,不应该在其它任何地方去复写或调用该方法,它属于ViewGroup。
* 而这个方法最终也会调用View的draw(canvas)一个参数的方法来进行绘制。
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (!hasDisplayList) {
// 调用computeScroll()方法,这个方法是用来与Scroller类结合实现实现动画效果的
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
...
if (!hasDisplayList) {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
// 继续调用dispatchDraw()方法递归处理
dispatchDraw(canvas);
} else {
// 调用View的draw(canvas)一个参数的方法
draw(canvas);
}
} else {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);
}
...
return more;
}
了解更多关于Scroller类的相关内容,可以查看《 Android中的Scroller类》这篇博客。
draw总结:
-
View
绘制的画布canvas
是从surface
对象中获得,而最终也是绘制到surface
中去。surface
提供了一个双缓存机制,可以提高绘制的效率; - 系统在
View
类中的draw()
方法已经将背景、边框、修饰等都绘制出来了,如果在自定义View时直接继承View
时是重写draw()
方法,就必须和系统中View
的draw()
方法一样,一步一步的实现,否则就不能将控件展示出来; - 因为自定义控件一般重写
onDraw()
方法,所以每一个控件都会绘制滚动条和其他的修饰; - 自定义
View
如果直接继承制View
时,需要重写onDraw()
方法,在onDraw()
中绘制的内容就是最终展示到界面的内容,自定义View如果是直接继承ViewGroup
,那就重写dispatchDraw()
方法,绘制ViewGroup
的孩子控件; - Android绘制的过程和measure、layout一样,可以看做是一个递归的过程。
网友评论