一篇文章搞定《Android中View的绘制流程》

本文前言

像事件的分发一样,View的绘制流程我也分成了三部分来讲
分别为:1、怎样到达ViewRootImpl。2、到达ViewRootImpl做了什么。3、View的最终绘制
之后我会利用一个自定义View的实例来反刍这篇文章,像《一篇文章搞定搞定事件分发》一样。

怎样到达ViewRootImpl

过程如下:

1、首先View是在Activity的onCreate阶段通过setContentView解析xml加载进去的。也就是View是在OnCreate阶段加到缓存中的。
2、加在哪个缓存中了呢:这个View对象会被添加到ActivityThread的ActivityClientRecord对象中(后面可以看到取出来添加到DecorView中),并放入ActivityThread的mActivities集合中缓存当前Activity对象的状态信息。
3、ActivityThread.handleResumeActivity()是在Activity的生命周期中的onResume阶段调用的,用于将Activity设置为resumed状态。
4、ActivityThread.handleResumeActivity()会调用WindowManager的addView()方法来把Activity的Window视图添加到窗口上。源码如下:

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, String reason) {
        .....
        final Activity a = r.activity;
        if (r.window == null && !a.mFinished && willBeVisible) {
            .....
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            if (!a.mWindowAdded) {
                ......
                a.mWindowAdded = true;
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
    }
}

5、可以看到ActivityThread.handleResumeActivity()方法中,会先通过Activity的getWindow()方法获取到当前Activity的Window,设置相关的Window参数,接着调用WindowManager的addView方法将Window的视图加入到窗口中。
6、这里面ActivityClientRecord只是Activity的一个信息类,记录一下Activity的状态、所属进程等相关信息。
7、WindowManager实现类为WindowManagerImpl,WindowManagerImpl中addView()方法又会调用WindowManagerGlobal的addView()方法。源码如下:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ······
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    //即将开始流程绘制    
    root.setView(view, wparams, panelParentView);
    ·······
}

8、可以看到他会创建出我们熟知的ViewRootImpl。并将我们的View Set给ViewRootImpl进行管理。
上面的这个过程可以理解为Android ManagerService的功劳,通过管理Activity的生命周期从而加载我们的View。

流程图小结:

在这里插入图片描述

到达ViewRootImpl做了什么

像事件的分发一样,最终View都交给了ViewRootImpl来进行管理。这也是ViewRootImpl代码上万行的原因。经过不断的迭代,View的体积已经很庞大了。这也许是Jetpack Compose出现的原因吧,来替换掉这庞大的View体系,毕竟庞大就难以维护。
下面看看ViewRootImpl对View的绘制做了哪些关键的步骤。

第一步:setView()

首先上面讲到了ViewRootImpl的setView。那setView做了什么呢?
我们知道ViewRootImpl中的mView就是我们的根布局DecorView这个在事件的分发中也有提到。这里再说一遍吧:
其实上面认真读代码的人已经发现了,就在第一步handleResumeActivity方法中。我们先是创建了window之后获取了decor,并通过windowsManager的wm.addView(decor, l)设置给了下一步。最终到达ViewRootImpl。
下面是ViewRootImpl的setView方法。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
     if (mView == null) {
        mView = view;
         // Schedule the first layout -before- adding to the window
         // manager, to make sure we do the relayout before receiving
         // any other events from the system.
         requestLayout();
    }
}

可以看到ViewRootImpl首先将window给他的view和自己的mView进行了绑定。毕竟自己家的小孩才能管,对吧。

第二步:performTraversals()

下一步就来到了View绘制的关键方法performTraversals()。
上面可以看到执行了requestLayout()方法,requestLayout()方法中会异步执行performTraversals()方法,View的三大流程都是在该方法中执行的。

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

到这儿我们算是明白View的绘制流程是从哪儿开始的,接下来分析这个过程到底是怎么做的。
直接简化一下performTraversals的代码便于理解(简化把----嘻嘻)

private void performTraversals() {

    //计算DecorView根View的MeasureSpecint childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    performLayout(lp, mWidth, mHeight);

    performDraw();
}

真正的奥秘就在performMeasure()、performLayout()、performDraw()大家看着熟悉吗是不是想到了,说烂嘴的onMeasure()、onLayout()、onDraw()方法。
没错昂、看来你很有学习Android的脑瓜。他最终就调用了我们的mView也就是DecorView的三大绘制方法。

