2024年安卓最全Android应用层View绘制流程与源码分析(1),2024年最新秋招面试技巧

【附】相关架构及资料

源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,和技术大牛一起讨论交流解决问题。

image

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • {@link #measure(int, int)}. Calling the superclass’

  • {@link #onMeasure(int, int)} is a valid use.

  • The base class implementation of measure defaults to the background size,

  • unless a larger size is allowed by the MeasureSpec. Subclasses should

  • override {@link #onMeasure(int, int)} to provide better measurements of

  • their content.

  • If this method is overridden, it is the subclass’s responsibility to make

  • sure the measured height and width are at least the view’s minimum height

  • and width ({@link #getSuggestedMinimumHeight()} and

  • {@link #getSuggestedMinimumWidth()}).

  • @param widthMeasureSpec horizontal space requirements as imposed by the parent.

  •                     The requirements are encoded with
    
  •                     {@link android.view.View.MeasureSpec}.
    
  • @param heightMeasureSpec vertical space requirements as imposed by the parent.

  •                     The requirements are encoded with
    
  •                     {@link android.view.View.MeasureSpec}.
    
  • @see #getMeasuredWidth()

  • @see #getMeasuredHeight()

  • @see #setMeasuredDimension(int, int)

  • @see #getSuggestedMinimumHeight()

  • @see #getSuggestedMinimumWidth()

  • @see android.view.View.MeasureSpec#getMode(int)

  • @see android.view.View.MeasureSpec#getSize(int)

*/

//View的onMeasure默认实现方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

看见没有,其实注释已经很详细了(自定义View重写该方法的指导操作注释都有说明),不做过多解释。

对于非ViewGroup的View而言,通过调用上面默认的onMeasure即可完成View的测量,当然你也可以重载onMeasure并调用setMeasuredDimension来设置任意大小的布局,但一般不这么做,因为这种做法不太好,至于为何不好,后面分析完你就明白了。

我们可以看见onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束。既然这样那我们就看看设置的默认尺寸大小吧,可以看见setMeasuredDimension传入的参数都是通过getDefaultSize返回的,所以再来看下getDefaultSize方法源码,如下:

public static int getDefaultSize(int size, int measureSpec) {

int result = size;

//通过MeasureSpec解析获取mode与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;

}

看见没有,如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格。

回过头继续看上面onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View传递进来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,具体如下:

protected int getSuggestedMinimumWidth() {

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

protected int getSuggestedMinimumHeight() {

return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}

看见没有,建议的最小宽度和高度都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的。

到此一次最基础的元素View的measure过程就完成了。上面说了View实际是嵌套的,而且measure是递归传递的,所以每个View都需要measure。实际能够嵌套的View一般都是ViewGroup的子类,所以在ViewGroup中定义了measureChildren, measureChild, measureChildWithMargins方法来对子视图进行测量,measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。如下我们以ViewGroup中稍微复杂的measureChildWithMargins方法来分析:

/**

  • Ask one of the children of this view to measure itself, taking into

  • account both the MeasureSpec requirements for this view and its padding

  • and margins. The child must have MarginLayoutParams The heavy lifting is

  • done in getChildMeasureSpec.

  • @param child The child to measure

  • @param parentWidthMeasureSpec The width requirements for this view

  • @param widthUsed Extra space that has been used up by the parent

  •    horizontally (possibly by other children of the parent)
    
  • @param parentHeightMeasureSpec The height requirements for this view

  • @param heightUsed Extra space that has been used up by the parent

  •    vertically (possibly by other children of the parent)
    

*/

protected void measureChildWithMargins(View child,

int parentWidthMeasureSpec, int widthUsed,

int parentHeightMeasureSpec, int heightUsed) {

//获取子视图的LayoutParams

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

//调整MeasureSpec

//通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格

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);

//调运子View的measure方法,子View的measure中会回调子View的onMeasure方法

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

关于该方法的参数等说明注释已经描述的够清楚了。该方法就是对父视图提供的measureSpec参数结合自身的LayoutParams参数进行了调整,然后再来调用child.measure()方法,具体通过方法getChildMeasureSpec来进行参数调整。所以我们继续看下getChildMeasureSpec方法代码,如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

//获取当前Parent View的Mode和Size

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

//获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0

int size = Math.max(0, specSize - padding);

//定义返回值存储变量

int resultSize = 0;

int resultMode = 0;

//依据当前Parent的Mode进行switch分支逻辑

switch (specMode) {

// Parent has imposed an exact size on us

//默认Root View的Mode就是EXACTLY

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {

//如果child的layout_wOrh属性在xml或者java中给予具体大于等于0的数值

//设置child的size为真实layout_wOrh属性值,mode为EXACTLY

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

//如果child的layout_wOrh属性在xml或者java中给予MATCH_PARENT

// Child wants to be our size. So be it.

//设置child的size为size,mode为EXACTLY

resultSize = size;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

//如果child的layout_wOrh属性在xml或者java中给予WRAP_CONTENT

//设置child的size为size,mode为AT_MOST

// Child wants to determine its own size. It can’t be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

//其他Mode分支类似

}

//将mode与size通过MeasureSpec方法整合为32位整数返回

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

可以看见,getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension。

所以可以看见onMeasure的参数其实就是这么计算出来的。同时从上面的分析可以看出来,最终决定View的measure大小是View的setMeasuredDimension方法,所以我们可以通过setMeasuredDimension设定死值来设置View的mMeasuredWidth和mMeasuredHeight的大小,但是一个好的自定义View应该会根据子视图的measureSpec来设置mMeasuredWidth和mMeasuredHeight的大小,这样的灵活性更大,所以这也就是上面分析onMeasure时说View的onMeasure最好不要重写死值的原因。

可以看见当通过setMeasuredDimension方法最终设置完成View的measure之后View的mMeasuredWidth和mMeasuredHeight成员才会有具体的数值,所以如果我们自定义的View或者使用现成的View想通过getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

还记得前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章3-3小节探讨的inflate方法加载一些布局显示时指定的大小失效问题吗?当时只给出了结论,现在给出了详细原因分析,我想不需要再做过多解释了吧。

至此整个View绘制流程的第一步就分析完成了,可以看见,相对来说还是比较复杂的,接下来进行小结。

2-2 measure原理总结

通过上面分析可以看出measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。具体measure核心主要有如下几点:

  • MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;

MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;

MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;

  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

  • View的布局大小由父View和子View共同决定。

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

3 View绘制流程第二步:递归layout源码分析


在上面的背景介绍就说过,当ViewRootImpl的performTraversals中measure执行完成以后会接着执行mView.layout,具体如下:

private void performTraversals() {

mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());

}

可以看见layout方法接收四个参数,这四个参数分别代表相对Parent的左、上、右、下坐标。而且还可以看见左上都为0,右下分别为上面刚刚测量的width和height。

至此又回归到View的layout(int l, int t, int r, int b)方法中去实现具体逻辑了,所以接下来我们开始分析View的layout过程。

整个View树的layout递归流程图如下:

这里写图片描述

3-1 layout源码分析

layout既然也是递归结构,那我们先看下ViewGroup的layout方法,如下:

@Override

public final void layout(int l, int t, int r, int b) {

super.layout(l, t, r, b);

}

看着没有?ViewGroup的layout方法实质还是调运了View父类的layout方法,所以我们看下View的layout源码,如下:

public void layout(int l, int t, int r, int b) {

//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量

//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout

boolean changed = isLayoutModeOptical(mParent) ?

setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

//需要重新layout

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

//回调onLayout

onLayout(changed, l, t, r, b);

}

}

看见没有,类似measure过程,lauout调运了onLayout方法。

对比上面View的layout和ViewGroup的layout方法可以发现,View的layout方法是可以在子类重写的,而ViewGroup的layout是不能在子类重写的,言外之意就是说ViewGroup中只能通过重写onLayout方法。那我们接下来看下ViewGroup的onLayout方法,如下:

@Override

protected abstract void onLayout(boolean changed,

int l, int t, int r, int b);

看见没有?ViewGroup的onLayout()方法竟然是一个抽象方法,这就是说所有ViewGroup的子类都必须重写这个方法。所以在自定义ViewGroup控件中,onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。

再看下View的onLayout方法源码,如下:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}

我勒个去!是一个空方法,没啥可看的。

既然这样那我们只能分析一个现有的继承ViewGroup的控件了,就拿LinearLayout来说吧,如下是LinearLayout中onLayout的一些代码:

public class LinearLayout extends ViewGroup {

@Override

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);

}

}

}

看见没有,LinearLayout的layout过程是分Vertical和Horizontal的,这个就是xml布局的orientation属性设置的,我们为例说明ViewGroup的onLayout重写一般步骤就拿这里的VERTICAL模式来解释吧,如下是layoutVertical方法源码:

void layoutVertical(int left, int top, int right, int bottom) {

final int paddingLeft = mPaddingLeft;

int childTop;

int childLeft;

// Where right end of child should go

//计算父窗口推荐的子View宽度

final int width = right - left;

//计算父窗口推荐的子View右侧位置

int childRight = width - mPaddingRight;

// Space available for child

//child可使用空间大小

int childSpace = width - paddingLeft - mPaddingRight;

//通过ViewGroup的getChildCount方法获取ViewGroup的子View个数

final int count = getVirtualChildCount();

//获取Gravity属性设置

final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;

final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

//依据majorGravity计算childTop的位置值

switch (majorGravity) {

case Gravity.BOTTOM:

// mTotalLength contains the padding already

childTop = mPaddingTop + bottom - top - mTotalLength;

break;

// mTotalLength contains the padding already

case Gravity.CENTER_VERTICAL:

childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;

break;

case Gravity.TOP:

default:

childTop = mPaddingTop;

break;

}

//重点!!!开始遍历

for (int i = 0; i < count; i++) {

final View child = getVirtualChildAt(i);

if (child == null) {

childTop += measureNullChild(i);

} else if (child.getVisibility() != GONE) {

//LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值

final int childWidth = child.getMeasuredWidth();

final int childHeight = child.getMeasuredHeight();

//获取子View的LayoutParams

final LinearLayout.LayoutParams lp =

(LinearLayout.LayoutParams) child.getLayoutParams();

int gravity = lp.gravity;

if (gravity < 0) {

gravity = minorGravity;

}

final int layoutDirection = getLayoutDirection();

final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);

//依据不同的absoluteGravity计算childLeft位置

switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {

case Gravity.CENTER_HORIZONTAL:

childLeft = paddingLeft + ((childSpace - childWidth) / 2)

  • lp.leftMargin - lp.rightMargin;

break;

case Gravity.RIGHT:

childLeft = childRight - childWidth - lp.rightMargin;

break;

case Gravity.LEFT:

default:

childLeft = paddingLeft + lp.leftMargin;

break;

}

if (hasDividerBeforeChildAt(i)) {

childTop += mDividerHeight;

}

childTop += lp.topMargin;

//通过垂直排列计算调运child的layout设置child的位置

setChildFrame(child, childLeft, childTop + getLocationOffset(child),

childWidth, childHeight);

childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);

}

}

}

