高级UI系列(一): 自定义UI理论篇(2)

简介: 简介: 自定义view是区分中级开发和初级开发的分水岭,虽说今年校招,工作三四年的老程序员一直在劝退客户端,作为职场打拼多年的老菜鸟,对android还是挺有信心的,虽说对view的知识也只是停留在纸上,很少真正落地做一些复杂高性能的ui控件,之前在akulaku确实见识了一群技术大牛,高级ui控件伸手就来,让我羡慕不已,这一次我也从基础到源码再到实战开始写几篇自定义view教程。大家有什么好的见解也欢迎到评论区多多交流。

四. view树的绘制流程
1681531046221.png

4.1 view树的绘制流程是谁负责的?

1681531121519.png

view树的绘制流程是通过viewroot去负责绘制的,viewroot这个类的命名有点坑,最初看到这个名字,翻译过来是view的根节点,

     但是事实完全不是这样,viewroot其实不是View的根节点,它连view节点都算不上,它的主要作用是View树的管理者,负责将decorviewphonewindow“组合”起来,

  而View树的根节点严格意义上来说只有decorview;每个decorview都有一个viewroot与之关联,这种关联关系是由windowmanager去进行管理的;

4.2 view的添加

1681531197324.png记住每添加一次view都会刷新一次ui,这里面可能出现卡顿问题,之前在做周年庆活动的时候往里flowlayout添加item,measure,layout, draw方法会被执行多次。

4.3 measure

1681531225726.png

话说自定义ui执行的方法中:measurelayoutdraw,我们第一个接触方法就是measure了,今天来就说说measure执行过程。 那么问题来了:

  1. 1.view为什么要有view过程 ?
  • 因为在androidview有自适应尺寸的机制,在用自适应尺寸来定义view大小的时候,view的真实尺寸还不能确定,这时候就需要根据view的宽高匹配规则,经过计算,得到具体的像素值,view过程就是干这件事
  1. 2.measure过程都干了点什么事 ?
      由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义view大小的时候,view的真实尺寸还不能确定。但是view尺寸最终需要映射到屏幕上的像素大小,所以measure过程就是干这件事,自适应各种尺寸值,

      经过计算,得到具体的像素值。measure过程会遍历整棵view树,然后依次测量每个view真实的尺寸。具体是每个viewgroup会向它内部的每个子view发送measure命令,然后由具体子viewonmeasure()来测量自己的尺寸。

      最后测量的结果保存在viewmmeasuredwidthmmeasuredheight中,保存的数据单位是像素。

1681531267327.png

3.对于自适应的尺寸机制,如何合理的测量一棵view树 ?


  系统在遍历完布局文件后,针对布局文件,在内存中生成对应的view树结构,这个时候,整棵view树种的所有view对象,都还没有具体的尺寸,因为measure过程最终是要确定每个view打的准确尺寸,也就是准确的像素值。


  但是刚开始的时候,viewlayout_widthlayout_height两个属性的值,都只是自适应的尺寸,也就是match_parentwrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。


  所以当一个view需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。

  • a. 针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。
  • b. 针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。

1681531352785.png

  • 也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,

    一边需要考虑到自己的content大小,一边还要考虑的父布局的限制信息,然后综合评定,测量出一个最优的结果
  1. 4.那么viewgroup是如何向子view传递限制信息的 ?

 谈到传递限制信息,那就是measurespec类了,该类贯穿于整个measure过程,用来传递父布局对子view尺寸测量的约束信息。简单来说,该类就保存两类数据。

  1. 1.子view当前所在父布局的具体尺寸。
  2. 2.父布局对子view的限制类型。  那么限制类型又分为三种类型:
  1. 1.UNSPECIFIED,不限定。
  • view想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)
  1. 2.EXACTLY,精确的。
  • 根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。
  1. 3.AT_MOST,最多的。
  • 根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。
  1. 3.scrollview嵌套listview问题 ?
     只存在滑动卡顿这一问题,可以采用如下两种简单方式快速解决,利用RecyclerView内部方法
rV.setHasFixedSize(true);
rV.setNestedScrollingEnabled(false);

4.4 layout