第三步:DecorView中的Measure()、Layout()、Draw()

让我们来看一下:
performMeasure()

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

performLayout()

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
        final View host = mView;
        if (host == null) {
            return;
        }
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

performDraw()
这个方法中没有直接调用mView的Draw。是先调用draw方法从而调用drawSoftware。最后还是一样会调用Draw。
源码如下:都被简化的了方便理解

private boolean performDraw() {
    try {
        boolean canUseAsync = draw(fullRedrawNeeded, usingAsyncReport && mSyncBuffer);
        ....
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
    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) {
    try {
        ....
        mView.draw(canvas);
        ....
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}  

最后可以看到调用了mView的Measure、Layout、Draw方法。
我们点进去发现,在View的Measure、Layout、Draw会调用我们熟知的onMeasure()、onLayout()、onDraw()方法。这个源码就不粘贴出来了。大家可以点进去看一下。

View的最终绘制

所谓View的最终绘制就是。简单来说就是View是如果通过onMeasure()、onLayout()、onDraw(),最终绘制的界面上的。
下面会对这三个重要方法进行知识解析,随后我会以两篇自定义View的文章去用例子来反刍这个知识点。

View的层级和View树

1、首先我们之前有说到我们Set的XML布局是我们的DecorView。这个前面已经证实过了。
2、通过源码我们还知道DecorView是存放在Window中的,而PhoneWindow是Window唯一的实现类。
3、每个Activity都有一个对应的Window。当一个Activity启动时,它会创建一个Window实例。所以说Activity包含着Window
所以我们基础的层级为Activity->PhoneWindow->DecorView
其次就是我们Set的布局了(这个就不细说了,我们的xml布局其实就是ViewGroup和View的组合组成的树状的View)
盗的树的层级图如下:
在这里插入图片描述
这边只讲这三个方法的定义和理论、下一篇会结合实战来讲。放在这一篇太冗余了。
首先要知道这三个是做什么的,简单来说就是,画多大、画在哪里、画什么。
之后三个方法都会以:什么作用?注意点!怎么用!三个方面来说一下。
ps:这里定义看着不太理解也没关系,读一遍之后打开下一篇文字对照着去走一遍自定义View的例子,就很明确了,不然谁来看定义都是比较懵的。

onMeasure()

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

什么作用:

他会测量我们View树中所有View的宽和高,以DecorView根View为起点,遍历计算所有孩子View的大小。从而计算每个控件的大小进行绘制我们的布局。
主要分为两种情况如下

有可能是用父布局来确定子View大小:这里既然都需要来确认子布局的大小,那么其实只有我们的子View是wrap_content需要我们来次测量并设置我们View的大小。

有可能是用子View大小来确定父布局大小:那么我们其实就需要遍历所有的子View大小,从而计算我们的父View大小。既然是通过一堆的View确定那么我们的父View其实大多数就是以ViewGroup的形式出现的。

注意什么:

那就不得不说他的MeasureSpec参数了,所有的测量相关内容都在这个参数中。可以看到onMeasure函数中的两个参数widthMeasureSpec和heightMeasureSpec。那么他们代表什么呢?有什么作用。
MeasureSpec可以理解为是View的测量依据,它是系统将View的参数根据父容器的规则转换而成的,之后根据它来测量出View的宽和高。
可以看到他为一个Int值他存储着父View的测量模式和测量大小。
那么是怎么存储的呢?
他是利用了Int为32bit位来去存储相关内容的。
在这里插入图片描述
首先测量模式为三种:

模式作用
EXACTLY精确模式00表示父容器已经检测出View所需要的精确大小。这时View的最终大小就是SpecSize的值,它对应于View参数中的match_parent和具体大小数值这两种模式。
AT_MOST最大模式01表示父容器指定了一个可用大小的数值,记录在SpecSize中,View的大小不能大于它,但具体的值还是看View的具体实现。它对应于View参数中的wrap_content。该模式下父控件无法确认子View的尺寸,只能通过子控件的情况来确定大小。
UNSPECIFIED未指定模式11父容器不对View有任何限制,给它想要的任何尺寸。自定义View一般用不到,一般用于系统中的一些控件比如ListView、ScrollView。

怎么用:

主要API,最常用的就是这三个API了。完整例子在下一篇整出来。

// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)

// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)

// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

比如如果我们设置了wrapcontent,不想使用父类的剩余宽高,我们可以在onMeasure做判断,通过setMeasureDimension然后自己设置宽高。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val width = MeasureSpec.getSize(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(200, 200)
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(100, height)
    } else if (heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width, 100)
    }
}

