Android View视图 以ConstraintLayout为例

绘制流程

硬件驱动

主要包括两大部分:渲染和合成;

渲染是由GPU将所需要的画面分层,把每层的绘图指令转化为二进制数组

绝大多数Andorid设备的合成是由封装在GPU中的DPU芯片承担,DPU将GPU输出的所有图层进行合成(执行计算“脏区域”、格式转换、处理缩放等操作)

由于DPU合成数量的限制,亦或者没有不单独配置合成芯片时,那么图层的合成工作该如何进行处理?此时需要实现HWC接口,这样就可以满足sf进程进行图层合成时的工作分配问题。

Android实现

1.GraphicBuffer

每当应用有显示需求时,应用会向系统申请一块GraphicBuffer内存,GraphicBuffer可以理解为带有硬件的互斥锁的一块同步内存他会被GPUCPUDPU三个不同的硬件访问,

一个GraphicBuffer对象完整的生命周期大概是这样:

  • 渲染阶段:应用有绘图需求了,由GPU分配一块内存给应用,应用调用GPU执行绘图,此时使用者是GPU
  • 合成阶段:GPU渲染完成后将图层传递给sf进程,sf进程决定由谁来合成,hwc或者GPU
    • 如果使用GPU合成,那么此时buffer的使用者依旧是GPU
    • 如果使用hwc合成,那么此时buffer的使用者是hwc
  • 显示阶段:所有的buffer在此阶段的使用者都是hwc,因为hwc控制着显示芯片

2.BufferQueue

既然GraphicBuffer是渲染合成的实际执行者,那应该如何对GraphicBuffer进行管理呢?libgui组件库中的BufferQueue则是用来管理GraphicBuffer的,其内部封装了GraphicBuffer的队列,并对外提供了GraphicBuffer对象出列/入列的接口。 BufferQueue还为每个GraphicBuffer对象包装了几种不同的状态,每个Buffer的一生,就是在不断地循环FREE->DEQUEUED->QUEUED->ACQUIRED->FREE这个过程

设计上,BufferQueue使用了生产者/消费者模式,绝大多数的情况下,APP作为GraphicBuffer的生产者,sf进程作为GraphicBuffer的消费者,它们俩共同操作一个buffer队列

状态转换过程:

生产者:APP进程
1、producer->dequeueBuffer()
​	 从队列取出一个状态为“FREE”的buffer,此时该buffer状态变化为:FREE->DEQUEUED
2、producer->queueBuffer()
​	 将渲染完成的buffer入列,此时该buffer状态变化为:DEQUEUED->QUEUED
消费者:sf进程
1、consumer->acquireBuffer()
​	 从队列中取出一个状态为“QUEUED”的渲染完的buffer准备去合成送显,此时该buffer
的状态变化为:QUEUED->ACQUIRED
2、consumer->releaseBuffer()
​	 buffer内容已经显示过了,可以重新入列给APP使用了,此时该buffer的状态变化为:
ACQUIRED->FREE

3.Surface

对于应用开发者来说,Surface是实际和我们打交道的核心类,Surface作为图像的生产者,持有BufferQueue的引用,对图像进行渲染和合成。

以2D绘图的流程来举例,可参考View中的Ondraw方法:

1、需要显示图形时,首先创建一个Surface对象

2、调用Surface#lockCanvas()获取Canvas对象

(内部调用C的方法,申请一块图形GraphicBuffer,后续所有的绘图结果都会写入这块内存中)

3、调用Canvas的draw开头的函数执行一系列的绘图操作

4、调用Surface#unlockCanvasAndPost()将绘制完成的图层提交,等待下一步合成显示

绘制三部曲

绘制从ViewRootImpl#doTraversal()方法开始执行绘制,在ViewRootImpl中会申请surface,会跟从视图一层层向下遍历

Android 如何绘制视图

绘制流程分为三步:测量、布局、绘制,分别对应onMeasure()、onLayout()、onDraw(),

  • onMeasure():测量当前控件的大小,为正式布局提供建议(只是建议,至于是否使用,要看onLayout()函数
  • onLayout():一般使用layout()函数对所有子控件进行布局
  • onDraw():根据布局的位置绘图

测量

测量是view树自顶向下的遍历,确定每个 ViewGroup 和 View 元素的尺寸。

1.View的测量过程

过程通过view中的measure(int widthMeasureSpec, int heightMeasureSpec)方法(ViewGruop调用),再调用 setMeasuredDimension() 方法将测量的结果保存起来。

其中方法中的widthMeasureSpec 和 heightMeasureSpec 参数则是由父 View 和子 View 的尺寸共同作用得到的32位二进制数

  • UNSPECIFIED :未指定模式,可以随意设置自己的尺寸信息。比如当你的父视图是可以纵向滚动的 ScrollView ,那子视图的高度大小对于父视图来说没有意义。无论你多高(即使超出屏幕),都可以通过滑动屏幕来查看
  • EXACTLY :精确模式。当你收到此模式时,表示父视图希望你就这么大(不要小于或大于给定的大小),通常在 xml 中指定大小或者设为 match_parent或具体数值 时会收到 EXACTLY 模式
  • AT_MOST :最大模式。当你收到此模式时,表示你可以在父视图给定的范围内随意发挥,但最好不要超过父视图给你的大小,通常在 xml 中设为 wrap_content 时会收到 AT_MOST 模式

大多数情况下View的MeasureSpec 的创建是父视图通过调用 getChildMeasureSpec() 方法,由父视图的 SpecMode子View自身的LayoutParams 共同决定

但是ScrollView的子view是一个例外,因为在测量ScrollView的子view时没有调用 getChildMeasureSpec() 方法,而是直接调用 makeMeasureSpec() 方法为子 View 指定了测量模式与大小

void measureChild() {
    ViewGroup.LayoutParams lp = child.getLayoutParams();
  
    int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,lp.width);
    //高度指定为parentSize
    int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSize,MeasureSpec.UNSPECIFIED);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