1681531465042.png

  1. 1.系统为什么要有layout过程?
  • view框架在经过第一步的measure过程后,成功计算了每一个view的尺寸。但是要成功的把view绘制到屏幕上,只有view的尺寸还不行,还需要准确的知道该view应该被绘制到什么位置。除此之外,对一个viewgroup而言,还需要根据自己特定的layout规则,来正确的计算出子view的绘制位置,已达到正确的layout目的。这也就是layout过程的职责。
  • 该位置是view相对于父布局坐标系的相对位置,而不是以屏幕坐标系为准的绝对位置。这样更容易保持树型结构的递归性和内部自治性。而view的位置,可以无限大,超出当前viewgroup的可视范围,这也是通过改变view位置而实现滑动效果的原理。
  1. 2.layout过程都干了点什么事?!
  • 由于view是以树结构进行存储,所以典型的数据操作就是递归操作,所以,view框架中,采用了内部自治的layout过程。
  • 每个叶子节点根据父节点传递过来的位置信息,设置自己的位置数据,每个非叶子节点,除了负责根据父节点传递过来的位置信息,设置自己的位置数据外(如果有父节点的话),还需要根据自己内部的layout规则(比如垂直排布等),计算出每一个子节点的位置信息,然后向子节点传递layout过程。
  • 对于viewgroup,除了根据自己的parent传递的位置信息,来设置自己的位置之外,还需要根据自己的layout规则,为每一个子view计算出准确的位置(相对于子view的父布局的位置)。

1681531519160.png

对于view,根据自己的parent传递的位置信息,来设置自己的位置。view对象的位置信息,在内部是以4个成员变量的保存的,分别是mLeftmRightmTopmBottom。他们的含义如图所示

1681531581702.png

4.5 draw

1681531625629.png

  1. 1.系统为什么要有draw过程?
  • View框架在经过了measure过程和layout过程之后,就已经确定了每一个view的尺寸和位置。那么接下来,也是一个重要的过程,就是draw过程,draw过程是用来绘制view的过程。它的作用就是使用graphic框架提供的各种绘制功能,绘制出当前view想要的样子。

  1. 2.draw过程都干了点什么事?
  • View框架中,draw过程主要是绘制View的外观。ViewGroup除了负责绘制自己之外,还需要负责绘制所有的子View。而不含子ViewView对象,就负责绘制自己就可以了。

  • draw过程的主要流程如下:
  1. 1.绘制 backgroud(drawBackground)
  2. 2.如果需要的话,保存canvaslayer,来准备fading(不是必要的步骤)
  3. 3.绘制viewcontentonDraw方法)
  4. 4.绘制childrendispatchDraw方法)
  5. 5.如果需要的话,绘制fading edges,然后还原layer(不是必要的步骤)
  6. 6.绘制装饰器、比如scrollBaronDrawForeground
  7. 1681531728228.png


五. layoutparams

5.1 marginlayoutparams

  MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。

  实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的

  • 属性优先级问题
       MarginLayoutParams主要就是增加了上下左右4种外间距。在构造方法中,先是获取了margin属性;如果该值不合法,就获取horizontalMargin
      如果该值不合法,再去获取leftMarginrightMargin属性(verticalMargintopMarginbottomMargin同理)。我们可以据此总结出这几种属性的优先级

margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

  • 属性覆盖问题
      优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释

Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

5.2 layoutparamsview如何建立联系

  • 在XML中定义View
  • 在Java代码中直接生成View对应的实例对象

5.3 addview

1681531851241.png

1681531890444.png

/**
 * 重载方法1:添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 */
public void addView(View child) {
    addView(child, -1);
}
/**
 * 重载方法2:在指定位置添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
 */
public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}
/**
 * 重载方法3:添加一个子View
 * 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
 */
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();  // 生成当前ViewGroup默认的LayoutParams
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}
/**
 * 重载方法4:添加一个子View,并使用传入的LayoutParams
 */
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}
/**
 * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
 */
public void addView(View child, int index, LayoutParams params) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    .....
    if (mTransition != null) {
        mTransition.addChild(this, child);
    }
    if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
        params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
    }
    if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
        child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
    } else {
        child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
    }
    if (index < 0) {
        index = mChildrenCount;
    }
    addInArray(child, index);
    // tell our children
    if (preventRequestLayout) {
        child.assignParent(this);
    } else {
        child.mParent = this;
    }
    .....
}

5.4 自定义layoutparams

  • 1. 创建自定义属性
<resources>
    <declare-styleable name="xxxViewGroup_Layout">
        <!-- 自定义的属性 -->
        <attr name="layout_simple_attr" format="integer"/>
        <!-- 使用系统预置的属性 -->
        <attr name="android:layout_gravity"/>
    </declare-styleable>
</resources>
  • 2. 继承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public int simpleAttr;
    public int gravity;
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析布局属性
        TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
        simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
        gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);
        typedArray.recycle();//释放资源
    }
    public LayoutParams(int width, int height) {
        super(width, height);
    }
    public LayoutParams(MarginLayoutParams source) {
        super(source);
    }
    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}
  • 3. 重写ViewGroup中几个与LayoutParams相关的方法
// 检查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 
    return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默认的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 
    return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
// 对传入的LayoutParams进行转化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 
    return new SimpleViewGroup.LayoutParams(p);
}
// 对传入的LayoutParams进行转化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 
    return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}