从上面分析的ViewGroup子类LinearLayout的onLayout实现代码可以看出,一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子View的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子View的具体位置。

到这里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别(上面分析measure过程已经说过getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效)。可以看出来getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。那我们看下View源码中这些方法的实现吧,如下:

public final int getMeasuredWidth() {

return mMeasuredWidth & MEASURED_SIZE_MASK;

}

public final int getMeasuredHeight() {

return mMeasuredHeight & MEASURED_SIZE_MASK;

}

public final int getWidth() {

return mRight - mLeft;

}

public final int getHeight() {

return mBottom - mTop;

}

public final int getLeft() {

return mLeft;

}

public final int getRight() {

return mRight;

}

public final int getTop() {

return mTop;

}

public final int getBottom() {

return mBottom;

}

这也解释了为什么有些情况下getWidth()和getMeasuredWidth()以及getHeight()和getMeasuredHeight()会得到不同的值,所以这里不做过多解释。

到此整个View的layout过程分析就算结束了,接下来进行一些总结工作。

3-2 layout原理总结

整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

  • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

  • 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的(前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》也有提到过)。

  • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

4 View绘制流程第三步:递归draw源码分析


在上面的背景介绍就说过,当ViewRootImpl的performTraversals中measure和layout执行完成以后会接着执行mView.layout,具体如下:

