2024年安卓最全Android View的绘制流程,面试心得体会500字

最后

有任何问题,欢迎广大网友一起来交流,分享高阶Android学习视频资料和面试资料包~

偷偷说一句:群里高手如云,欢迎大家加群和大佬们一起交流讨论啊!

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

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

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

int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {

case MeasureSpec.UNSPECIFIED: //表示该View的大小父视图未定,设置为默认值

result = size;

break;

case MeasureSpec.AT_MOST:

case MeasureSpec.EXACTLY:

result = specSize;

break;

}

return result;

}

getDefaultSize的第一个参数size等于getSuggestedMinimumXXXX返回的的值(建议的最小宽度和高度),而建议的最小宽度和高度都是由View的Background尺寸与通过设置View的minXXX属性共同决定的,这个size可以理解为View的默认长度,而第二个参数measureSpec,是父View传给自己的MeasureSpec,这个measureSpec是通过测量计算出来的,具体的计算测量过程前面在讲解MeasureSpec已经讲得比较清楚了(是有父View的MeasureSpec和子View自己的LayoutParams 共同决定的)只要这个测试的mode不是UNSPECIFIED(未确定的),那么默认的就会用这个测量的数值当做View的高度。

对于View默认是测量很简单,大部分情况就是拿计算出来的MeasureSpec的size 当做最终测量的大小。而对于其他的一些View的派生类,如TextView、Button、ImageView等,它们的onMeasure方法系统了都做了重写,不会这么简单直接拿 MeasureSpec 的size来当大小,而去会先去测量字符或者图片的高度等,然后拿到View本身content这个高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等),而不是像View.java 直接用MeasureSpec的size做为View的大小。

4、ViewGroup的Measure过程

ViewGroup 类并没有实现onMeasure,我们知道测量过程其实都是在onMeasure方法里面做的,我们来看下FrameLayout 的onMeasure 方法,具体分析看注释哦。

//FrameLayout 的测量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int maxHeight = 0;

int maxWidth = 0;

int childState = 0;

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

final View child = getChildAt(i);

if (mMeasureAllChildren || child.getVisibility() != GONE) {

// 遍历自己的子View,只要不是GONE的都会参与测量,measureChildWithMargins方法在最上面

// 的源码已经讲过了,如果忘了回头去看看,基本思想就是父View把自己的MeasureSpec

// 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下传,

// 传递叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。

measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

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

maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);

maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);

}

}

//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,

//对于FrameLayout 可能用最大的字View的大小,对于LinearLayout,可能是高度的累加,

//具体测量的原理去看看源码。总的来说,父View是等所有的子View测量结束之后,再来测量自己。

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),

resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));

}

到目前为止,基本把Measure 主要原理都过了一遍,接下来我们会结合实例来讲解整个match的过程,首先看下面的代码:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:id=“@+id/linear”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:layout_marginTop=“50dp”

android:background=“@android:color/holo_blue_dark”

android:paddingBottom=“70dp”

android:orientation=“vertical”>

<TextView

android:id=“@+id/text”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:background=“@color/material_blue_grey_800”

android:text=“TextView”

android:textColor=“@android:color/white”

android:textSize=“20sp” />

<View

android:id=“@+id/view”

android:layout_width=“match_parent”

android:layout_height=“150dp”

android:background=“@android:color/holo_green_dark” />

上面的代码对于出来的布局是下面的一张图

对于上面图可能有些不懂,这边做下说明:

整个图是一个DecorView,DecorView可以理解成整个页面的根View,DecorView是一个FrameLayout,包含两个子View,一个id=statusBarBackground的View和一个是LineaLayout,id=statusBarBackground的View,我们可以先不管(我也不是特别懂这个View,应该就是statusBar的设置背景的一个控件,方便设置statusBar的背景),而这个LinearLayout比较重要,它包含一个title和一个content,title很好理解其实就是TitleBar或者ActionBar,content 就更简单了,setContentView()方法你应该用过吧,android.R.id.content 你应该听过吧,没错就是它,content是一个FrameLayout,你写的页面布局通过setContentView加进来就成了content的直接子View。

整个View的布局图如下:

这张图在下面分析measure,会经常用到,主要用于了解递归的时候view 的measure顺序

注:

1、 header的是个ViewStub,用来惰性加载ActionBar,为了便于分析整个测量过程,我把Theme设成NoActionBar,避免ActionBar 相关的measure干扰整个过程,这样可以忽略掉ActionBar 的测量,在调试代码更清晰。

