Android UI绘制之View绘制的工作原理

Android UI绘制之View绘制的工作原理,第1张

这是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(Randroididcontent) 。通过 contentgetChildAt(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 为 MeasureSpecUNSPECIFIED 即未指定模式,那么返回由方法参数传递过来的尺寸作为 View 的测量宽度和高度;

如果 specMode 不是 MeasureSpecUNSPECIFIED 即是最大模式或者精确模式,那么返回从 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绘制背景( backgrounddraw(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开发艺术探索》

WindowManager wm = getWindowManager();

Display display = wmgetDefaultDisplay();

androidviewWindowManagerLayoutParams lp = dialoggetWindow()getAttributes();

lpwidth = displaygetWidth();

lpheight =LayoutParamsWRAP_CONTENT;

dialoggetWindow()setAttributes(lp);

任意取得任意设置

在代码里修改,先获取这两个Button, 然后获取一个的宽,通过setLayoutParams(ViewGroupLayoutParams

params),修改第二个的宽高,注意,获取view的宽高的方法有3中,你要选择合适的。

为什么viewpost()能保证获取到view的宽高?

Viewpost()的原理: 以Handler为基础,Viewpost() 将传入任务添加到 View绘制任务所在的消息队列尾部,从而保证Viewpost() 任务的执行时机是在View 绘制任务完成之后的。 其中,几个关键点:

所以:

具体源码分析请看: Android:为什么viewpost()能保证获取到view的宽高?

为什么onCreate()使用viewpost()无法立刻执行任务(如获取宽高),需要在onResume()后才可获取?

在onCreate()时,AttachInfo还没被赋值(为null)(是在viewdispatchAttachedToWindow()才被赋值),所以会走下述源码的过程2;通过上面分析,此过程的作用仅是:保存了通过post()添加的任务,并没执行。

若只是创建一个 View & 调用它的post(),那么post的任务会不会被执行?

不会。主要原因是:

每个View中post() 需执行的任务,必须得添加到窗口视图-执行绘制流程 - 任务才会被post到消息队列里去等待执行,即依赖于dispatchAttachedToWindow ();

若View未添加到窗口视图,那么就不会走绘制流程,post() 添加的任务最终不会被post到消息队列里,即得不到执行。(但会保存到HandlerAction数组里)

上述例子,因为它没有被添加到窗口视图,所以不会走绘制流程,所以该任务最终不会被post到消息队列里 & 执行

此时只需要添加将View添加到窗口,那么post()的任务即可被执行

viewpos()传入的任务被执行的有效期是多久?

在整个 Activity 的生命周期内都可以正常使用 Viewpost() 任务

任务被执行是构造AttachInfo,所以任务释放即时释放AttachInfo (置为null)。而AttachInfo 的释放 *** 作(置为null)是在 Activity 生命周期 onDestory 方法之后

下面,我们将分析,什么时候调用上述入口,即DecorViewdispatchDetachedFromWindow();

此时需从 将DecorView从WindowManager中移除 开始讲起:移除 Window 窗口任务是通过 ActivityThreadhandleDestoryActivity()完成。

Viewpost() 任务被执行的有效期是在 Activity 生命周期 onDestory()后。本质是追踪AttachInfo的释放过程(置为null)

AttachInfo的释放过程是在 将DecorView从WindowManager中移除时:回调DecorViewdispatchDetachedFromWindow(),其具体行为是:

而上述过程是在ActivityThreadhandleDestoryActivity()中回调 ActivityonDestory()之后。

至此,关于viewpost()的四大常见疑问 (坑)内容讲解完毕。

不定期分享关于 安卓开发 的干货,追求 短、平、快 ,但 却不缺深度

View的构造函数:共有4个

系统自带的View可以在xml中配置属性,对于写的好的自定义View同样可以在xml中配置属性,为了使自定义的View的属性可以在xml中配置,需要以下4个步骤:

一定要记住:无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。

Android的坐标系定义为:

View的位置由4个顶点决定的 4个顶点的位置描述分别由4个值决定:

View的位置是通过viewgetxxx()函数进行获取:(以Top为例)

与MotionEvent中 get()和getRaw()的区别

MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的。

1 创建自定义属性

2 继承MarginLayout

3 重写ViewGroup中几个与LayoutParams相关的方法

在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:

测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:

针对上表,这里再做一下具体的说明

一般getIntrinsicWidth/Height能获得内部宽/高 Drawable其内部宽高就是图

片的宽高 颜色Drawable没有内部宽高的概念 内部宽高不等同于它的大小,一般

Drawable没有大小概念(作为View背景时,会被拉伸至View的大小)

  先很负责任的说一下,这个内容也是百度来的,但是很不负责任的是,当初只记录解决方案,忘了记录是查看的哪篇博客了,这里先对不知道借鉴的谁表示感谢。无法分享链接,就厚着脸皮把(转)字去掉了,请大家谅解。

  先提供一下 Android知识点——目录 的链接,然后让我们进入正题。

  实际上,这篇博客所说的内容并不是所有人都可以用到,毕竟大多数时候,我们只需要展示,而并不需要知道的宽高;有的时候我们只需要知道展示的宽高(即ImageView)的宽高,不需要知道资源的实际尺寸。

  但是需求千千万万嘛,以程序员的脑洞,怎么能想到产品的脑洞究竟有多大呢?我这里就遇到了一个需求,那就是需要在一个可缩放的上标注icon(类似地图上的marker)。这还不算完,毕竟在找到的缩放控件 PhotoView 中,我们点击到上后,是有点击点位在整个上的百分比坐标回调的。而多端通过百分比是很容易就能在中获取到相同的点位,并回显出对应的icon的(没办法,谁让我找的是方便计算百分比的呢),结果Web端优先做了这部分功能,使用的是在原图上的具体坐标。这样我百分比的计划自然就落空了,只能想办法计算出具体的点位。

  因此获取的原始尺寸就是一个必不可少的环节,我刚刚百度了一下,查到 wangke_king 的 Android获取的宽度和高度 中使用的方法是:

  我这里没有亲测过,不过应该是没有问题,但是很遗憾我们的需求是在网络上做测量,所以这个方法也无法使用,不过如果其他有类似本地需求的,不妨尝试一下。而我之前找到的解决方案为:

  首先说明,上述的方法是可以实现的尺寸测量的,只是有一个小小的问题,那就是想要计算出Drawable的宽高,需要必须等到加载完成之后,尝试了使用viewpost(),监听组件加载完成,但是并不是每次都能获取到Drawable的宽高,因此当初的解决方案是写了个两秒钟的定时器,每50毫秒测量一次,直到获取到值为止。这样的解决方案可谓是相当无脑了,而且还要消耗很多不必要的资源。

  还好皇天不负有心人啊,终于找到了通过Glide获取宽高的方式:

  这样我们就可以通过回调,在Glide将网络注入到对应的组件的时候,得到的Bitmap,然后在通过Bitmap来获取的宽高。但是需要注意的一点是,Bitmap的泛型是需要手动去设置的哦。

  另外SimpleTarget现在已经过时,暂时还没有查到。我搜索过SimpleTarget过时使用什么替换,有一些说法是使用BitmapImageViewTarget ,不过下面是实际测试结果。

链接:

百度-景色

测量结果:

信息:

如果不是我使用有误的话,BitmapImageViewTarget 是无法替换SimpleTarget ,实现测量原始宽高的功能的。

自定义View,想要自定义给定宽和高,你要写自定义属性,然后在xml文件中指定宽高才会有效,同时当给定的宽和高的值是wrap_content 或 fill_parent 这类的,这时需要在自定义View中重写onMeasure方法,进行控件的宽高测量。

我们知道,Activity 是在 ActivityThread 的 performLaunchActivity 中进行创建的,在创建完成之后就会调用其 attach 方法,它是先于 onCreate、onStart、onResume 等生命周期函数的,因此将 attach 方法作为这篇文章主线的开头:

attach() 方法就是 new 一个 PhoneWindow 并且关联 WindowManager。

接下来就到了 onCreate 方法:

这一步就是把我们的布局文件解析成 View 塞到 DecorView 的一个 id 为 Ridcontent 的 ContentView 中,DecorView 本身是一个 FrameLayout,它还承载了 StatusBar、NavigationBar 。

然后在 handleResumeActivity 中,通过 WindowManager 的 addView 方法把 DecorView 添加进去,实际实现是 WindowManagerImpl 的 addView 方法,它里面再通过 WindowManagerGlobal 的实例去 addView 的,在它里面就会 new 一个 ViewRootImpl,也就是说最后是把 DecorView 传给了 ViewRootImpl 的 setView 方法。ViewRootImpl 是 DecorView 的管理者,它负责 View 树的测量、布局、绘制,以及通过 Choreographer 来控制 View 的刷新。

WMS 是所有 Window 窗口的管理员,负责 Window 的添加和删除、Surface 的管理和事件派发等等,因此每一个 Activity 中的 PhoneWindow 对象如果需要显示等 *** 作,就必须要与 WMS 交互才能进行。

在 ViewRootImpl 的 setView 方法中,会调用 requestLayout,并且通过 WindowSession 的 addToDisplay 与 WMS 进行交互。WMS 会为每一个 Window 关联一个 WindowStatus。

SurfaceFlinger 主要是进行 Layer 的合成和渲染。

在 WindowStatus 中,会创建 SurfaceSession,SurfaceSession 会在 Native 层构造一个 SurfaceComposerClient 对象,它是应用程序与 SurfaceFlinger 沟通的桥梁。

经过步骤四和步骤五之后,ViewRootImpl 与 WMS、SurfaceFlinger 都已经建立起连接,但此时 View 还没显示出来,我们知道,所有的 UI 最终都要通过 Surface 来显示,那么 Surface 是什么时候创建的呢?

这就要回到前面所说的 ViewRootImpl 的 requestLayout 方法了,首先会 checkThread 检查是否是主线程,然后调用 scheduleTraversals 方法,scheduleTraversals 方法会先设置同步屏障,然后通过 Choreographer 类在下一帧到来时去执行 doTraversal 方法。简单来说,Choreographer 内部会接受来自 SurfaceFlinger 发出的 Vsync 垂直同步信号,这个信号周期一般是 16ms 左右。doTraversal 方法首先会先移除同步屏障,然后 performTraversals 真正进行 View 的绘制流程,即调用 performMeasure、performLayout、performDraw。不过在它们之前,会先调用 relayoutWindow 通过 WindowSession 与 WMS 进行交互,即把 Java 层创建的 Surface 与 Native 层的 Surface 关联起来。

接下来就是正式绘制 View 了,从 performTraversals 开始,Measure、Layout、Draw 三步走。

第一步是获取 DecorView 的宽高的 MeasureSpec 然后执行 performMeasure 流程。MeasureSpec 简单来说就是一个 int 值,高 2 位表示测量模式,低 30 位用来表示大小。策略模式有三种,EXACTLY、AT_MOST、UNSPECIFIED。EXACTLY 对应为 match_parent 和具体数值的情况,表示父容器已经确定 View 的大小;AT_MOST 对应 wrap_content,表示父容器规定 View 最大只能是 SpecSize;UNSPECIFIED 表示不限定测量模式,父容器不对 View 做任何限制,这种适用于系统内部。接着说,performMeasure 中会去调用 DecorView 的 measure 方法,这个是 View 里面的方法并且是 final 的,它里面会把参数透传给 onMeasure 方法,这个方法是可以重写的,也就是我们可以干预 View 的测量过程。在 onMeasure 中,会通过 getDefaultSize 获取到宽高的默认值,然后调用 setMeasureDimension 将获取的值进行设置。在 getDefaultSize 中,无论是 EXACTLY 还是 AT_MOST,都会返回 MeasureSpec 中的大小,这个 SpecSize 就是测量后的最终结果。至于 UNSPECIFIED 的情况,则会返回一个建议的最小值,这个值和子元素设置的最小值以及它的背景大小有关。从这个默认实现来看,如果我们自定义一个 View 不重写它的 onMeasure 方法,那么 warp_content 和 match_parent 一样。所以 DecorView 重写了 onMeasure 函数,它本身是一个 FrameLayout,所以最后也会调用到 FrameLayout 的 onMeasure 函数,作为一个 ViewGroup,都会遍历子 View 并调用子 View 的 measure 方法。这样便实现了层层递归调用到了每个子 View 的 onMeasure 方法进行测量。

第二步是执行 performLayout 的流程,也就是调用到 DecorView 的 layout 方法,也就是 View 里面的方法,如果 View 大小发生变化,则会回调 onSizeChanged 方法,如果 View 状态发生变化,则会回调 onLayout 方法,这个方法在 View 中是空实现,因此需要看 DecorView 的父容器 FrameLayout 的 onLayout 方法,这个方法就是遍历子 View 调用其 layout 方法进行布局,子 View 的 layout 方法被调用的时候,它的 onLayout 方法又会被调用,这样就布局完了所有的 View。

第三步就是 performDraw 方法了,里面会调用 drawSoftware 方法,这个方法需要先通过 mSurface lockCanvas 获取一个 Canvas 对象,作为参数传给 DecorView 的 draw 方法。这个方法调用的是 View 的 draw 方法,先绘制 View 背景,然后绘制 View 的内容,如果有子 View 则会调用子 View 的 draw 方法,层层递归调用,最终完成绘制。

完成这三步之后,会在 ActivityThread 的 handleResumeActivity 最后调用 Activity 的 makeVisible,这个方法就是将 DecorView 设置为可见状态。

>

以上就是关于Android UI绘制之View绘制的工作原理全部的内容,包括:Android UI绘制之View绘制的工作原理、android 如何获取AlertDialog的宽高、安卓开发:一个view宽度等于另一个view的宽度等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/9536925.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-29
下一篇2023-04-29

发表评论

登录后才能评论

评论列表(0条)

    保存