自定义view是区分中级开发和初级开发的分水岭,虽说今年校招,工作三四年的老程序员一直在劝退客户端,作为职场打拼多年的老菜鸟,对android还是挺有信心的,虽说对view的知识也只是停留在纸上,很少真正落地做一些复杂高性能的ui控件,之前在akulaku确实见识了一群技术大牛,高级ui控件伸手就来,让我羡慕不已,这一次我也从基础到源码再到实战开始写几篇自定义view教程。大家有什么好的见解也欢迎到评论区多多交流。
一. 自定义UI的基本方法
说到自定义view,我们就能联想到最基本的三个方法: onmeasure()、onlayout()、ondraw();
view能在activity显示出来。
都要经历测量、布局和绘制三个步骤,而这三个步骤分别对应三个动作:measure、layout和draw。那他们分别代表什么作用呢?
- 测量:
onmeasure()决定view的大小; - 布局:
onlayout()决定view在viewgroup中的位置; - 绘制:
ondraw()决定绘制这个view。
二. 自定义UI分类
按照ui继承属性来说,其实自定义view大概可分为两种,一种是自定义单view,一种是自定义viewgroup,自定义单view又根据重写方法不同分为,自绘view和继承view,他们的区别是:自绘view仅仅设置特定ui,不会涉及交互,继承view是基于系统特殊控件,保留原有功能,加以扩展。自定义viewgroup直接让组合控件成为一个新的父布局控件,这种方式难度有点大。
-
自定义
View- 只需要重写
onmeasure()和ondraw()
- 只需要重写
-
自定义
viewgroup- 则只需要重写
onmeasure()和onlayout()
- 则只需要重写
三. 自定义UI基础
3.1 view的分类
刚刚说了,视图View主要分为两类,这边我再次总结一下:
| 类别 | 解释 | 特点 |
|---|---|---|
自定义单view | 即一个view,如textview | 不包含子view |
自定义viewgroup | 即多个view组成的viewgroup,如LinearLayout | 包含子view |
3.2 view类简介
学习自定义view,我们首先得知道,它是干什么用的,为什么会有这个东西?然后了解他里面是如何实现的?这边先聊聊他为什么会在android里出现,总结原因有两个:
view类是android中各种组件的基类,如view是viewgroup基类view表现为显示在屏幕上的各种视图
android中的ui组件都由view、viewgroup组成。
点开view源码,我们发现里面有四个关键的构造函数,下面我就详细讲解一下,每个构造方法所代表的现实意义吧。
源码分析:
// 1. 如果View是在Java代码里面new的,则调用第一个构造函数
public CarsonView(Context context) {
super(context);
}
// 2. 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不会自动调用
// 3. 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// API21之后才使用
// 不会自动调用
// 4. 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
3.3 attributeset与自定义属性
系统自带的View可以在xml中配置属性,对于写的好的自定义view同样可以在xml中配置属性,为了使自定义的view的属性可以在xml中配置,需要以下4个步骤:
-
通过
<declare-styleable>为自定义view添加属性 -
在
xml中为相应的属性声明属性值 -
在运行时(一般为构造函数)获取属性值
-
将获取到的属性值应用到
view
3.4 view视图结构
对于多view的视图,结构是树形结构:最顶层是viewgroup,viewgroup下可能有多个viewgroup或view,如下图:
-
phonewindow是android系统中最基本的窗口系统,继承自windows类,负责管理界面显示以及事件响应。它是activity与view系统交互的接口 -
decorview是phonewindow中的起始节点view,继承于view类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个framelayout -
viewroot在activtiy启动时创建,负责管理、布局、渲染窗口UI等等
一定要记住: 无论是measure过程、layout过程还是draw过程,永远都是从view树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个view树中各个view,最终确定整个view树的相关属性。
3.5 android坐标系
android的坐标系定义为:
- 屏幕的左上角为坐标原点
- 向右为x轴增大方向
- 向下为y轴增大方向
❌ 注意事项: 不要把他和的数学坐标系搞混了
3.6 view位置(坐标)描述
view的位置由「4个顶点」决定的「4个顶点的位置」描述分别由4个值决定:
请记住:
view的位置是相对于父控件而言的
- Top:子
view上边界到父view上边界的距离 - Left:子
view左边界到父view左边界的距离 - Bottom:子
view下边距到父view上边界的距离 - Right:子
view右边界到父view左边界的距离
3.7 view位置获取方式
view的位置是通过view.getxxx()函数进行获取:(以Top为例)
// 获取Top位置
public final int getTop() {
return mTop;
}
// 其余如下:
getLeft(); // 获取子View左上角距父View左侧的距离
getBottom(); // 获取子View右下角距父View顶部的距离
getRight(); // 获取子View右下角距父View左侧的距离
与MotionEvent中 get()和getRaw()的区别
//get() :触摸点相对于其所在组件坐标系的坐标
event.getX();
event.getY();
//getRaw() :触摸点相对于屏幕默认坐标系的坐标
event.getRawX();
event.getRawY();
3.8 android中颜色相关内容
3.8.1 Android支持的颜色模式:
3.8.2 以ARGB8888为例介绍颜色定义:
四. view树的绘制流程
4.1 view树的绘制流程是谁负责的?
view树的绘制流程是通过viewroot去负责绘制的,viewroot这个类的命名有点坑,最初看到这个名字,翻译过来是view的根节点,
但是事实完全不是这样,viewroot其实不是View的根节点,它连view节点都算不上,它的主要作用是View树的管理者,负责将decorview和phonewindow“组合”起来,
而View树的根节点严格意义上来说只有decorview;每个decorview都有一个viewroot与之关联,这种关联关系是由windowmanager去进行管理的;
4.2 view的添加
记住每添加一次view都会刷新一次ui,这里面可能出现卡顿问题,之前在做周年庆活动的时候往里flowlayout添加item,measure,layout, draw方法会被执行多次。
4.3 measure
话说自定义ui执行的方法中:
measure、layout和draw,我们第一个接触方法就是measure了,今天来就说说measure执行过程。 那么问题来了:
view为什么要有view过程 ?- 因为在
android中view有自适应尺寸的机制,在用自适应尺寸来定义view大小的时候,view的真实尺寸还不能确定,这时候就需要根据view的宽高匹配规则,经过计算,得到具体的像素值,view过程就是干这件事
- 因为在
measure过程都干了点什么事 ?
由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义view大小的时候,view的真实尺寸还不能确定。但是view尺寸最终需要映射到屏幕上的像素大小,所以measure过程就是干这件事,自适应各种尺寸值,
经过计算,得到具体的像素值。measure过程会遍历整棵view树,然后依次测量每个view真实的尺寸。具体是每个viewgroup会向它内部的每个子view发送measure命令,然后由具体子view的onmeasure()来测量自己的尺寸。
最后测量的结果保存在view的mmeasuredwidth和mmeasuredheight中,保存的数据单位是像素。- 对于自适应的尺寸机制,如何合理的测量一棵
view树 ?
系统在遍历完布局文件后,针对布局文件,在内存中生成对应的view树结构,这个时候,整棵view树种的所有view对象,都还没有具体的尺寸,因为measure过程最终是要确定每个view打的准确尺寸,也就是准确的像素值。
但是刚开始的时候,view中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。
所以当一个view需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。- a. 针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。
- b. 针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。
也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,
一边需要考虑到自己的content大小,一边还要考虑的父布局的限制信息,然后综合评定,测量出一个最优的结果
- 那么
viewgroup是如何向子view传递限制信息的 ?
谈到传递限制信息,那就是measurespec类了,该类贯穿于整个measure过程,用来传递父布局对子view尺寸测量的约束信息。简单来说,该类就保存两类数据。
- 子
view当前所在父布局的具体尺寸。 - 父布局对子
view的限制类型。
那么限制类型又分为三种类型:-
- UNSPECIFIED,不限定。
- 子
view想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)
-
- EXACTLY,精确的。
- 根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在
MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。
-
- AT_MOST,最多的。
- 根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。
-
scrollview嵌套listview问题 ?
只存在滑动卡顿这一问题,可以采用如下两种简单方式快速解决,利用RecyclerView内部方法
rV.setHasFixedSize(true);
rV.setNestedScrollingEnabled(false);
4.4 layout
-
- 系统为什么要有
layout过程?
view框架在经过第一步的measure过程后,成功计算了每一个view的尺寸。但是要成功的把view绘制到屏幕上,只有view的尺寸还不行,还需要准确的知道该view应该被绘制到什么位置。除此之外,对一个viewgroup而言,还需要根据自己特定的layout规则,来正确的计算出子view的绘制位置,已达到正确的layout目的。这也就是layout过程的职责。- 该位置是
view相对于父布局坐标系的相对位置,而不是以屏幕坐标系为准的绝对位置。这样更容易保持树型结构的递归性和内部自治性。而view的位置,可以无限大,超出当前viewgroup的可视范围,这也是通过改变view位置而实现滑动效果的原理。
- 系统为什么要有
-
layout过程都干了点什么事?!
-
由于
view是以树结构进行存储,所以典型的数据操作就是递归操作,所以,view框架中,采用了内部自治的layout过程。 -
每个叶子节点根据父节点传递过来的位置信息,设置自己的位置数据,每个非叶子节点,除了负责根据父节点传递过来的位置信息,设置自己的位置数据外(如果有父节点的话),还需要根据自己内部的
layout规则(比如垂直排布等),计算出每一个子节点的位置信息,然后向子节点传递layout过程。 -
对于
viewgroup,除了根据自己的parent传递的位置信息,来设置自己的位置之外,还需要根据自己的layout规则,为每一个子view计算出准确的位置(相对于子view的父布局的位置)。 -
对于
view,根据自己的parent传递的位置信息,来设置自己的位置。view对象的位置信息,在内部是以4个成员变量的保存的,分别是mLeft、mRight、mTop、mBottom。他们的含义如图所示
4.5 draw
-
- 系统为什么要有
draw过程?
- View框架在经过了
measure过程和layout过程之后,就已经确定了每一个view的尺寸和位置。那么接下来,也是一个重要的过程,就是draw过程,draw过程是用来绘制view的过程
它的作用就是使用graphic框架提供的各种绘制功能,绘制出当前view想要的样子。
- 系统为什么要有
-
draw过程都干了点什么事?
-
View框架中,draw过程主要是绘制View的外观。ViewGroup除了负责绘制自己之外,还需要负责绘制所有的子View。而不含子View的View对象,就负责绘制自己就可以了。 -
draw过程的主要流程如下:-
绘制
backgroud(drawBackground) -
如果需要的话,保存
canvas的layer,来准备fading(不是必要的步骤) -
绘制
view的content(onDraw方法) -
绘制
children(dispatchDraw方法) -
如果需要的话,绘制
fading edges,然后还原layer(不是必要的步骤) -
绘制装饰器、比如
scrollBar(onDrawForeground)
-
五. layoutparams
5.1 marginlayoutparams
MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。
实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的
- 属性优先级问题
MarginLayoutParams主要就是增加了上下左右4种外间距。在构造方法中,先是获取了margin属性;如果该值不合法,就获取horizontalMargin;
如果该值不合法,再去获取leftMargin和rightMargin属性(verticalMargin、topMargin和bottomMargin同理)。我们可以据此总结出这几种属性的优先级
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
- 属性覆盖问题
优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
5.2 layoutparams与view如何建立联系
- 在XML中定义View
- 在Java代码中直接生成View对应的实例对象
5.3 addview
/**
* 重载方法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.MarginLayoutParamsFrameLayout.LayoutParamsLinearLayout.LayoutParamsRelativeLayout.LayoutParamsRecyclerView.LayoutParamsGridLayoutManager.LayoutParamsStaggeredGridLayoutManager.LayoutParamsViewPager.LayoutParamsWindowManager.LayoutParams
六. measurespec
6.1 measurespec定义
测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:
- UNSPECIFIED
父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大) - EXACTLY
父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。 - AT_MOST
你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
6.2 measurespec 的意义
通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法
6.3 measurespec值的确定
MeasureSpec值到底是如何计算得来的呢?
measurespe子View的MeasureSpec值是根据子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);
}
针对下表,这里再做一下具体的说明
- 对于应用层
View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定 对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。- 当
view采用固定宽高的时候,不管父容器的MeasureSpec是什么view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
- 当
view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,- 那么
view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式, - 那么view也是最大模式并且其大小不会超过父容器的剩余空间;
- 那么
- 当
view的宽高是wrap_content时,不管父容器的模式是精准还是最大化- view的模式总是最大化并且大小不能超过父容器的剩余空间。
Unspecified模式,这个模式主要用于系统内部多次measure的情况下- 一般来说,我们不需要关注此模式 (这里注意自定义
View放到ScrollView的情况 需要处理) 。
- 一般来说,我们不需要关注此模式 (这里注意自定义
- 当
七. 自定义UI原因
-
android系统内置view无法实现我们的需求
-
- 处于性能考虑
你的 点赞、评论、收藏、转发,是对我的巨大鼓励!