问:上面讲了那么久MeasureSpec,那么MeasureSpec值到底是如何计算得来?
答:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。
总结来说所有的View都是由父View的MeasureSpec和view本身的LayoutParams来决定。

onLayout()

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
}

什么作用:

用于设置子View在父View中的布局位置,以及设置子View的大小等相关布局信息。
当一个View的onLayout方法被调用时,表示该View已经完成了测量阶段的工作,并且所有的子View都已经完成了测量和布局工作,此时可以计算出子View在父View中的位置和大小。
其中他需要调用layout用来确定自身位置,自身位置确定后,调用onlayout确定子view的位置。
也就是说layout方法确定view本身的位置,onLayout确定所有子元素的位置。

注意什么:

上面说到layout方法确定view本身的位置,当确认之后会调用onLayout。那么就是说当回调onLayout时,他的的坐标已经是确认的了。
那么他的onLayout的四个位置参数即为当前View在父布局中的位置和大小,具体如下:

left - 当前View的左侧边缘在父布局中的X坐标,相对于父View的左边距离(单位为像素)。
top -当前View的顶部边缘在父布局中的Y坐标。
right - 当前View的右侧边缘在父布局中的X坐标。
bottom - 当前View的底部边缘在父布局中的Y坐标。

这些参数表示了当前View的位置和大小,并且相对于父布局的坐标系。
事实上,这些位置参数可以用于计算当前View的其他相关属性和位置,如宽度、高度、中心点坐标等。
因此,onLayout方法的四个参数也可以称为位置和大小参数。
所以要熟悉我们的坐标系:直接上图(屏幕坐标系和视图坐标系)
在这里插入图片描述
View获取自身宽高

  • getHeight():获取View自身高度
  • getWidth():获取View自身宽度

获得View到其父控件(ViewGroup)的距离

  • getTop():获取View自身顶边到其父布局顶边的距离
  • getLeft():获取View自身左边到其父布局左边的距离
  • getRight():获取View自身右边到其父布局左边的距离
  • getBottom():获取View自身底边到其父布局顶边的距离

MotionEvent提供的方法(点击事件需要的坐标,蓝色圆圈为点击点)

  • getX():获取点击事件距离控件左边的距离,即视图坐标
  • getY():获取点击事件距离控件顶边的距离,即视图坐标
  • getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
  • getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标

怎么用:

小例子:获取宽与高的最大值用于设置正方形 View 的边长

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        // 使用宽高的最大值设置边长
        var width = right - left
        var height = bottom - top
        var size = Math.max(width, height);
        super.layout(left, top, left + size, top + size)
}

onDraw()

这个在后面实战中细讲,想对它有个概念理解。

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
}

什么作用:

Draw过程比较简单,主要作用是将View绘制到屏幕上面。
他的过程是:

  1. 绘制背景background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispathDraw)
  4. 绘制装饰(onDrawScrollBars)

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会便利调用所有子元素的draw方法,如此draw事件就被一层层的传递下去了。

注意什么:

1、不要在onDraw方法中进行耗时操作,否则会导致界面卡顿,影响用户体验。
2. 不要在onDraw方法中创建对象,尽量复用已有对象。因为onDraw在绘制过程中会频繁的调用,会造成JVM不断的创建、回收对象。从而造成内存抖动,进而卡顿。
3. 如果需要在View外部修改View的属性,比如设置背景颜色等,最好在View的构造函数中设置。不然会不起作用。

怎么用:

在onDraw方法中使用Canvas对象绘制需要的图形
在需要更新View的时候,调用View的invalidate方法,触发系统重新调用onDraw方法。

class CustomView : View {
    private var canvas : Canvas? = null
    private lateinit var paint : Paint
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr){
        //初始化
        canvas = Canvas() //< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
        paint = Paint()
        paint.color = Color.parseColor("#F50808")
    }


    override fun onDraw(canvas: Canvas) {
        //画一个圆
        canvas.drawCircle(100f, 100f, 50f, paint)
    }
}

总结

总结就是文章内容过多,分批阅读。自定义View需要多实战。
虽然大多数时候我们都是copy其他人的自定义View去修改。但是想要提升自己,还是一定要去自己去实现一些自定义View的
后面会更新1-2篇的自定义View的实战。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值