最基本的 三个方法
- onMeasure()
- onLayout()
- onDraw()
View在Activity中显示出来,要经历测量、布局和绘制三个步骤,
分别对应三个动作:measure、layout和draw。
测量:onMeasure()决定View的大小;
布局:onLayout()决定View在ViewGroup中的位置;
绘制:onDraw()决定绘制这个view
自定义控件分类
- 自定义View: 只需要重写onMeasure()和onDraw()
- 自定义ViewGroup :则只需要重写onMeasure()和onLayout()
视图View主要分为两类
- 单一视图 即一个View,如TextView 不包含子View
- 视图组 即多个View组成的ViewGroup,如LinearLayout 包含子View
View
View类是Android中各种组件的基类,如View是ViewGroup基类View表现为显示在屏幕上的各种视图
Android中的UI组件都由View、ViewGroup组成。
View的构造函数:共有4个
//如果view是在Java代码里面new的,则调用第一个构造函数
public CarsonView(context context) {
super(context);
}
//如果view是在.xm1里声明的,则调用第二个构造函数
//自定义属性是从attributeSet参数传进来的
public Carsonview(Context context, AttributeSet attrs) {
super(context, attrs);
}
//不会自动调用
//一般是在第二个构造函数里主动调用
//如view有style属性时
public Carsonview(Context context, AttributeSet attrs, int defsty1eAttr){super(context, attrs, defSty1eAttr);
}
//API21之后才使用
//不会自动调用
//一般是在第二个构造函数里主动调用
//如view有style属性时
public CarsonView(Context context, AttributeSet attrs, int defsty1eAttr, intdefstyleRes) {
super(context, attrs, defSty1eAttr, defStyleRes);
}
ViewGroup 的方法调用
View的方法调用
View的层次结构
Android 坐标系
- 屏幕的左上角为坐标原点
- 向右为x轴增大方向
View 位置(坐标)描述
View的位置由4个顶点决定的4个顶点的位置描述分别由4个值决定:
View的位置是相对于父控件而言的
- Top: 子View上边界到父view上边界的距离
- Left: 子View左边界到父view左边界的距离
- Bottom: 子View下边距到父View上边界的距离
- Right: 子View右边界到父view左边界的距离
位置获取方式
- getTop()
- getLeft();
- getRight();
- getBottom();
MotionEvent get()和getRaw()的区别
//get()触摸点相对于其所在组件坐标系的坐标event.getX();
event.getX();
//getRawX():触摸点相对于屏幕默认坐标系的坐标event.getRawx();
event.getRawY();
Android 支持的颜色模式
- ARGB8888 四通道高精度(32位)
- ARGB4444 四通道低精度(16位)
- RGB565 Android屏幕默认模式(16位)
- Alpha8 仅有透明通道(8位)
· 字母表示通道类型;
· 数值表示该类型用多少位二进制来描述。
· 例子:ARGB8888,表示有四个通道(ARGB);每个对应的通道均
用8位来描述。
A(Alpha) 透明度 (0-255)
R(Red) 红色 (0-255)
G(Green) 绿色 (0-255)
B(Blue) 蓝色 (0-255)
View的绘制流程
View树的绘制流程是通过ViewRoot去负责绘制的,ViewRoot这个类的命名有点坑,最初看到这个名字,翻译过来是view的根节点,但是事实完全不是这样,ViewRoot其实不是View的根节点,它连view节点都算不上。
它的主要作用是View树的管理者,负责将DecorView和PhoneWindow组合”起来,而View树的根节点严格意义上来说只有DecorView;
每个DecorView都有一个ViewRoot与之关联,这种关联关系是由WindowManager去进行管理的;
- 依次调用如下顺序:
WindowManager.addView()
ViewRootImpl.setView
ViewRootImpl.requestLayout
ViewRootImpl.scheduleTraversals
TraversalRunnable.run
ViewRootImpl.doTraversal
ViewRootImpl.performTraversals
- performTraversals 开始计算视图大小
https://www.cnblogs.com/xyhuangjinfu/p/5435201.html
1、系统为什么要有measure过程?
开发人员在绘制UI的时候,基本都是通过XML布局文件的方式来配置UI,而每个View必须要设置的两个群属性就是layout_width和layout_height,这两个属性代表着当前View的尺寸。
这两个属性的取值只能为三种类型:
1、固定的大小,比如100dp。
2、刚好包裹其中的内容,wrap_content。
3、想要和父布局一样大,match_parent / fill_parent。
由于Android希望提供一个更优雅的GUI框架,所以提供了自适应的尺寸,也就是 wrap_content 和 match_parent 。
试想一下,那如果这些属性只允许设置固定的大小,那么每个View的尺寸在绘制的时候就已经确定了,所以可能都不需要measure过程。但是由于需要满足自适应尺寸的机制,所以需要一个measure过程。
2、measure过程都干了点什么事?
由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义View大小的时候,View的真实尺寸还不能确定。
但是View尺寸最终需要映射到屏幕上的像素大小,所以measure过程就是干这件事,把各种尺寸值,经过计算,得到具体的像素值。
measure过程会遍历整棵View树,然后依次测量每个View真实的尺寸。
具体是每个ViewGroup会向它内部的每个子View发送measure命令,然后由具体子View的onMeasure()来测量自己的尺寸。
最后测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的数据单位是像素。
3、对于自适应的尺寸机制,如何合理的测量一颗View树?
系统在遍历完布局文件后,针对布局文件,在内存中生成对应的View树结构。
这个时候,整棵View树中的所有View对象,都还没有具体的尺寸,因为measure过程最终是要确定每个View打的准确尺寸,也就是准确的像素值。
但是刚开始的时候,View中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。
所以当一个View需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。
1、针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。
2、针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。
由于树这种数据结构的特殊性,我们在研究measure的过程时,可以只研究一个ViewGroup和2个View的简单场景。大概示意图如下:
也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,
一边需要考虑到自己的content大小,
一边还要考虑的父布局的限制信息,
然后综合评定,测量出一个最优的结果。
4、那么ViewGroup是如何向子View传递限制信息的?
谈到传递限制信息,那就是MeasureSpec类了,该类贯穿于整个measure过程,用来传递父布局对子View尺寸测量的约束信息。简单来说,该类就保存两类数据。
1、子View当前所在父布局的具体尺寸。
2、父布局对子View的限制类型。
那么限制类型又分为三种类型:
1、UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)
2、EXACTLY,精确的。意思就是,根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。
3、AT_MOST,最多的。意思就是,根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。
ScrollView内部嵌套ListView的问题
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
- ListView的onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View child = obtainView(0, mIsScrap);
childHeight = child.getMeasuredHeight();
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
}
当MeasureSpec mode为UNSPECIFIED的时候,只测量第一个item打的高度,跟问题描述相符,所以我们猜测可能是因为ScrollView传递了一个UNSPECIFIED限制给ListView。
- ScrollView的onMeasure代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
调用了父类的onMeasure:
看看FrameLayout的onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
}
调用了measureChildWithMargins,但是因为ScrollView覆写了该方法,所以看看ScrollView的measureChildWithMargins方法:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final int childHeightMeasureSpec =
MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(parentHeightMeasureSpec),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
果然,它向ListView的onMeasure传递了一个UNSPECIFIED的限制。
为什么呢,想想,因为ScrollView,本来就是可以在竖直方向滚动的布局,所以,它对它所有的子View的高度就是UNSPECIFIED,意思就是,不限制子View有多高,因为我本来就是需要竖直滑动的,它的本意就是如此,所以它对子View高度不做任何限制。
为什么这种解决方法可以解决这个问题?
看看ListView的onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View child = obtainView(0, mIsScrap);
childHeight = child.getMeasuredHeight();
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
}
只要让heightMode == MeasureSpec.AT_MOST,它就会测量它的完整高度,所以第一个数据,限制mode的值就确定下来了。第二个数据就是尺寸上限,如果给个200,那么当ListView数据过多的时候,该ListView最大高度就是200了,还是不能完全显示内容,怎么办?那么就给个最大值吧,最大值是多少呢,Integer.MAX_VALUE?
先看一下MeasureSpec的代码说明:
private static final int MODE_SHIFT = 30;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
他用最高两位存储mode,用其他剩余未存储size。所以Integer.MAX_VALUE >> 2,就是限制信息所能携带的最大尺寸数据。所以最后就需要用这两个值做成一个限制信息,传递给ListView的height维度。
也就是如下代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
layout
https://www.cnblogs.com/xyhuangjinfu/p/5435253.html
View框架的工作流程为:测量每个View大小(measure)-->把每个View放置到相应的位置(layout)-->绘制每个View(draw)。
1、系统为什么要有layout过程?
View框架在经过第一步的measure过程后,成功计算了每一个View的尺寸。但是要成功的把View绘制到屏幕上,只有View的尺寸还不行,还需要准确的知道该View应该被绘制到什么位置。
除此之外,对一个ViewGroup而言,还需要根据自己特定的layout规则,来正确的计算出子View的绘制位置,已达到正确的layout目的。这也就是layout过程的职责。
该位置是View相对于父布局坐标系的相对位置,而不是以屏幕坐标系为准的绝对位置。
这样更容易保持树型结构的递归性和内部自治性。
而View的位置,可以无限大,超出当前ViewGroup的可视范围,这也是通过改变View位置而实现滑动效果的原理。
2、layout过程都干了点什么事?
由于View是以树结构进行存储,所以典型的数据操作就是递归操作,所以,View框架中,采用了内部自治的layout过程。
每个叶子节点根据父节点传递过来的位置信息,设置自己的位置数据,每个非叶子节点,除了负责根据父节点传递过来的位置信息,设置自己的位置数据外(如果有父节点的话),还需要根据自己内部的layout规则(比如垂直排布等),计算出每一个子节点的位置信息,然后向子节点传递layout过程。
对于ViewGroup,除了根据自己的parent传递的位置信息,来设置自己的位置之外,还需要根据自己的layout规则,为每一个子View计算出准确的位置(相对于子View的父布局的位置)。
对于View,根据自己的parent传递的位置信息,来设置自己的位置。
View对象的位置信息,在内部是以4个成员变量的保存的,分别是mLeft、mRight、mTop、mBottom。他们的含义如图所示。
Android View框架的draw机制
1、系统为什么要有draw过程?
View框架在经过了measure过程和layout过程之后,就已经确定了每一个View的尺寸和位置。那么接下来,也是一个重要的过程,就是draw过程,draw过程是用来绘制View的过程,它的作用就是使用graphic框架提供的各种绘制功能,绘制出当前View想要的样子。
2、draw过程都干了点什么事?
View框架中,draw过程主要是绘制View的外观。ViewGroup除了负责绘制自己之外,还需要负责绘制所有的子View。而不含子View的View对象,就负责绘制自己就可以了。
draw过程的主要流程如下:
1、绘制 backgroud(drawBackground)
2、如果需要的话,保存canvas的layer,来准备fading(不是必要的步骤)
3、绘制view的content(onDraw方法)
4、绘制children(dispatchDraw方法)
5、如果需要的话,绘制fading edges,然后还原layer(不是必要的步骤)
6、绘制装饰器、比如scrollBar(onDrawForeground)
LayoutParams
LayoutParams翻译过来就是布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己。
从这个定义中也可以看出来LayoutParams与ViewGroup是息息相关的,因此脱离ViewGroup谈LayoutParams是没有意义的。
事实上,每个ViewGroup的子类都有自己对应的LayoutParams类,典型的如LinearLayout.LayoutParams和FrameLayout.LayoutParams等,可以看出来LayoutParams都是对应ViewGroup子类的内部类
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
LayoutParams与View如何建立联系
- 在XML中定义View
- 在Java代码中直接生成View对应的实例对象
自定义LayoutParams
- 创建自定义属性
<!一自定义的属性 一>
<!一 使用系统预置的属性一>
- 继承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);
}
- 重写与LayoutParams的方法
//检查LayoutParams是否合法
@override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleviewGroup.LayoutParams;
}
//生成默认的LayoutParams
@override
protected ViewGroup.LayoutParams generateDefaultLayoutParamsO {
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(getcontextO, attrs);
}
MeasureSpec
- widthMeasureSpec
- heightMeasureSpec
specMode
UNSPECIFIED 父控件不对你有任何限制,你想要多大给你多大,想上天就上天。
这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)EXACTLY 父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
AT_MOST你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
specSize
在某种specMode下的参考尺寸
MeasureSpecs 的意义
通过将 SpecMode 和 SpecSize 打包成一个int值可以避免过多的对象内存分配,为了方便操作,其提供了打包/解包方法
MeasureSpecs 值的确定
MeasureSpec (测量规格,32位的int值) =
Mode (测量模式,高2位即31,32位)+
size(具体测量大小,低30位)
子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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY://父控件是精确的
//如果child的布局参数有固定值,比如"layout_width" = "100dp"
//那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
//比如TextView根据设置的字符串大小来决定自己的大小
//那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
//所以测量模式就是AT_MOST,测量大小就是父控件的size
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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
注: parentSize 为父容器中目前可使用的大小
针对上表,这里再做一下具体的说明
对于应用层View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定
对于不同的父容器和view本身不同的LayoutParams, view就可以有多种MeasureSpec。
1.当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
2.当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;
3.当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。
- Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况需要处理)。
https://www.jianshu.com/p/0723ff4123e1