Android View的绘制

作者 : 开心源码 本文共8399个字,预计阅读时间需要21分钟 发布时间: 2022-05-12 共58人阅读

本文主要关注View的测量、布局、绘制三个步骤,探讨这三个步骤的执行流程。本文暂不涉及View和Window之间的交互以及Window的管理。再论述完这三个步骤之后,文末以自己设置TagGroup为例,讲述如何自己设置ViewGroup。

View 树的绘图流程

View树的绘图流程是由核心类:ViewRootImpl 来解决的,ViewRootImpl作为整个控件树的根部,它是控件树正常运作的动力所在,控件的测量、布局、绘制以及输入事件的派发解决都由ViewRootImpl触发。

核心成员变量

这里我主要讲几个Handler:

ViewRootHandler

这是ViewRootImpl调度的核心,其解决的消息事件主要有:
MSG_INVALIDATE、MSG_INVALIDATE_RECT、MSG_RESIZED、MSG_DISPATCH_INPUT_EVENT、MSG_CHECK_FOCUS、MSG_DISPATCH_DRAG_EVENT、MSG_CLOSE_SYSTEM_DIALOGS、MSG_UPDATE_CONFIGURATION等

主要有以下几类:View绘制相关、输入焦点等客户交互相关、系统通知相关。

有经验的同学一定遇到过这样的场景:动态创立一个View之后,想要直接获取measureWidth 和 measureHeight往往取不到,这个时候我们会通过view.postDelayed()方法去获取。那么,问题来了,为什么这样就能取到呢?

答案就在ViewRootImpl中的ViewRootHandler,view.post–> attachInfo.mHandler.post –> ViewRootImpl ViewRootHandler. 这个Handler保证了当你post的runable被执行到时,view早就测量好了。