2、包含Header(ActionBar)和id/content 的那个父View,我不知道叫什么名字好,我们姑且叫它ViewRoot(看上图),它是垂直的LinearLayout,放着整个页面除statusBar 的之外所有的东西,叫它ViewRoot 应该还ok,一个代号而已。

既然我们知道整个View的Root是DecorView,那么View的绘制是从哪里开始的呢,我们知道每个Activity 均会创建一个 PhoneWindow对象,是Activity和整个View系统交互的接口,每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带,绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程的。

我们来看下ViewRootImpl的performTraversals 方法:

private void performTraversals() {

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);

int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

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

mView.draw(canvas);

}

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;

}

return measureSpec;

}

performTraversals 中我们看到的mView其实就是DecorView,View的绘制从DecorView开始, 在mView.measure()的时候调用getRootMeasureSpec获得两个MeasureSpec做为参数,getRootMeasureSpec的两个参数(mWidth, lp.width)mWith和mHeight 是屏幕的宽度和高度, lp是WindowManager.LayoutParams,它的lp.width和lp.height的默认值是MATCH_PARENT,所以通过getRootMeasureSpec 生成的测量规格MeasureSpec 的mode是MATCH_PARENT ,size是屏幕的高宽。

因为DecorView 是一个FrameLayout 那么接下来会进入FrameLayout 的measure方法,measure的两个参数就是刚才getRootMeasureSpec的生成的两个MeasureSpec,DecorView的测量开始了。

首先是DecorView 的 MeasureSpec ,根据上面的分析DecorView 的 MeasureSpec是Windows传过来的,我们画出DecorView 的MeasureSpec 图:

注:

1、-1 代表的是EXACTLY,-2 是AT_MOST

2、由于屏幕的像素是1440x2560,所以DecorView 的MeasureSpec的size 对应于这两个值

那么接下来在FrameLayout 的onMeasure()方法DecorView开始for循环测量自己的子View,测量完所有的子View再来测量自己,由下图可知,接下来要测量ViewRoot的大小

//FrameLayout 的测量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int maxHeight = 0;

int maxWidth = 0;

int childState = 0;

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

final View child = getChildAt(i);

if (mMeasureAllChildren || child.getVisibility() != GONE) {

// 遍历自己的子View,只要不是GONE的都会参与测量,measureChildWithMargins方法在最上面

// 的源码已经讲过了,如果忘了回头去看看,基本思想就是父View把自己当MeasureSpec

// 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下穿,

// 传递叶子节点,叶子节点没有子View,只要负责测量自己就好了。

measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

}

}

}

DecorView 测量ViewRoot 的时候把自己的widthMeasureSpec和heightMeasureSpec传进去了,接下来你就要去看measureChildWithMargins的源码了

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

}

ViewRoot 是系统的View,它的LayoutParams默认都是match_parent,根据我们文章最开始MeasureSpec 的计算规则,ViewRoot 的MeasureSpec mode应该等于EXACTLY(DecorView MeasureSpec 的mode是EXACTLY,ViewRoot的layoutparams 是match_parent),size 也等于DecorView的size,所以ViewRoot的MeasureSpec图如下:

算出ViewRoot的MeasureSpec 之后,开始调用ViewRoot.measure 方法去测量ViewRoot的大小,然而ViewRoot是一个LinearLayout ,ViewRoot.measure最终会执行的LinearLayout 的onMeasure 方法,LinearLayout 的onMeasure 方法又开始逐个测量它的子View,上面的measureChildWithMargins方法又会被调用,那么根据View的层级图,接下来测量的是header(ViewStub),由于header的Gone,所以直接跳过不做测量工作,所以接下来轮到ViewRoot的第二个child content(android.R.id.content),我们要算出这个content 的MeasureSpec,所以又要拿ViewRoot 的MeasureSpec 和 android.R.id.content的LayoutParams 做计算了,计算过程就是调用getChildMeasureSpec的方法,

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {

final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height);

}

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

int specMode = MeasureSpec.getMode(spec); //获得父View的mode

int specSize = MeasureSpec.getSize(spec); //获得父View的大小

int size = Math.max(0, specSize - padding); //父View的大小-自己的Padding+子View的Margin,得到值才是子View可能的最大值。

}

由上面的代码

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

padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed

算出android.R.id.content 的MeasureSpec 的size