private void performTraversals() {

final Rect dirty = mDirty;

canvas = mSurface.lockCanvas(dirty);

mView.draw(canvas);

}

draw过程也是在ViewRootImpl的performTraversals()内部调运的,其调用顺序在measure()和layout()之后,这里的mView对于Actiity来说就是PhoneWindow.DecorView,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程。

先来看下View树的递归draw流程图,如下:

这里写图片描述

如下我们详细分析这一过程。

4-1 draw源码分析

由于ViewGroup没有重写View的draw方法,所以如下直接从View的draw方法开始分析:

public void draw(Canvas canvas) {

/*

  • 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

if (!dirtyOpaque) {

drawBackground(canvas);

}

// skip step 2 & 5 if possible (common case)

// Step 2, save the canvas’ layers

if (drawTop) {

canvas.saveLayer(left, top, right, top + length, null, flags);

}

// Step 3, draw the content

if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

// Step 5, draw the fade effect and restore layers

if (drawTop) {

matrix.setScale(1, fadeHeight * topFadeStrength);

matrix.postTranslate(left, top);

fade.setLocalMatrix(matrix);

p.setShader(fade);

canvas.drawRect(left, top, right, top + length, p);

}

// Step 6, draw decorations (scrollbars)

onDrawScrollBars(canvas);

}

看见整个View的draw方法很复杂,但是源码注释也很明显。从注释可以看出整个draw过程分为了6步。源码注释说(”skip step 2 & 5 if possible (common case)”)第2和5步可以跳过,所以我们接下来重点剩余四步。如下:

第一步,对View的背景进行绘制。

可以看见,draw方法通过调运drawBackground(canvas);方法实现了背景绘制。我们来看下这个方法源码,如下:

private void drawBackground(Canvas canvas) {

//获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable

final Drawable background = mBackground;

//根据layout过程确定的View位置来设置背景的绘制区域

if (mBackgroundSizeChanged) {

background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);

mBackgroundSizeChanged = false;

rebuildOutline();

}

//调用Drawable的draw()方法来完成背景的绘制工作

background.draw(canvas);

}

第三步,对View的内容进行绘制。

可以看到,这里去调用了一下View的onDraw()方法,所以我们看下View的onDraw方法(ViewGroup也没有重写该方法),如下:

/**

  • Implement this to do your drawing.

  • @param canvas the canvas on which the background will be drawn

*/

protected void onDraw(Canvas canvas) {

}

可以看见,这是一个空方法。因为每个View的内容部分是各不相同的,所以需要由子类去实现具体逻辑。

第四步,对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制。

我们来看下View的draw方法中的dispatchDraw(canvas);方法源码,可以看见如下:

/**

  • Called by draw to draw the child views. This may be overridden

  • by derived classes to gain control just before its children are drawn

  • (but after its own view has been drawn).

  • @param canvas the canvas on which to draw the view

*/

protected void dispatchDraw(Canvas canvas) {

}

看见没有,View的dispatchDraw()方法是一个空方法,而且注释说明了如果View包含子类需要重写他,所以我们有必要看下ViewGroup的dispatchDraw方法源码(这也就是刚刚说的对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制的原因,因为如果是View调运该方法是空的,而ViewGroup才有实现),如下:

@Override

protected void dispatchDraw(Canvas canvas) {

final int childrenCount = mChildrenCount;

final View[] children = mChildren;

for (int i = 0; i < childrenCount; i++) {

if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {

more |= drawChild(canvas, child, drawingTime);

}

}

// Draw any disappearing views that have animations

if (mDisappearingChildren != null) {

for (int i = disappearingCount; i >= 0; i–) {

more |= drawChild(canvas, child, drawingTime);

}

}

}

可以看见,ViewGroup确实重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

return child.draw(canvas, this, drawingTime);

}