public boolean post(Runnable action) {    final AttachInfo attachInfo = mAttachInfo;    if (attachInfo != null) {        return attachInfo.mHandler.post(action);    }    // Postpone the runnable until we know on which thread it needs to run.    // Assume that the runnable will be successfully placed after attach.    getRunQueue().post(action);    return true;}

Choreographer.FrameHandler

Choreographer这个类来控制同步解决输入(Input)、动画(Animation)、绘制(Draw)三个UI操作,这里不得不提一下Choreographer.FrameHandler目的就在于ViewRootImpl中涉及到到的View绘制流程,是通过Choreographer.FrameHandler来进行调度的。

private final class FrameHandler extends Handler {    public FrameHandler(Looper looper) {        super(looper);    }    @Override    public void handleMessage(Message msg) {        switch (msg.what) {            case MSG_DO_FRAME:                doFrame(System.nanoTime(), 0);                break;            case MSG_DO_SCHEDULE_VSYNC:                doScheduleVsync();                break;            case MSG_DO_SCHEDULE_CALLBACK:                doScheduleCallback(msg.arg1);                break;        }    }}

关于Choreographer,读者可以参考下这篇文章,讲的非常详细:Android Choreographer 源码分析

View树流程控制:performTraversals

整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数开展,该函数所做 的工作可简单概略为能否需要重新计算视图大小(measure)、能否需要重新安置视图的位置(layout)、以及能否需要重绘(draw),流程图如下:

image

更详细的图示如下:

View树绘制过程.png

performTraversals 方法非常庞大,整个源码在800行左右,看起来会让人吐血。这个方法主要的过程有四个:

预测量阶段
这是进入performTraversals()方法后的第一个阶段,它会对控件树进行第一次测量。测量结果可以通过mView. getMeasuredWidth()/Height()取得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次得到回调。

布局窗口阶段
根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。

最终测量阶段
预测量的结果是控件树所期望的窗口尺寸。然而因为在WMS中影响窗口布局的因素很多(参考第4章),WMS不肯定会将窗口精确地布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。因而在这一阶段,performTraversals()将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。

布局控件树阶段
完成最终测量之后便可以对控件树进行布局了。测量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。

绘制阶段
这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。

那问题来了,这个方法什么时候会被触发,或者者说Android系统什么时候会对整个View树进行一次全量的操作呢?从源码中,我们可以看到以下几个核心的方法会触发:

  1. requestLayout: 注意在View中也有同样的一个requestLayout方法,view中的requestLayout方法调用的就是ViewRootImpl中的requestLayout,最终触发View树的绘制流程,即 measure-layout-draw;
  2. invalidate:同样的View中也有一个invalidate方法,View中该方法的调用最终调用的也是ViewRootImpl中的方法。有经验的同学一定知道,invalidate只会触发draw,不会触发measure和 layout。具体的ViewRootImpl会通过变量mLayoutRequested控制能否要进行measure和layout,invalidate操作时这个变量为false
@Overridepublic void requestLayout() {    if (!mHandlingLayoutInLayoutRequest) {        checkThread();        mLayoutRequested = true;        scheduleTraversals();    }}void invalidate() {    ...    if (!mWillDrawSoon) {       scheduleTraversals();    }}

View 绘制流程函数调用链

image

有几点注意:
? invalidate/postInvalidate 只会触发 draw;
? requestLayout,会触发 measure、layout 和 draw 的过程;
? 它们都是走的 scheduleTraversals -> performTraversals,用不同的标记位来进行区分;
? resume 会触发 invalidate;
? dispatchDraw 是用来绘制 child 的,发生在自己的 onDraw 之后,child 的 draw 之前
Measure 和 Layout 的具体过程

Measure 和 Layout 的具体过程

image

关于Measure过程,不得不详细提一下MeasureSpec。MeasureSpec是一个复合整型变量(32bit),用于指导控件对自身进行测量,它有两个分量:前两位表示SPEC_MODE,后30位表示SPEC_SIZE。SPEC_MODE的取值取决于此控件的LayoutParams.width/height的设置,SPEC_SIZE则是父视图给定的指导大小。

SPEC_MODE有三种模式,具体的计算如下:

MeasureSpec.UNSPECIFIED: 表示控件在进行测量时,可以无视SPEC_SIZE的值。控件可以是它所期望的任意尺寸。

MeasureSpec.EXACTLY: 表示子控件必需为SPEC_SIZE所制定的尺寸。当控件的LayoutParams.width/height为一确定值,或者者是MATCH_PARENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。

MeasureSpec.AT_MOST: 表示子控件可以是它所期望的尺寸,但是不得大于SPEC_SIZE。当控件的LayoutParams.width/height为WRAP_CONTENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。

自己设置一个TagGroup

讲了这么多,下面我们来实操一下。

需求:自己设置一个TagGroup,用来显示一系列标签元素。要求标签样式完全可以自己设置,标签间距可在xml中指定,要有最多显示多少行的控制,显示不全时要展现“更多 …”

样式协定

在attrs.xml中协定样式:

<declare-styleable name="DjTagGroup">    <attr name="tag_horizontalSpacing" format="dimension" />    <attr name="tag_verticalSpacing" format="dimension" />    <attr name="max_row" format="integer"/></declare-styleable>

协定接口,用来提供具体的标签元素:

public interface TagViewHolder {    View getView();}

自己设置Measure过程

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);    final int widthSize = MeasureSpec.getSize(widthMeasureSpec);    final int heightSize = MeasureSpec.getSize(heightMeasureSpec);    measureChildren(widthMeasureSpec, heightMeasureSpec);    int width = 0;    int height = 0;    int row = 0; // The row counter.    int rowWidth = 0; // Calc the current row width.    int rowMaxHeight = 0; // Calc the max tag height, in current row.    if (moreTagHolder != null) {        moreTagMeasureWidth = moreTagHolder.getView().getMeasuredWidth();        moreTagMeasureHeight = moreTagHolder.getView().getMeasuredHeight();    }    final int count = getChildCount();    for (int i = 0; i < count; i++) {        final View child = getChildAt(i);        final int childWidth = child.getMeasuredWidth();        final int childHeight = child.getMeasuredHeight();        if (child.getVisibility() != GONE) {            // judge the max_row            if (row + 1 >= maxRow && rowWidth + childWidth  > widthSize) {                break;            }            rowWidth += childWidth;            if (rowWidth > widthSize) { // Next line.                rowWidth = childWidth; // The next row width.                height += rowMaxHeight + verticalSpacing;                rowMaxHeight = childHeight; // The next row max height.                row++;            } else { // This line.                rowMaxHeight = Math.max(rowMaxHeight, childHeight);            }            rowWidth += horizontalSpacing;        }    }    // Account for the last row height.    height += rowMaxHeight;    // Account for the padding too.    height += getPaddingTop() + getPaddingBottom();    // If the tags grouped in one row, set the width to wrap the tags.    if (row == 0) {        width = rowWidth;        width += getPaddingLeft() + getPaddingRight();    } else {// If the tags grouped exceed one line, set the width to match the parent.        width = widthSize;    }    setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,            heightMode == MeasureSpec.EXACTLY ? heightSize : height);}