由于ViewRoot 的mPaddingBottom=100px(这个可能和状态栏的高度有关,我们测量的最后会发现id/statusBarBackground的View的高度刚好等于100px,ViewRoot 是系统的View的它的Padding 我们没法改变,所以计算出来Content(android.R.id.content) 的MeasureSpec 的高度少了100px ,它的宽高的mode 根据算出来也是EXACTLY(ViewRoot 是EXACTLY和android.R.id.content 是match_parent)。所以Content(android.R.id.content)的MeasureSpec 如下(高度少了100px):

Content(android.R.id.content) 是FrameLayout,递归调用开始准备计算id/linear的MeasureSpec,我们先给出结果:

图中有两个要注意的地方:

1、id/linear的heightMeasureSpec 的mode=AT_MOST,因为id/linear 的LayoutParams 的layout_height=“wrap_content”

2、id/linear的heightMeasureSpec 的size 少了200px, 由上面的代码

padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;

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

由于id/linear 的 android:layout_marginTop=“50dp” 使得lp.topMargin=200px (本设备的density=4,px=4*pd),在计算后id/linear的heightMeasureSpec 的size 少了200px。(布局代码前面已给出,可自行查看id/linear 控件xml中设置的属性)

linear.measure接着往下算linear的子View的的MeasureSpec,看下View 层级图,往下走应该是id/text,接下来是计算id/text的MeasureSpec,直接看图,mode=AT_MOST ,size 少了280,别问我为什么 …specSize - padding.

算出id/text 的MeasureSpec 后,接下来text.measure(childWidthMeasureSpec, childHeightMeasureSpec);准备测量id/text 的高宽,这时候已经到底了,id/text是TextView,已经没有子类了,这时候跳到TextView的onMeasure方法了。TextView 拿着刚才计算出来的heightMeasureSpec(mode=AT_MOST,size=1980),这个就是对TextView的高度和宽度的约束,进到TextView 的onMeasure(widthMeasureSpec,heightMeasureSpec) 方法,在onMeasure 方法执行调试过程中,我们发现下面的代码:

TextView字符的高度(也就是TextView的content高度[wrap_content])测出来=107px,107px 并没有超过1980px(允许的最大高度),所以实际测量出来TextView的高度是107px。

最终算出id/text 的mMeasureWidth=1440px,mMeasureHeight=107px。

贴一下布局代码,免得你忘了具体布局。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:id=“@+id/linear”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:layout_marginTop=“50dp”

android:background=“@android:color/holo_blue_dark”

android:paddingBottom=“70dp”

android:orientation=“vertical”>

<TextView

android:id=“@+id/text”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:background=“@color/material_blue_grey_800”

android:text=“TextView”

android:textColor=“@android:color/white”

android:textSize=“20sp” />

<View

android:id=“@+id/view”

android:layout_width=“match_parent”

android:layout_height=“150dp”

android:background=“@android:color/holo_green_dark” />

TextView的高度已经测量出来了,接下来测量id/linear的第二个child(id/view),同样的原理测出id/view的MeasureSpec.

id/view的MeasureSpec 计算出来后,调用view.measure(childWidthMeasureSpec, childHeightMeasureSpec)的测量id/view的高宽,之前已经说过View measure的默认实现是

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(

getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

最终算出id/view的mMeasureWidth=1440px,mMeasureHeight=600px。

id/linear 的子View的高度都计算完毕了,接下来id/linear就通过所有子View的测量结果计算自己的高宽,id/linear是LinearLayout,所有它的高度计算简单理解就是子View的高度的累积+自己的Padding.

最终算出id/linear的mMeasureWidth=1440px,mMeasureHeight=987px。

最终算出id/linear出来后,id/content 就要根据它唯一的子View id/linear 的测量结果和自己的之前算出的MeasureSpec一起来测量自己的结果,具体计算的逻辑去看FrameLayout onMeasure 函数的计算过程。以此类推,接下来测量ViewRoot,然后再测量id/statusBarBackground,虽然不知道id/statusBarBackground 是什么,但是调试的过程中,测出的它的高度=100px, 和 id/content 的paddingTop 刚好相等。在最后测量DecorView 的高宽,最终整个测量过程结束。所有的View的大小测量完毕。所有的getMeasureWidth 和 getMeasureWidth 都已经有值了。Measure 分析到此为止,如有不懂,评论留言(简书:kelin)

layout过程#

============

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

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

performTraversals 方法执行完mView.measure 计算出mMeasuredXXX后就开始执行layout 函数来确定View具体放在哪个位置,我们计算出来的View目前只知道view矩阵的大小,具体这个矩阵放在哪里,这就是layout 的工作了。layout的主要作用 :根据子视图的大小以及布局参数将View树放到合适的位置上。

既然是通过mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); 那我们来看下layout 函数做了什么,mView肯定是个ViewGroup,不会是View,我们直接看下ViewGroup 的layout函数

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

if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {

if (mTransition != null) {

mTransition.layoutChange(this);

}

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

} else {

// record the fact that we noop’d it; request layout when transition finishes

mLayoutCalledWhileSuppressed = true;

}

}