5.5 layoutParams常见的子类

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

  • ViewGroup.MarginLayoutParams
  • FrameLayout.LayoutParams
  • LinearLayout.LayoutParams
  • RelativeLayout.LayoutParams
  • RecyclerView.LayoutParams
  • GridLayoutManager.LayoutParams
  • StaggeredGridLayoutManager.LayoutParams
  • ViewPager.LayoutParams
  • WindowManager.LayoutParams

六. measurespec

1681532102367.png

6.1 measurespec定义

1681532134126.png

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

  • UNSPECIFIED
      父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
  • EXACTLY
      父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
  • AT_MOST
      你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。

1681532177437.png

6.2 measurespec 的意义

  通过将 SpecModeSpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法

6.3 measurespec值的确定

MeasureSpec值到底是如何计算得来的呢?


  measurespeViewMeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里

  /**
     *
     * 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个
     * 最可能符合条件的child view的测量规格。  
     * @param spec 父控件的测量规格
     * @param padding 父控件里已经占用的大小
     * @param childDimension child view布局LayoutParams里的尺寸
     * @return child view 的测量规格
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec); //父控件的测量模式
        int specSize = MeasureSpec.getSize(spec); //父控件的测量大小
        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;
        switch (specMode) {
        // 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
        case MeasureSpec.EXACTLY:
            //如果child的布局参数有固定值,比如"layout_width" = "100dp"
            //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            //如果child的布局参数是"match_parent",也就是想要占满父控件
            //而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
            //比如TextView根据设置的字符串大小来决定自己的大小
            //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
            //所以测量模式就是AT_MOST,测量大小就是父控件的size
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
        case MeasureSpec.AT_MOST:
            //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
            //但同样的,child的尺寸上限也是父控件的尺寸上限size
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            //child想要根据自己逻辑决定大小,那就自己决定呗
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

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

1681532298119.png

  • 对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定 对于不同的父容器和view本身不同的LayoutParamsview就可以有多种MeasureSpec
  • view采用固定宽高的时候,不管父容器的MeasureSpec是什么
  • viewMeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
  • view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,
  • 那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,
  • 那么view也是最大模式并且其大小不会超过父容器的剩余空间;
  • view的宽高是wrap_content时,不管父容器的模式是精准还是最大化
  • view的模式总是最大化并且大小不能超过父容器的剩余空间。
  • Unspecified模式,这个模式主要用于系统内部多次measure的情况下
  • 一般来说,我们不需要关注此模式 (这里注意自定义View放到ScrollView的情况 需要处理)

七. 自定义UI原因

  1. android系统内置view无法实现我们的需求
  2. 处于性能考虑






相关文章
|
5天前
|
JavaScript 前端开发
Vue实现Element UI框架的自定义输入框或下拉框在输入时对列表选项进行过滤,以及右键列表选项弹出菜单进行删除
本文介绍了如何在Vue框架结合Element UI库实现自定义输入框或下拉框,在输入时对列表选项进行过滤,并支持右键点击列表选项弹出菜单进行删除的功能。
6 0
|
1月前
|
JavaScript
vue + element UI 表单中内嵌自定义组件的表单校验触发方案
vue + element UI 表单中内嵌自定义组件的表单校验触发方案
43 5
|
2月前
|
XML IDE 开发工具
【Android UI】自定义带按钮的标题栏
【Android UI】自定义带按钮的标题栏
41 7
【Android UI】自定义带按钮的标题栏
|
1月前
Element UI【实战范例】下拉选择 el-select 的 change 事件传入选中值+自定义参数
Element UI【实战范例】下拉选择 el-select 的 change 事件传入选中值+自定义参数
110 1
|
1月前
Element UI 源码改造 —— 自定义数字输入框的实现
Element UI 源码改造 —— 自定义数字输入框的实现
34 1
|
1月前
|
容器
Element UI 自定义环形进度条里的内容
Element UI 自定义环形进度条里的内容
68 2
|
1月前
|
数据安全/隐私保护
Element UI 密码输入框--可切换显示隐藏,自定义图标
Element UI 密码输入框--可切换显示隐藏,自定义图标
66 0
|
1月前
Element UI 【表格合计】el-table 实战范例 -- 添加单位,自定义计算逻辑
Element UI 【表格合计】el-table 实战范例 -- 添加单位,自定义计算逻辑
38 0
|
1月前
【亲测有效】Element UI 自定义 Notification 通知样式不生效,设置this.$notify样式不生效问题
【亲测有效】Element UI 自定义 Notification 通知样式不生效,设置this.$notify样式不生效问题
26 0
|
1月前
Element UI 自定义/修改下拉弹窗的样式(如级联选择器的下拉弹窗样式)
Element UI 自定义/修改下拉弹窗的样式(如级联选择器的下拉弹窗样式)
34 0