Ⅰ android UI | View 的绘制流程详解
上一篇文章讲解了,从setContentView方法到了解View是如何绘制的: 传送门
在这篇博客讲述了, 在ViewRootImpl类中performTraversals方法中具体的绘制过程,其中里面就有 performMeasure()、performLayout()、performDraw() 三个方法的调用, 那么要了解View 的测量、布局、绘制,就分别跟这三个方法有关系。
先来看看performMeasure()方法的调用过程
先看performMeasure方法,这个方法有两个参数,都是通过getRootMeasureSpec()方法计算得到
这里有一个关键类MeasureSpec,在这里需要了解下这个类的原理。
这里要感谢 这位博主 ,他讲述的很清晰,我自己动手测算了,很容易理解。大概就是用一个数字通过高位记录Mode,地位记录size的方式,记录两个数据,都是通过一个掩码做位移运算得来。就是说这个变量(measureSpec)的值可以通过掩码分别得到测量mode 和 测量size。
继续查看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);方法:
这里mView是DecorView对象,那么他调用的实际上是View的measure方法,查询DecorView和FrameLayout都没有measure方法,所以他调用的是View的measure方法
DecorView.onMeasure()方法如下:
FrameLayout.onMeasure方法如下:(这个方法里面都很重要)
我们先看 , measureChildWithMargins 方法,
getChildMeasureSpec 方法内容如下:
那么假如我们写的布局根节点是LinearLayout,那么就会在执行到View.measure方法里面的onMeasure方法时,就会调用到LinearLayout.onMeasure方法,具体内容如下:
通过源码可以看到,还是会循环调用子View , 就这样循环递归的测量完最里面的一个view,这个过程中onMeasure方法可能会被多次执行。
还是从ViewRootImpl.performTraversals开始
这里跟measure 调用流程其实一样,DecorView和FrameLayout没有重写layout方法,所以调用的是View.layout方法,
由于当前对象是decorView,所以调用的是DecorView.onLayout方法:
FrameLayout.onLayout 方法如下:
继续查看 View.draw方法
在这个方法里面,所有重要的方法都在里面,onDraw、dispatchDraw 等等都在里面,我看了下这个方法里面都挺重要就没删减,也都能看得懂。
到此View的整个绘制流程就搞清楚了。
关于子view测量
1、不管父View是何模式,若子View有确切数值,则子View大小就是其本身大小,且mode是EXACTLY
2、若子View是match_parent,则模式与父View相同,且大小同父View(若父View是UNSPECIFIED,则子View大小为0)
3、若子View是wrap_content,则模式是AT_MOST,大小同父View,表示不可超过父View大小(若父View是UNSPECIFIED,则子View大小为0)
关于绘制流程
我们自定义的view,基本上只需要重写 onMeasure、onLayout、onDraw即可
Ⅱ Carson带你学Android:你真的了解view.post()吗
为什么view.post()能保证获取到view的宽高?
View.post()的原理: 以Handler为基础,View.post() 将传入任务添加到 View绘制任务所在的消息队列尾部,从而保证View.post() 任务的执行时机是在View 绘制任务完成之后的。 其中,几个关键点:
所以:
具体源码分析请看: Android:为什么view.post()能保证获取到view的宽高?
为什么onCreate()使用view.post()无法立刻执行任务(如获取宽高),需要在onResume()后才可获取?
在onCreate()时,AttachInfo还没被赋值(为null)(是在view.dispatchAttachedToWindow()才被赋值),所亮物猜以会走下述源码的过程2;通过上面分析,此过程的作用仅是:保存了通过post()添加的任务,并没执行。
若只是创建一个 View & 调用它的post(),那么post的任务会不会被执行?
不会。主要原因是:
每个View中post() 需执行的任务,必须得添加到窗口视图-执行绘制流程 - 任务才会被post到消息队列里去等待执行,即依赖于dispatchAttachedToWindow ();
若View未添加到窗口视图,那么就不会走绘制流程,post() 添加的任务最终不会被post到消息队列里,即得不到执行。(但会保存到HandlerAction数组里)
上述例子,因为它没有被添加到窗口视图,所以不会走绘制流程,所以该任务最终不会被post到消息队列里 & 执行
此时只需要添加将View添加到窗口,那么post()的任务即可蚂租被执行
view.pos()传入的任务被执行的有效期是多久?
在整个 Activity 的生命周期内都可以正常使用 View.post() 任务
任务被执行是构造AttachInfo,所敬型以任务释放即时释放AttachInfo (置为null)。而AttachInfo 的释放操作(置为null)是在 Activity 生命周期 onDestory 方法之后
下面,我们将分析,什么时候调用上述入口,即DecorView.dispatchDetachedFromWindow();
此时需从 将DecorView从WindowManager中移除 开始讲起:移除 Window 窗口任务是通过 ActivityThread.handleDestoryActivity()完成。
View.post() 任务被执行的有效期是在 Activity 生命周期 onDestory()后。本质是追踪AttachInfo的释放过程(置为null)
AttachInfo的释放过程是在 将DecorView从WindowManager中移除时:回调DecorView.dispatchDetachedFromWindow(),其具体行为是:
而上述过程是在ActivityThread.handleDestoryActivity()中回调 Activity.onDestory()之后。
至此,关于view.post()的四大常见疑问 (坑)内容讲解完毕。
不定期分享关于 安卓开发 的干货,追求 短、平、快 ,但 却不缺深度 。
Ⅲ Android 重学系列 View的绘制流程(六) 硬件渲染(上)
本文开始聊聊Android中的硬件渲染。如果跟着我的文章顺序,从SF进程到App进程的绘制流程一直阅读,我们到这里已经有了一定的基础,可以试着进行横向比对如Chrome浏览器渲染流程,看看软件渲染,硬件渲染,SF合成都做了什么程度的优化。
先让我们回顾一下负责硬件渲染的主体对象ThreadedRenderer在整个绘制流程中做了哪几个步骤。
在硬件渲染的过程中,有一个很核心的对象RenderNode,作为每一个View绘制的节点对象。
当每一次进行准备进行绘制的时候,都会雷打不动执行如下三个步骤:
如果遇到什么问题欢迎来到 https://www.jianshu.com/p/c84bfa909810 下进行讨论
实际上整个硬件渲染的设计还是比较庞大。因此本文先聊聊ThreadedRender整个体系中主要对象的构造以及相关的原理。
首先来认识下面几个重要的对象有一个大体的印象。
在java层中面向Framework中,只有这么多,下面是一一映射的简图。
能看到实际上RenderNode也会跟着View 树的构建同时一起构建整个显示层级。也是因此ThreadedRender也能以RenderNode为线索构建出一套和软件渲染一样的渲染流程。
仅仅这样?如果只是这么简单,知道我习惯的都知道,我喜欢把相关总结写在最后。如果把总揽写在正文开头是因为设计比较繁多。因为我们如果以流水线的形式进行剖析容易造成迷失细节的困境。
让我继续介绍一下,在硬件渲染中native层的核心对象。
如下是一个思维导图:
有这么一个大体印象后,就不容易迷失在源码中。我们先来把这些对象的实例化以及上面列举的ThreadedRenderer在ViewRootImpl中执行行为的顺序和大家来聊聊其原理,先来看看ThreadedRenderer的实例化。
当发现mSurfaceHolder为空的时候会调用如下函数:
而这个方法则调用如下的方法对ThreadedRenderer进行创建:
文件:/ frameworks / base / core / java / android / view / ThreadedRenderer.java
能不能创建的了ThreadedRenderer则决定于全局配置。如果ro.kernel.qemu的配置为0,说明支持OpenGL 则可以直接返回true。如果qemu.gles为-1说明不支持OpenGL es返回false,只能使用软件渲染。如果设置了qemu.gles并大于0,才能打开硬件渲染。
我们能看到ThreadedRenderer在初始化,做了三件事情:
关键是看1-3点中ThreadRenderer都做了什么。
文件:/ frameworks / base / core / jni / android_view_ThreadedRenderer.cpp
能看到这里是直接实例化一个RootRenderNode对象,并把指针的地址直接返回。
能看到RootRenderNode继承了RenderNode对象,并且保存一个JavaVM也就是我们所说的Java虚拟机对象,一个java进程全局只有一个。同时通过getForThread方法,获取ThreadLocal中的Looper对象。这里实际上拿的就是UI线程的Looper。
在这个构造函数有一个mDisplayList十分重要,记住之后会频繁出现。接着来看看RenderNode的头文件:
文件:/ frameworks / base / libs / hwui / RenderNode.h
实际上我把几个重要的对象留下来:
文件:/ frameworks / base / core / java / android / view / RenderNode.java
能看到很简单,就是包裹一个native层的RenderNode返回一个Java层对应的对象开放Java层的操作API。
能看到这个过程生成了两个对象:
这个对象实际上让RenderProxy持有一个创建动画上下文的工厂。RenderProxy可以通过ContextFactoryImpl为每一个RenderNode创建一个动画执行对象的上下文AnimationContextBridge。
文件:/ frameworks / base / libs / hwui / renderthread / RenderProxy.cpp
在这里有几个十分重要的对象被实例化,当然这几个对象在聊TextureView有聊过( SurfaceView和TextureView 源码浅析 ):
我们依次看看他们初始化都做了什么。
文件:/ frameworks / base / libs / hwui / renderthread / RenderThread.cpp
能看到其实就是简单的调用RenderThread的构造函数进行实例化,并且返回对象的指针。
RenderThread是一个线程对象。先来看看其头文件继承的对象:
文件:/ frameworks / base / libs / hwui / renderthread / RenderThread.h
其中RenderThread的中进行排队处理的任务队列实际上是来自ThreadBase的WorkQueue对象。
文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h
ThreadBase则是继承于Thread对象。当调用start方法时候其实就是调用Thread的run方法启动线程。
另一个更加关键的对象,就是实例化一个Looper对象到WorkQueue中。而直接实例化Looper实际上就是新建一个Looper。但是这个Looper并没有获取当先线程的Looper,这个Looper做什么的呢?下文就会揭晓。
WorkQueue把一个Looper的方法指针设置到其中,其作用可能是完成了某一件任务后唤醒Looper继续工作。
而start方法会启动Thread的run方法。而run方法最终会走到threadLoop方法中,至于是怎么走进来的,之后有机会会解剖虚拟机的源码线程篇章进行讲解。
在threadloop中关键的步骤有如下四个:
在这个过程中创建了几个核心对象:
另一个核心的方法就是,这个方法为WorkQueue的Looper注册了监听:
能看到在这个Looper中注册了对DisplayEventReceiver的监听,也就是Vsync信号的监听,回调方法为displayEventReceiverCallback。
我们暂时先对RenderThread的方法探索到这里,我们稍后继续看看回调后的逻辑。
文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h
能看到这里的逻辑很简单实际上就是调用Looper的pollOnce方法,阻塞Looper中的循环,直到Vsync的信号到来才会继续往下执行。详细的可以阅读我写的 Handler与相关系统调用的剖析 系列文章。
文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h
实际上调用的是WorkQueue的process方法。
文件:/ frameworks / base / libs / hwui / thread / WorkQueue.h
能看到这个过程中很简单,几乎和Message的loop的逻辑一致。如果Looper的阻塞打开了,则首先找到预计执行时间比当前时刻都大的WorkItem。并且从mWorkQueue移除,最后添加到toProcess中,并且执行每一个WorkItem的work方法。而每一个WorkItem其实就是通过从某一个压入方法添加到mWorkQueue中。
到这里,我们就明白了RenderThread中是如何消费渲染任务的。那么这些渲染任务又是哪里诞生呢?
上文聊到了在RenderThread中的Looper会监听Vsync信号,当信号回调后将会执行下面的回调。
能看到这个方法的核心实际上就是调用drainDisplayEventQueue方法,对ui渲染任务队列进行处理。
能到在这里mVsyncRequested设置为false,且mFrameCallbackTaskPending将会设置为true,并且调用queue的postAt的方法执行ui渲染方法。
还记得queue实际是是指WorkQueue,而WorkQueue的postAt方法实际实现如下:
/ frameworks / base / libs / hwui / thread / WorkQueue.h
情景带入,当一个Vsync信号达到Looper的监听者,此时就会通过WorkQueue的drainDisplayEventQueue 压入一个任务到队列中。
每一个默认的任务都是执行dispatchFrameCallback方法。这里的判断mWorkQueue中是否存在比当前时间更迟的时刻,并返回这个WorkItem。如果这个对象在头部needsWakeup为true,说明可以进行唤醒了。而mWakeFunc这个方法指针就是上面传下来:
把阻塞的Looper唤醒。当唤醒后就继续执行WorkQueue的process方法。也就是执行dispatchFrameCallbacks方法。
在这里执行了两个事情:
先添加到集合中,在上面提到过的threadLoop中,会执行如下逻辑:
如果大小不为0,则的把中的IFrameCallback全部迁移到mFrameCallbacks中。
而这个方法什么时候调用呢?稍后就会介绍。其实这部分的逻辑在TextureView的解析中提到过。
接下来将会初始化一个重要对象:
这个对象名字叫做画布的上下文,具体是什么上下文呢?我们现在就来看看其实例化方法。
文件:/ frameworks / base / libs / hwui / renderthread / CanvasContext.cpp
文件:/ device / generic / goldfish / init.ranchu.rc
在init.rc中默认是opengl,那么我们就来看看下面的逻辑:
首先实例化一个OpenGLPipeline管道,接着OpenGLPipeline作为参数实例化CanvasContext。
文件:/ frameworks / base / libs / hwui / renderthread / OpenGLPipeline.cpp
能看到在OpenGLPipeline中,实际上就是存储了RenderThread对象,以及RenderThread中的mEglManager。透过OpenGLPipeline来控制mEglManager进而进一步操作OpenGL。
做了如下操作:
文件:/ frameworks / base / libs / hwui / renderstate / RenderState.cpp
文件:/ frameworks / base / libs / hwui / renderthread / DrawFrameTask.cpp
实际上就是保存这三对象RenderThread;CanvasContext;RenderNode。
文件:/ frameworks / base / core / jni / android_view_ThreadedRenderer.cpp
能看到实际上就是调用RenderProxy的setName方法给当前硬件渲染对象设置名字。
文件:/ frameworks / base / libs / hwui / renderthread / RenderProxy.cpp
能看到在setName方法中,实际上就是调用RenderThread的WorkQueue,把一个任务队列设置进去,并且调用runSync执行。
能看到这个方法实际上也是调用post执行排队执行任务,不同的是,这里使用了线程的Future方式,阻塞了执行,等待CanvasContext的setName工作完毕。
Ⅳ Android UI绘制之View绘制的工作原理
这是AndroidUI绘制流程分析的第二篇文章,主要分析界面中View是如何绘制到界面上的具体过程。
ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
measure 过程决定了 View 的宽/高, Measure 完成以后,可以通过 getMeasuredWidth 和 getMeasuredHeight 方法来获取 View 测量后的宽/高,在几乎所有的情况下,它等同于View的最终的宽/高,但是特殊情况除外。 Layout 过程决定了 View 的四个顶点的坐标和实际的宽/高,完成以后,可以通过 getTop、getBottom、getLeft 和 getRight 来拿到View的四个顶点的位置,可以通过 getWidth 和 getHeight 方法拿到View的最终宽/高。 Draw 过程决定了 View 的显示,只有 draw 方法完成后 View 的内容才能呈现在屏幕上。
DecorView 作为顶级 View ,一般情况下,它内部会包含一个竖直方向的 LinearLayout ,在这个 LinearLayout 里面有上下两个部分,上面是标题栏,下面是内容栏。在Activity中,我们通过 setContentView 所设置的布局文件其实就是被加到内容栏中的,而内容栏id为 content 。可以通过下面方法得到 content:ViewGroup content = findViewById(R.android.id.content) 。通过 content.getChildAt(0) 可以得到设置的 view 。 DecorView 其实是一个 FrameLayout , View 层的事件都先经过 DecorView ,然后才传递给我们的 View 。
MeasureSpec 代表一个32位的int值,高2位代表 SpecMode ,低30位代表 SpecSize , SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。
SpecMode 有三类,如下所示:
UNSPECIFIED
EXACTLY
AT_MOST
LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。
对于顶级View,即DecorView和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定;
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;
MeasureSpec一旦确定,onMeasure就可以确定View的测量宽/高。
小结一下
当子 View 的宽高采用 wrap_content 时,不管父容器的模式是精确模式还是最大模式,子 View 的模式总是最大模式+父容器的剩余空间。
View 的工作流程主要是指 measure 、 layout 、 draw 三大流程,即测量、布局、绘制。其中 measure 确定 View 的测量宽/高, layout 确定 view 的最终宽/高和四个顶点的位置,而 draw 则将 View 绘制在屏幕上。
measure 过程要分情况,如果只是一个原始的 view ,则通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量过程外,还会遍历调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程。
如果是一个原始的 View,那么通过 measure 方法就完成了测量过程,在 measure 方法中会去调用 View 的 onMeasure 方法,View 类里面定义了 onMeasure 方法的默认实现:
先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源码:
可以看到, getMinimumWidth 方法获取的是 Drawable 的原始宽度。如果存在原始宽度(即满足 intrinsicWidth > 0),那么直接返回原始宽度即可;如果不存在原始宽度(即不满足 intrinsicWidth > 0),那么就返回 0。
接着看最重要的 getDefaultSize 方法:
如果 specMode 为 MeasureSpec.UNSPECIFIED 即未指定模式,那么返回由方法参数传递过来的尺寸作为 View 的测量宽度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精确模式,那么返回从 measureSpec 中取出的 specSize 作为 View 测量后的宽度和高度。
看一下刚才的表格:
当 specMode 为 EXACTLY 或者 AT_MOST 时,View 的布局参数为 wrap_content 或者 match_parent 时,给 View 的 specSize 都是 parentSize 。这会比建议的最小宽高要大。这是不符合我们的预期的。因为我们给 View 设置 wrap_content 是希望View的大小刚好可以包裹它的内容。
因此:
如果是一个 ViewGroup,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行 measure 过程。
ViewGroup 并没有重写 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 这几个方法专门用于测量子元素。
如果是 View 的话,那么在它的 layout 方法中就确定了自身的位置(具体来说是通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft , mRight , mTop , mBottom 这四个值), layout 过程就结束了。
如果是 ViewGroup 的话,那么在它的 layout 方法中只是确定了 ViewGroup 自身的位置,要确定子元素的位置,就需要重写 onLayout 方法;在 onLayout 方法中,会调用子元素的 layout 方法,子元素在它的 layout 方法中确定自己的位置,这样一层一层地传递下去完成整个 View 树的 layout 过程。
layout 方法的作用是确定 View 本身的位置,即设定 View 的四个顶点的位置,这样就确定了 View 在父容器中的位置;
onLayout 方法的作用是父容器确定子元素的位置,这个方法在 View 中是空实现,因为 View 没有子元素了,在 ViewGroup 中则进行抽象化,它的子类必须实现这个方法。
1.绘制背景( background.draw(canvas); );
2.绘制自己( onDraw );
3.绘制 children( dispatchDraw(canvas) );
4.绘制装饰( onDrawScrollBars )。
dispatchDraw 方法的调用是在 onDraw 方法之后,也就是说,总是先绘制自己再绘制子 View 。
对于 View 类来说, dispatchDraw 方法是空实现的,对于 ViewGroup 类来说, dispatchDraw 方法是有具体实现的。
通过 dispatchDraw 来传递的。 dispatchDraw 会遍历调用子元素的 draw 方法,如此 draw 事件就一层一层传递了下去。dispatchDraw 在 View 类中是空实现的,在 ViewGroup 类中是真正实现的。
如果一个 View 不需要绘制任何内容,那么就设置这个标记为 true,系统会进行进一步的优化。
当创建的自定义控件继承于 ViewGroup 并且不具备绘制功能时,就可以开启这个标记,便于系统进行后续的优化;当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要关闭这个标记。
参考:《Android开发艺术探索》
Ⅳ Android ViewRootImpl
本文主要分析两个问题:
1、为什么View 的绘制流程是从 ViewRootImpl 的performTraversals()方法开始的?
2、View 的invalidate方法是怎么触发到ViewRootImpl 的performTraversals()方法的。
在阅读本文前,最好先了解window的添加过程,Android消息处理机制 和 View 的绘制流程。推荐先阅读以下文章:
Android Window和WindowManager
Android-消息机制
Android View 的绘制流程
android 源码注释的意思是:ViewRootImpl是视图层次结构的顶部,实现 View 和 WindowManager 之间所需的协议。是 WindowManager Global 的内部实现中重要的组成部分。
View 的绘制流程是从 ViewRootImpl 的performTraversals()方法开始的,那到底是哪里调用了performTraversals()方法呢,下面我们分析一下:
1.私有属性的performTraversals()方法肯定是在内部调用起来的,经过搜索找到是doTraversal()方法调用了。
2.接着找到了,调用了doTraversal() 的TraversalRunnable 类
3.内部只有一个地方实例化了TraversalRunnable 的实例mTraversalRunnable ,查到到两个方法内都调用了mTraversalRunnable ,明显 scheleTraversals 是主动触发这个 Runnable 。这就表明调用了scheleTraversals ()函数的地方都主动触发了view的刷新。
4.接着我们看一下 mChoreographer.postCallback 做了什么
可以看到,最后后走进了scheleVsyncLocked()方法内。
5.mDisplayEventReceiver 的类 是 FrameDisplayEventReceiver,继承自
DisplayEventReceiver 。
最后走到这里就没了,那么这个方法是做了什么呢,这个方法的注释是这个意思:安排在下一个显示帧开始时传送单个垂腔察直同步脉冲。意思就是,调用了这个方法可以收到系统传送过来的垂直同步脉冲信号。Android系统每隔16ms就会发送一个VSYNC信号(VSYNC:vertical synchronization 垂直同步,帧同步),触发对UI进行渲染。这个垂直同步信对于应用来说了,只有了订阅了监听,才能收到。而且是订阅一次,收到一次。
6.既然是在这个类里面订阅垂直同步信号的,那回调也应该在这里。于是找到了以下方法。
native code 调用到 onVsync,这个方法的注释解释如下:当接收到垂直同步脉冲时调用。接收者应该渲染一个帧,然后调用 {@link scheleVsync} 来安排下一个垂直同步脉冲。
这个方法的具体实现在前面分析到的FrameDisplayEventReceiver 类里面。
这里可以看到,其实mHandler就是当前主线程的handler,当接收到onVsync信号的时候,将自己封装到Message中,等到Looper处理,最后Looper处理消息基圆宽的时候就会调用run方法,这里是Handler的机制,不做解释。
7.最后,如下图调用所示,最终从mCallbackQueues取回之前添加的任务再执行run方法,也就是TraservalRunnable的run方法。
总结上面的分析,调用流程如下图所示如下:
上面我们分析到只要调用了ViewRootImpl 的scheleTraversals ()方法,最终就能调用了ViewRootImpl 的performTraversals()来开始绘制。搏亮那肯定是我们常调用的view刷新的接口,经过一系列的方法调用,最终调用了ViewRootImpl 的scheleTraversals ()方法。下面我们分析一下常用的View 的 invalidate()接口是怎么调用到了ViewRootImpl 的scheleTraversals ()方法。
可以看出,invalidate有多个重载方法,但最终都会调用invalidateInternal方法,在这个方法内部,进行了一系列的判断,判断View是否需要重绘,接着为该View设置标记位,然后把需要重绘的区域传递给父容器,即调用父容器的invalidateChild方法。
接着我们看ViewGroup#invalidateChild:
由于不断向上调用父容器的方法,到最后会调用到ViewRootImpl的invalidateChildInParent方法,我们来看看它的源码,ViewRootImpl#invalidateChildInParent:
最后调用了scheleTraversals方法,触发View的工作流程。至此,我们已经完整地分析了一次调用View 的 invalidate()方法到触发ViewRootImpl 的scheleTraversals()方法。
Ⅵ Android 自定义View之Draw过程(上)
Draw 过程系列文章
Android 展示之三部曲:
前边我们已经分析了:
这俩最主要的任务是: 确定View/ViewGroup可绘制的矩形区域。
接下来将会分析,如何在这给定的区域内绘制想要的图形。
通过本篇文章,你将了解到:
Android 提供了关于View最基础的两个类:
然而ViewGroup 并没有约定其内部的子View是如何布局的,是叠加在一起呢?还是横向摆放、纵向摆放等。同样的View 也没有约定其展示的内容是啥样,是矩形、圆形、三角形、一张图片、一段文字抑或是不规则的形状?这些都要我们自己去实现吗?
不尽然,值得高兴的是Android已经考虑到上述需求了,为了开发方便已经预制了一些常用的ViewGroup、View。
如:
继承自ViewGroup的子类
继承自View的子类
虽然以上衍生的View/ViewGroup子类已经大大为我们提供了便利,但也仅仅是通用场景下的通用控件,我们想实现一些较为复杂的效果,比如波浪形状进度条、会发光的球体等,这些系统控件就无能为力了,也没必要去预制千奇百怪的控件。想要达到此效果,我们需要自定义View/ViewGroup。
通常来说自定义View/ViewGroup有以下几种:
3 一般不怎么用,除非布局比较特殊。1、2、4 是我们常用的手段,对于我们常说的"自定义View" 一般指的是 4。
接下来我们来看看 4是怎么实现的。
在xml里引用MyView
效果如下:
黑色部分为其父布局背景。
红色矩形+黄色圆形即是MyView绘制的内容。
以上是最简单的自定义View的实现,我们提取重点归纳如下:
由上述Demo可知,我们只需要在重写的onDraw(xx)方法里绘制想要的图形即可。
来看看View 默认的onDraw(xx)方法:
发现是个空实现,因此继承自View的类必须重写onDraw(xx)方法才能实现绘制。该方法传入参数为:Canvas类型。
Canvas翻译过来一般叫做画布,在重写的onDraw(xx)里拿到Canvas对象后,有了画布我们还需要一支笔,这只笔即为Paint,翻译过来一般称作画笔。两者结合,就可以愉快的作画(绘制)了。
你可能发现了,在Demo里调用
并没有传入Paint啊,是不是Paint不是必须的?实际上调用该方法后,底层会自动生成Paint对象。
可以看到,底层初始化了Paint,并且给其设置的颜色为在Java层设置的颜色。
onDraw(xx)比较简单,开局一个Canvas,效果全靠画。
试想,这个Canvas怎么来的呢,换句话说是谁调用了onDraw(xx)。发挥一下联想功能,在Measure、Layout 过程有提到过两者套路很像:
那么Draw过程是否也是如此套路呢?看见了onDraw(xx),那么draw(xx)还远吗?
没错,还真有draw(xx)方法:
可以看出,draw(xx)主要分为两个部分:
不管是A分支还是B分支,都进行了好几步的绘制。
通常来说,单一一个View的层次分为:
后面绘制的可能会遮挡前边绘制的。
对于一个ViewGroup来说,层次分为:
来看看A分支标注的4个点:
(1)
onDraw(canvas)
前面分析过,对于单一的View,onDraw(xx)是空实现,需要由我们自定义绘制。
而对于ViewGroup,也并没有具体实现,如果在自定义ViewGroup里重写onDraw(xx),它会执行吗?默认是不会执行的,相关分析请移步:
Android ViewGroup onDraw为什么没调用
(2)
dispatchDraw(canvas),来看看在View.java里的实现:
发现是个空实现,再看看ViewGroup.java里的实现:
也即是说,对于单一View,因为没有子布局,因此没必要再分发Draw,而对于ViewGroup来说,需要触发其子布局发起Draw过程(此过程后续分析),可以类比事件分发过程View、ViewGroup的处理。感兴趣的请移步:
Android 输入事件一撸到底之View接盘侠(3)
(3)
OverLay,顾名思义就是"盖在某个东西上面",此处是在绘制内容之后,绘制前景之前。怎么用呢?
以上是给一个ViewGroup设置overLay,效果如下:
你可能发现了,这和设置overLay差不多的嘛,实际还是有差别的。在onDrawForeground(xx)里会重新调整Drawable的尺寸,该尺寸与View大小一致,之前给Drawable设置的尺寸会失效。运行效果如下:
可以看出,ViewGroup都被前景盖住了。
再来看看B分支的重点:边缘渐变效果
先来看看TextView 边缘渐变效果:
加上这俩参数。
实际上系统自带的一些控件也使用了该效果,如NumberPicker、YearPickerView
以上是NumberPicker 的效果,可以看出是垂直方向渐变的。
对于View.java 里的onDraw(xx)、draw(xx),ViewGroup.java里并没有重写。
而对于dispatchDraw(xx),在View.java里是空实现。在ViewGroup.java里发起对子布局的绘制。
来看看标记的2点:
(1)
设置padding的目的是为了让子布局留出一定的空隙出来,因此当设置了padding后,子布局的canvas需要根据padding进行裁减。判断标记为:
FLAG_CLIP_TO_PADDING 默认设置为true
FLAG_PADDING_NOT_NULL 只要有padding不为0,该标记就会打上。
也就是说:只要设置了padding 不为0,子布局显示区域需要裁减。
能不能不让子布局裁减显示区域呢?
答案是可以的。
考虑到一种场景:使用RecyclerView的时候,我们需要设置paddingTop = 20px,效果是:RecyclerView Item展示时离顶部有20px,但是滚动的时候永远滚不到顶部,看起来不是那么友好。这就是上述的裁减起作用了,需要将此动作禁止。通过设置:
当然也可以在xml里设置:
(2)
drawChild(xx)
从方法名上看是调用子布局进行绘制。
child.draw(x1,x2,x3)里分两种情况:
这两者具体作用与区别会在下篇文章分析,不管是硬件加速绘制还是软件加速绘制,最终都会调用View.draw(xx)方法,该方法上面已经分析过。
注意,draw(x1,x2,x3)与draw(xx)并不一样,不要搞混了。
用图表示:
View/ViewGroup Draw过程的联系:
一般来说,我们通常会自定义View,并且重写其onDraw(xx)方法,有没有绘制内容的ViewGroup需求呢?
是有的,举个例子,大家可以去看看RecyclerView ItemDecoration 的绘制,其中运用到了ViewGroup draw(xx)、ViewGroup onDraw(xx) 、View onDraw(xx)绘制的先后顺序来实现分割线,分组头部悬停等功能的。
本篇文章基于 Android 10.0
Ⅶ Android 自定义View之Layout过程
系列文章:
在上篇文章: Android 自定义View之Measure过程 ,我们分析了Measure过程,本次将会掀开承上启下的Layout过程神秘面纱,
通过本篇文章,你将了解到:
在上篇文章的比喻里,我们说过:
该ViewGroup 重写了onMeasure(xx)和onLayout(xx)方法:
同时,当layout 执行结束,清除PFLAG_FORCE_LAYOUT标记,该标记会影响Measure过程是否需要执行onMeasure。
该View 重写了onMeasure(xx)和onLayout(xx)方法:
MyViewGroup里添加了MyView、Button两个控件,最终运行的效果如下:
可以看出,MyViewGroup 里子布局的是横向摆放的。我们重点关注Layout过程。实际上,MyViewGroup里我们只重写了onLayout(xx)方法,MyView也是重写了onLayout(xx)方法。
接下来,分析View Layout过程。
与Measure过程类似,连接ViewGroup onLayout(xx)和View onLayout(xx)之间的桥梁是View layout(xx)。
可以看出,最终都调用了setFrame(xx)方法。
对于Measure过程在onMeasure(xx)里记录了尺寸的值,而对于Layout过程则在layout(xx)里记录了坐标值,具体来说是在setFrame(xx)里,该方法两个重点地方:
View.onLayout(xx)是空实现
从layout(xx)和onLayout(xx)声明可知,这两个方法都是可以被重写的,接下来看看ViewGroup是否重写了它们。
ViewGroup.layout(xx)虽然重写了layout(xx),但是仅仅做了简单判断,最后还是调用了View.layout(xx)。
这重写后将onLayout变为抽象方法,也就是说继承自ViewGroup的类必须重写onLayout(xx)方法。
我们以FrameLayout为例,分析其onLayout(xx)做了什么。
FrameLayout.onLayout(xx)为子布局Layout的时候,起始坐标都是以FrameLayout为基准,并没有记录上一个子布局占了哪块位置,因此子布局的摆放位置可能会重叠,这也是FrameLayout布局特性的由来。而我们之前的Demo在水平方向上记录了上一个子布局的摆放位置,下一个摆放时只能在它之后,因此就形成了水平摆放的功能。
由此类推,我们常说的某个子布局在父布局里的哪个位置,决定这个位置的即是ViewGroup.onLayout(xx)。
上边我们分析了View.layout(xx)、View.onLayout(xx)、ViewGroup.layout(xx)、ViewGroup.onLayout(xx),这四者什么关系呢?
View.layout(xx)
View.onLayout(xx)
ViewGroup.layout(xx)
ViewGroup.onLayout(xx)
View/ViewGroup 子类需要重写哪些方法:
用图表示:
通过上述的描述,我们发现Measure过程和Layout过程里定义的方法比较类似:
它俩的套路比较类似:measure(xx)、layout(xx)一般不需要我们重写,measure(xx)里调用onMeasure(xx),layout(xx)为调用者设置坐标值。
若是ViewGroup:onMeasure(xx)里遍历子布局,并测量每个子布局,最后将结果汇总,设置自己测量的尺寸;onLayout(xx)里遍历子布局,并设置每个子布局的坐标。
若是View:onMeasure(xx)则测量自身,并存储测量尺寸;onLayout(xx)不需要做什么。
Measure过程虽然比Layout过程复杂,但仔细分析后就会发现其本质就是为了设置两个成员变量:
而Layout过程虽然比较简单,其本质是为了设置坐标值
将Measure设置的变量和Layout设置的变量联系起来:
此外,Measure过程通过设置PFLAG_LAYOUT_REQUIRED 标记来告诉需要进行onLayout,而Layout过程通过清除 PFLAG_FORCE_LAYOUT来告诉Measure过程不需要执行onMeasure了。
这就是Layout的承上作用
我们知道View的绘制需要依靠Canvas绘制,而Canvas是有作用区域限制的。例如我们使用:
Cavas绘制的起点是哪呢?
对于硬件绘制加速来说:正是通过Layout过程中设置的RenderNode坐标。
而对于软件绘制来说:
关于硬件绘制加速/软件绘制 后续文章会分析。
这就是Layout的启下作用
以上即是Measure、Layout、Draw三者的内在联系。
当然Layout的"承上"还需要考虑margin、gravity等参数的影响。具体用法参见最开始的Demo。
getMeasuredWidth()/getMeasuredHeight 与 getWidth/getHeight区别
我们以获取width为例,分别来看看其方法:
getMeasuredWidth():获取测量的宽,属于"临时值"
getWidth():获取View真实的宽
在Layout过程之前,getWidth() 默认为0
何时可以获取真实的宽、高
下篇将分析Draw()过程,我们将分析"一切都是draw出来的"道理
本篇基于 Android 10.0