可以看见drawChild()方法调运了子View的draw()方法。所以说ViewGroup类已经为我们重写了dispatchDraw()的功能实现,我们一般不需要重写该方法,但可以重载父类函数实现具体的功能。

第六步,对View的滚动条进行绘制。

可以看到,这里去调用了一下View的onDrawScrollBars()方法,所以我们看下View的onDrawScrollBars(canvas);方法,如下:

/**

  • Request the drawing of the horizontal and the vertical scrollbar. The

  • scrollbars are painted only if they have been awakened first.

  • @param canvas the canvas on which to draw the scrollbars

  • @see #awakenScrollBars(int)

*/

protected final void onDrawScrollBars(Canvas canvas) {

//绘制ScrollBars分析不是我们这篇的重点,所以暂时不做分析

}

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。

到此,View的draw绘制部分源码分析完毕,我们接下来进行一些总结。

4-2 draw原理总结

可以看见,绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:

  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

  • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

  • View的绘制是借助onDraw方法传入的Canvas类来进行的。

  • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

5 View的invalidate和postInvalidate方法源码分析


学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

roup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

5 View的invalidate和postInvalidate方法源码分析


学习福利

【Android 详细知识点思维脑图(技能树)】

[外链图片转存中…(img-RjdolvMq-1714995361706)]

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-NYgIVpZq-1714995361707)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值