在自定义View的时候,大家都知道一般需要重写三个方法:onMeasure()(确定大小和模式)、onLayout()(确定摆放位置)、onDraw()(画,显示)。但是大部分只知道需要重写,但是不知道为什么是这三个,而不是别的,而且一般只用到第一和第三个就能满足大部分需求。刚刚开始的时候我也是只知道重写,能实现需求就好了,但是过段时间之后,会发现自己还停留在用的层面上,直到去年有一次面试的时候被面试官问道:为什么要重写这三个方法?那三种模式是如何被确定的?View和ViewGround各自的实现有什么区别和为什么?等等。因为简历上写着熟悉自定义View,好吧,应该写着会用而不是熟悉,如果这样能有多的面试机会的话。。。从那以后我决定要做有自我意识的程序员而不是面向谷歌程序员
熟悉View的绘制流程,可以写出提高性能的UI和布局,这会大大地改善APP的质量。相信很多人在Google如何提高app的性能的时候会看到大部分都介绍要减少布局嵌套、要防止过度渲染绘制,要控制好background的设置等等。虽然告诉解决办法,或者说是折中处理方法,但是却不知道为什么?有成语说得对:知彼知己,百战不殆。无论是做什么,在行动前都必须要了解自己所面对的事物,才能把事情完成得更好,编程也是一样,一天到晚都在那里改,改来改去,效果不甚好,更浪费时间,我们得找准原因,逐一击破才是。
本片内容需要结合着上一边文章来讲,所以需要先了解一下Activity的启动流程和setContentView()的流程分析最佳,还没看过的童鞋可以先看看:基于9.0的setContentView源码分析、基于Android 9.0的Activity启动流程源码分析
View的绘制入口
之前分析过了setContentView()的源码,知道了怎么把我们设置的布局给添加进去,但是还不知道怎样把布局渲染绘制出来。接下来我们开始分析这个过程
类:PhoneWindow
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor(); //注释1
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent); //注释2
}
mContentParent.requestApplyInsets(); //注释3
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
这个是之前分析setContentView()的源码的时候的一段源码,我们已经分析过来,这里就不便详细叙说,大概讲一下就好了。在注释1创建了DecorView,还创建了mContentParent给添加到DecorView上,注释2就把我们的布局给添加到mContentParent中,就这样把整个布局给添加进去了,接下来看一下注释3:
类 View
public void requestApplyInsets() {
requestFitSystemWindows();
}
public void requestFitSystemWindows() {
if (mParent != null) {
mParent.requestFitSystemWindows(); //注释1
}
}
当执行注释1的时候,mParent是ViewParent类型的变量:
protected ViewParent mParent;
赋值:
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
ViewParent 是一个抽象类,我们需要找它的实现类ViewRootImpl,so,搜索一下ViewRootImpl的requestFitSystemWindows();方法:
类 ViewRootImpl
public void requestFitSystemWindows() {
checkThread(); //检查当前线程是否为之前之前调用过的线程,也就是UI线程
mApplyInsetsRequested = true;
scheduleTraversals();
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
依次执行下来,直到 performTraversals(),一般网上的文章都是直接从 performTraversals()开始分析,但是很多人不知道是怎么找到这个入口的,就直接搜索,现在我直接把如何进入到 performTraversals()过程直接贴出来,应该清楚了吧。废话不说了,继续点击查看 performTraversals()的执行代码:
类 ViewRootImpl
private void performTraversals() {
//注释1
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//注释2
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//注释3
performLayout(lp, mWidth, mHeight);
//注释4
performDraw();
}
看到注释1、2、3、4有木有很是激动啊?终于找到了performMeasure()、performLayout()、performLayout()这三个方法,这三个方法会依次执行onMeasure()、onLayout()、onDraw()方法这三个方法。
建议对照着View的绘制流程图来看,这样看起来逻辑比较清晰:
先看getRootMeasureSpec()方法,这里根据根部局设置的宽高属性来决定了根部局的模式和大小
类 ViewRootImpl
//决定了根部局的模式和大小
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
//确定模式和大小
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
经过上面所贴的代码之后,确定了根布局的模式和大小,接下来就需要确定内容(控件)的模式和大小了
一、View的onMeasure
类 ViewRootImpl
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
直接点开看 mView.measure():
类 View
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
//注释1
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
省略......
onMeasure(widthMeasureSpec, heightMeasureSpec);
省略......
}
在注释1的时候判断自身的模式和大小:
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) { //如果是UNSPECIFIED模式
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode); //EXACTLY 和 AT_MOST模式
}
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
可以看出在执行getMode()方法获取模式的时候是根据传进来的measureSpec的&运算得到的:
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
回头看measureSpec到底是怎么传进来的,我们可以退到前一步的measure()方法来看,发现是我们直接传进来的:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
省略......
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
省略......
}
经过执行 MeasureSpec.adjust()方法,把measure()的两个参数分别传进来计算的,而widthMeasureSpec和vheightMeasureSpec则是在ViewRootImpl的performMeasure()传过来的,而performMeasure()则是通过getRootMeasureSpec()方法,结合根部局(父布局)来运算得到,这些在前面也讲到了这些。
由此可以得出一个结论,自身的模式和大小不仅仅受自身设置的宽高属性有关,还和父布局设置的属性有关,所显示的范围还受父布局的影响。
接下来到我们的重头戏了:onMeasure()
类 View
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
当需要重新设置模式和大小的时候,setMeasuredDimension()我们也经常用到,我们先看getDefaultSize()方法:
类 View
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
在getDefaultSize()方法中根据模式来确定了大小,但是这个还不是最终确定的大小,还要受子View的影响。但是基本上这个结果是和最后的大小是一致的。最后通过setMeasuredDimension()方法把getDefaultSize()运算的值设置一下就成了我们重写onMeasure()方法的参数值。
二、View的onLayout()
接下来看 performLayout():
类 ViewRootImpl
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
省略......
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
省略......
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
类 View
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;
}
省略......
onLayout(changed, l, t, r, b);
省略......
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
原来performLayout()只是把左右上下的坐标传进来,啥都没干。值得注意的是,这里的左右上下的坐标是依据父布局的坐标而不是依赖于屏幕的坐标,这个得要很清楚。
三、View的onDraw()
类 ViewRootImpl
private void performDraw() {
省略......
try {
boolean canUseAsync = draw(fullRedrawNeeded);
if (usingAsyncReport && !canUseAsync) {
mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
usingAsyncReport = false;
}
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
省略......
}
private boolean draw(boolean fullRedrawNeeded) {
省略......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
省略......
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
省略......
mView.draw(canvas);
省略......
}
一路执行下来,直到 mView.draw(canvas)这行代码,这时候已经跳到View的方法里了:
类 View
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
省略......
}
//是否自动填充
private void drawAutofilledHighlight(@NonNull Canvas canvas) {
if (isAutofilled()) {
Drawable autofilledHighlight = getAutofilledDrawable();
if (autofilledHighlight != null) {
autofilledHighlight.setBounds(0, 0, getWidth(), getHeight());
autofilledHighlight.draw(canvas);
}
}
}
不知道你们看不看得懂这些源码注释?做IT的应该都会些英语吧?注释已经很清楚说明了绘制是按照这七个步骤进行的:
1、如果设置了背景,就先绘制背景: drawBackground(canvas);
2、检查是否设置了View的边缘化,也就是类似于渐变的效果,如果没有,则进去if里面;
3、进入if里面之后,开始绘制内容: onDraw(canvas);
4、绘制item view: dispatchDraw(canvas);
5、如果View是设置了自动填充的,则进行自动填充绘制:drawAutofilledHighlight(canvas);
6、绘制装饰,比如前景、滚动条等等: onDrawForeground(canvas);
7、绘制默认的焦点高亮显示:drawDefaultFocusHighlight(canvas)。
在第二步骤判断的时候,如果没进去if里面,也是按照顺序调用上面几个方法的,只不过是比之前多了保存图存的操作。
ViewGroup的绘制特点
ViewGround和View的绘制流程是大同小异的,只是某些地方需要注意一下,比如经常使用到的LinearLayout布局控件,其继承的就是ViewGroup,按照上面的View的绘制解析,第一步是measure,所以先看一下LinearLayout的onMeasure()方法:
类 LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec); //竖直方向
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec); //横向方向
}
}
//可以先选择竖直方向来看一下
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
省略......
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) { //注释1
final View child = getVirtualChildAt(i);
省略......
f (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
//由于是EXACTLY 模式,不需要measure子view了
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else{
//如果不是EXACTLY模式则需要循环measure每个子item
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
//把每次测量过后的子view的高度不断累加
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
}
省略......
//计算padding、margin......这些
省略......
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//最后确定了LinearLayout的高度和宽度
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
由于源码太多,我抽出了关键的几句贴出来,详细的可以自己去看,但是流程都是递归measure子View来测量,然后累加起来,大概就是这样。就这样,在进入for循环递归的时候,会执行measureChildBeforeLayout()来调用子view的measure:
类 LinearLayout
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
继续进去看measureChildWithMargins()。
类 ViewGroup
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在最后一行代码执行了 child.measure()方法,也就是View的measure方法,剩下的和之前分析View的onMeasure()一摸一样。由此可以得出LinearLayout也确实是通过递归来测量子view的宽高,然后再累加来计算出实际高度的。
刚刚看的是竖直方向的,那横向方向呢?按照上面的递归计算逻辑,应该也是由外而内传递下去,再从内而外计算累加起来,只不过计算的是宽度而已,而看源码也确实如此,有兴趣的可以看一下,这里就不多加述说了。
接下来看下onLayout的方法实现
类 LinearLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
onLayout()也是分为竖直和横向方向,我们可以随便看,这里看竖直好了:
类 LinearLayout
void layoutVertical(int left, int top, int right, int bottom) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
}
}
查看setChildFrame()方法:
类 LinearLayout
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
从上面贴出的代码看,就和之前的measure一样,也是利用递归来不断调用子view的。既然如此,那onDraw()是否也一样呢?不一定!
在上面分析View的draw()方法的源码的时候,要执行onDraw()方法是有一个判断的:
类 View
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
}
从draw()方法可以看出,当dirtyOpaque = true的时候才会执行onDraw()方法,而ViewGroup在初始化的时候调用了 initViewGroup(),然后在里面调用了setFlags()方法:
类 ViewGroup
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
}
setFlags()是View的一个方法,而ViewGroup继承于View,所以直接看View的实现方法:
类 View
void setFlags(int flags, int mask) {
省略......
int old = mViewFlags;
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
int changed = mViewFlags ^ old;
省略......
if ((changed & DRAW_MASK) != 0) {
if ((mViewFlags & WILL_NOT_DRAW) != 0) {
if (mBackground != null
|| mDefaultFocusHighlight != null
|| (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
} else {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
} else {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
}
}
ViewGroup在初始化的时候就把mPrivateFlags设置成了WILL_NOT_DRAW,也就是不去执行onDraw()方法,所以 if (!dirtyOpaque) 为false,那onDraw()方法自然也不会执行,除非设置了背景(如:#00000000)或者在自定义ViewGroup的时候在构造函数里面,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。这也是为什么我们在 自定义View的时候会自动调用onDraw(),而自定义ViewGroup则不会调用。显然LinearLayout是继承与ViewGroup的,默认也是不调用onDraw()方法的。
但是很多时候,项目的需求是是千奇百怪的,为了迎合产品,我们往往需要自定义控件,但是在很多时候,我们可以继承现有的控件来拓展功能,而不必要全都要自己实现,这可以省下很多时间和功夫。那在继承ViewGroup这些类型的控件的时候,onDraw()方法又不被调用?除了设置重新设置flag或背景之外还有别的方法吗?答案:确实有一个。
在分析View的draw()方法源码的时候,有一个语句很是耐人寻味:
类 View
public void draw(Canvas canvas) {
省略......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
省略......
}
虽然onDraw()方法不一定被执行,但是 dispatchDraw()肯定会被执行的,所以在自定义ViewGroup的时候,可以重写dispatchDraw()方法来代替onDraw()方法。
总结
- 1、重写三个方法:在分析完了View的绘制流程之后,对于为什么要重写onMeasure()、onLayout()、onDraw()这三个方法有了清楚的了解,以及调用的顺序是怎么样的;
- 2、了解View和ViewGroup的绘制流程的区别,以及注意事项:View会自己调用onDraw()方法,而ViewGroup则不会默认调用,要想ViewGroup也调用可以在构造函数中调用setWillNotDraw(false),去掉其WILL_NOT_DRAW的flag、设置背景色或者改为重写dispatchDraw()方法;
- 3、布局优化、减少嵌套:在分析LinearLayout这些ViewGroup类型的控件的时候,发现在measure、layout的时候是采用递归不断循环绘制渲染的,如果布局嵌套太深,那花费的时候也就更久了,更别说还需要调用onDraw()方法的时候了,如果绘制时间较久,就会给用户造成卡顿的现象。所以需要在布局的时候多考虑嵌套问题,在遇到比较复杂的布局,不妨可以考虑自定义布局控件或者第三方的;
- 4、尽量少设置背景:控件设置了背景的话,会让draw的时候花费更多的时间,特别是那种嵌套的情况下,各自设置了背景会导致过度绘制渲染问题,红了就不好看了;
- 5、正确 获取控件的宽高size:应该老有人在onCreate()、onResume()这些方法中获取控件的大小吧,但无意外都是获取到了0或者-1,在View的绘制流程的时候直到,在调用onCreate()、onResume()这些方法的时候还没开始测量控件的宽高,也就无法获取,要想获取到真实的宽高大小,可以利用View.post(Runnable)的方法来获取,或者其他的方法,网上很多,这里就不一一列举了,总之就是必须要测量完成之后才可以获取到控件的宽高大小的。