自己设置layout过程

@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {    final int parentLeft = getPaddingLeft();    final int parentRight = r - l - getPaddingRight();    final int parentTop = getPaddingTop();    final int parentBottom = b - t - getPaddingBottom();    int childLeft = parentLeft;    int childTop = parentTop;    int row = 0;    int rowMaxHeight = 0;    boolean showMoreTag = false;    final int count = getChildCount();    int unTagCount = count;    if (moreTagHolder != null) {        unTagCount--;    }    for (int i = 0; i < unTagCount; i++) {        final View child = getChildAt(i);        final int width = child.getMeasuredWidth();        final int height = child.getMeasuredHeight();        if (child.getVisibility() != GONE) {            if (row + 1 >= maxRow && childLeft + width + (horizontalSpacing + moreTagMeasureWidth)  > parentRight) {                // 预留一个空位放置moreTag                showMoreTag = true;                break;            }            if (childLeft + width > parentRight) { // Next line                childLeft = parentLeft;                childTop += rowMaxHeight + verticalSpacing;                rowMaxHeight = height;                row++;            } else {                rowMaxHeight = Math.max(rowMaxHeight, height);            }            // this is point            child.layout(childLeft, childTop, childLeft + width, childTop + height);            childLeft += width + horizontalSpacing;        }    }    if (showMoreTag) {        final View child = getChildAt(count - 1);        final int width = child.getMeasuredWidth();        final int height = child.getMeasuredHeight();        child.layout(childLeft, childTop, childLeft + width, childTop + height);    }}

使用

在xml中直接引用

<com.xud.tag.DjTagGroup    android:id="@+id/dj_tag_group"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:padding="16dp"    app:tag_horizontalSpacing="8dp"    app:tag_verticalSpacing="8dp"    app:max_row="4"/>

定义自己的TagViewHolder

public class DjTagViewHolder implements DjTagGroup.TagViewHolder {    public String content;    public View rootView;    public TextView tagView;    public DjTagViewHolder(View itemView, String content) {        this.rootView = itemView;        tagView = itemView.findViewById(R.id.tag);        tagView.setText(content);        tagView.setOnClickListener(v -> Toast.makeText(context, "点击了:" + content, Toast.LENGTH_SHORT).show());    }    @Override    public View getView() {        return rootView;    }}

往DjTagGroup直接设置tags

private void initDjTags() {    String[] tags = TagGenarator.generate(10, 6);    List<DjTagGroup.TagViewHolder> viewHolders = new ArrayList<>();    for (String tag: tags) {        DjTagViewHolder viewHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false),                tag);        viewHolders.add(viewHolder);    }    DjTagViewHolder moreHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false),            "更多 ...");    djTagGroup.setTags(viewHolders, moreHolder);}

实际的效果

源码地址: Github: 自己设置View辑录DjCustomView

参考文章
Hencoder: 自己设置View相关

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android View的绘制

发表回复