2.ViewGroup的测量过程

ViewGroup 的 onMeasure() 方法没有默认实现,但是, Android 为 ViewGroup 准备了几个测量子 View 的方法.其中都会调用getChildMeasureSpec方法,通过View中LayoutParams的长宽来绘制(View 有 top 、bottom 、left 、right 和 padding 这几个属性,记录的是这个 View 在屏幕坐标系中的绝对位置,但它这几个属性只有在 layout阶段以后才会有具体的值)

//测量子视图

void measureChild()

//测量子视图并计算其Margin
//想要在代码中动态的修改 View 的 margin 属性,记得强转LayoutParams
//为 MarginLayoutParams 类型再进行操作

void measureChildWithMargins()

//获取子View的MeasureSpec,对于ViewGroup来说,这是非常重要的一个方法!!!

int getChildMeasureSpec(int spec, int padding, int childDimension)
3.多次执行onMeasure()

1)首次加载 Activity 时,View 都会执行2次 onMeasure() 方法的

 2)Dialog 或者是 Dialog 主题的 Activity时会执行3次 performTraversals() 方法,在 measureHierarchy() 方法中会执行2或3次测量,再加上述图片中视图尺寸发生变化还会再执行一次,所以每发起requestLayout请求时,会执行3或4次测量,3次调用则是9或12次

3)Window 权重导致多次调用  场景较少,待理解 

4)由 ViewGroup 自身发起调用

例如:

LinearLayout在设置权重或本身设置为自适应且子View设置填充父布局情况时,会进行多次测量

FrameLayout当自身宽高有一个不确定(自适应),且至少有两个子View的宽或高为填充父布局,会进行多次测量

可参考:   每日一问 | onMeasure()多次执行原因?-玩Android - wanandroid.com  密码 :2020

布局

布局阶段的任务量主要是在 ViewGroup ,父视图负责把子 View 们按照LayoutParams 规则摆放好

onLayout()是实现所有子控件布局的函数。所有控件的根节点是ViewRoot,ViewRoot会先通过setFrame()方法设置自身吃尺寸,然后调用onLayout()方法设置子控件布局。

view类的onLayout()是个空方法,viewGroup的onLayout()是个抽象方法需要子类自行实现。核心代码就是拿到onmeasure()中拿到的指导长宽的数值,通过child.layout()方法定义子views所在位置。

int childWidth = child.getMeasuredWidth();       ​ 
int childHeight = child.getMeasuredHeight(); 
child.layout(mChanged, mLeft, mTop, mRight, mBottom);

绘制 

绘制是先执行 ViewGroup 绘制流程,再执行子 View 的绘制流程,先绘制的内容会被后绘制的内容覆盖掉。如果开启硬件加速,会影响绘制执行的链路

  • 调用 drawBackground() 方法画 ViewGroup 的背景
  • 调用自身的 onDraw() 方法执行 Canvas 绘图逻辑
  • 调用 dispatchDraw() 通知子 View 执行 draw()
  • 调用 onDrawForeground() 方法画视图的前景

基本用法

1.layout_constraintBaseline_toBaselineOf
Baseline指的是文本基线,可以实现两个textsize不同的控件对齐。

2.margin需要对控件所需设置的边有约束后,才能进行距离的设置;可以配合gonemargin灵活使用

3.bias 偏移量,需要先行约束后,才能生效,例如对横向设置bias,则需要设置start to 和end to。

4.circle 设置圆心半径角度,进行约束控制位置

5.chainstyle 将多个控件设置为一条链的前提下,可以对多个控件所形成的链路自动形成间距。(三个模式)

6.长度宽度 若设置为0dp后,可以配合layout_constraintWidth_default属性对其长款进行设置,也可以配合weight属性设置权重(类似linearlayout)。

  • layout_constraintWidth_default
    • spread:默认,会尽量填充父布局。
    • wrap:自适应
    • percent:百分比,配合width_percent使用

7.当宽或高至少有一个尺寸被设置为0dp时,可以通过layout_constraintDimensionRadio 设置宽高比

宽设置为0dp,宽高比设置为1:1,这个时候textview1是一个正方形,效果如下:
在这里插入图片描述
除此之外,在设置宽高比的值的时候,还可以在前面加W或H,分别指定宽度或高度限制。例如:
layout_constraintDimensionRatio="H,2;3 指的是 高度被约束
layout_constraintDimensionRatio="W,2:3 指的是 宽度被约束

8.guideline控件,辅助线,可拖动,可通过orientation设置横纵,方便设置一组控件对齐的基准线

9.Group  可以把多个控件归为一组,方便隐藏或者显示一组控件

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值