代码可以看个大概,LayoutTransition是用于处理ViewGroup增加和删除子视图的动画效果,也就是说如果当前ViewGroup未添加LayoutTransition动画,或者LayoutTransition动画此刻并未运行,那么调用super.layout(l, t, r, b),继而调用到ViewGroup中的onLayout,否则将mLayoutSuppressed设置为true,等待动画完成时再调用requestLayout()。

这个函数是final 不能重写,所以ViewGroup的子类都会调用这个函数,layout 的具体实现是在super.layout(l, t, r, b)里面做的,那么我接下来看一下View类的layout函数

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

//设置View位于父视图的坐标轴

boolean changed = setFrame(l, t, r, b);

//判断View的位置是否发生过变化,看有必要进行重新layout吗

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

if (ViewDebug.TRACE_HIERARCHY) {

ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);

}

//调用onLayout(changed, l, t, r, b); 函数

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

mPrivateFlags &= ~LAYOUT_REQUIRED;

}

mPrivateFlags &= ~FORCE_LAYOUT;

}

1、setFrame(l, t, r, b) 可以理解为给mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了,这几个值构成的矩形区域就是该View显示的位置,这里的具体位置都是相对与父视图的位置。

2、回调onLayout,对于View来说,onLayout只是一个空实现,一般情况下我们也不需要重载该函数,:

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

}

对于ViewGroup 来说,唯一的差别就是ViewGroup中多了关键字abstract的修饰,要求其子类必须重载onLayout函数。

@Override

protected abstract void onLayout(boolean changed,

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

而重载onLayout的目的就是安排其children在父视图的具体位置,那么如何安排子View的具体位置呢?

int childCount = getChildCount() ;

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

View child = getChildAt(i) ;

//整个layout()过程就是个递归过程

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

}

代码很简单,就是遍历自己的孩子,然后调用 child.layout(l, t, r, b) ,给子view 通过setFrame(l, t, r, b) 确定位置,而重点是(l, t, r, b) 怎么计算出来的呢。还记得我们之前测量过程,测量出来的MeasuredWidth和MeasuredHeight吗?还记得你在xml 设置的Gravity吗?还有RelativeLayout 的其他参数吗,没错,就是这些参数和MeasuredHeight、MeasuredWidth 一起来确定子View在父视图的具体位置的。具体的计算过程大家可以看下最简单FrameLayout 的onLayout 函数的源码,每个不同的ViewGroup 的实现都不一样,这边不做具体分析了吧。

3、MeasuredWidth和MeasuredHeight这两个参数为layout过程提供了一个很重要的依据(如果不知道View的大小,你怎么固定四个点的位置呢),但是这两个参数也不是必须的,layout过程中的4个参数l, t, r, b完全可以由我们任意指定,而View的最终的布局位置和大小(mRight - mLeft=实际宽或者mBottom-mTop=实际高)完全由这4个参数决定,measure过程得到的mMeasuredWidth和mMeasuredHeight提供了视图大小测量的值,但我们完全可以不使用这两个值,所以measure过程并不是必须的。如果我们不使用这两个值,那么getMeasuredWidth() 和getWidth() 就很有可能不是同一个值,它们的计算是不一样的:

public final int getMeasuredWidth() {

return mMeasuredWidth & MEASURED_SIZE_MASK;

}

public final int getWidth() {

return mRight - mLeft;

}

layout 过程相对简单些,分析就到此为止。

draw过程#

=======

performTraversals 方法的下一步就是mView.draw(canvas); 因为View的draw 方法一般不去重写,官网文档也建议不要去重写draw 方法,所以下一步执行就是View.java的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

background.draw(canvas);

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

// Step 2, save the canvas’ layers

if (solidColor == 0) {

final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

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

最后

考虑到文章的篇幅问题,我把这些问题和答案以及我多年面试所遇到的问题和一些面试资料做成了PDF文档

喜欢的朋友可以关注、转发、点赞 感谢!

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

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

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

ground, if needed

background.draw(canvas);

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

// Step 2, save the canvas’ layers

if (solidColor == 0) {

final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

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

最后

考虑到文章的篇幅问题,我把这些问题和答案以及我多年面试所遇到的问题和一些面试资料做成了PDF文档

[外链图片转存中…(img-8rOm8Bkd-1714990713867)]

[外链图片转存中…(img-DsuYAAvF-1714990713868)]

喜欢的朋友可以关注、转发、点赞 感谢!

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

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

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

  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值