这是我在学习过程中总结的知识
目的是希望日后回来看或者需要用的时候可以 一目了然 # 的回顾、巩固、查缺补漏
不追求详细相当于书本的精简版或者说是导读(想看详细的直接对应翻书),但会尽力保证读者都能快速理解和快速使用(随理解加深会总结的更加精简),但必要时会附上一些较详细解释的链接
脚注是空白的:表示还没弄懂的知识,了解后会添加
· 为了更好地自定义View,还需要掌握View的底层工作原理,比如View的测量流程、布局流程以及绘制流程
· 除了View的三大流程,View的常见回调方法也需要熟练掌握,比如构造方法、onAttach、onVisibilityChanged、onDetach等
· 解决相应的滑动冲突
4.1 初识ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是链接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过3个过程
- measure用来测量View的宽度
- layout用来确定View在父容器中放置的位置
- draw负责将View绘制在屏幕上
流程图:
这个流程图中的方法先在顶级View中执行完,然后到子元素中重新执行,如此反复完成整个View树的遍历
- measure过程决定了View的宽和高,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽和高(存在特殊情况后面会讲到)
- layout过程决定了View四个顶点的坐标和实际Vew的宽高,可以通过getTop、getBottm、getLeft、getRight来拿到四个顶点的位置,getWidth、getHeight拿到View最终宽高
- draw决定了View的显示
如图所示,DecorView作为顶级View,它内部包含一个竖直的Linear
Layout,分为上部:标题栏,下部:内容栏(id是content,是一个FrameLayout)
在Activity中我们通过setContentView设置的布局文件是被加到内容栏中的
如何得到content?:ViewGrpoup content =(ViewGroup)findViewById(android.R. id.content)
如何得到我们设置的View?:content.getChildAt(0);
4.2 理解MeasureSpec
为了更好的理解View的测量过程,我们还需要理解MeasureSpec
作用:在测量过程,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeausreSpec,然后根据这个measureParams来测量View的宽高(不是一定是最终宽高)
4.2.1 MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)
// SpecMode有三类
// 父容器不对View有任何限制,要多大给多大,一般用于系统内部,表示一种测量的状态
UNSPECIFIED
// 父容器已经检测出View所需的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式
EXACTLY
// 父容器指定了SpecSize,对应于wrap_content。
AT_MOST
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,同时它也可以解包成其原始的SpecMode和SpecSize
4.2.2 Measure和LayoutParams
View测量时,系统会将 LayoutParams 在 父容器 的约束下转换成对应的MeasureSpec,然后再进行测量
对于顶级View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的 LayoutParams 来共同确定
MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高
对于普通的View(布局中的View),它的measure过程由ViewGroup传递而来,先看ViewGroup的measureChildWithMargins方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//自身的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//父容器的MeasureSpec+设置的padding和margin
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//以上两者结合来确定子容器的MeasureSpec
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上述方法会对子元素进行measure,measure之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec,此外我们可以看出还和View的margin和padding有关
具体可以看ViewGroup的getChildMeasureSpec,偷懒可以看下面的表格
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:
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.
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);
}
再通过一个表对getChildMeasureSpec的工作原理进行梳理
这个表在书的182页最下面
由表可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams就可以快速确定出子元素的MeasureSpec了
4.3 View的工作流程
工作流程主要是指之前说的三大流程measure、layout、draw
4.3.1 measure过程
如果只是一个原始的View,直接通过measure方法就完成了其测量过程
如果是一个ViewGroup,除了完成自己的测量过程,还会遍历去调用子元素的measure方法,再递归继续执行
1.View的measure过程
源代码路径:View-measure-onMeasure-getDefaultSize
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
对于AT_MOST和EXACTLY关键就是getDefaultSize通过父容器的MeasureSpec和自身的size返回一个测量后的大小
View的最终大小是在layout阶段确定的
几乎所有情况下View的测量大小和最终大小是相等的
关于getDefaultSize中的UNSPECIFIED和onMeasure中的 getSuggestMinimumWidth()
简单来说,在 getSuggestMinimumWidth 中
如果View没有设置背景,那么就会返回android:minWidth 这个属性所指定的值,可以为0
如果设置了背景,就返回android:minWidth和背景的最小宽度中的最大值
getSuggestMinimumWidth 和 getSuggestMinimumHeight的返回值就是View在UNSPECIFIED情况下测量的宽高
从getDefaultSize方法的实现来看,View 的宽高由specSize决定,所以:
直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于 match_parent
结合上述源代码代码和182页的表,子容器为wrap_content,父容器为AT_MOST最终得到的就是parentSize效果就是match_parent
解决方法是在wrap_content时设置一个宽高就可以,例子可看书186
2.ViewGroup的 measure 过程
ViewGroup 既要完成自己的measure又要遍历调用子元素的measure方法
ViewGroup 是一个抽象类,没有重写 View 的 onMeasure 方法,而是用 measureChildren 的方法
· 源代码地址:ViewGroup-measureChildren
这个方法遍历调用了子元素的 measureChild 方法
· 源代码地址:ViewGroup-measureChild
这个方法取出子元素的 LayoutParams 然后通过getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法进行测量
ViewGroup是一个抽象类,其测量过程的onMeasure方法是根据子类来具体实现的,比如LinearLayout、RelativeLayout.
例子:
通过查看LinearLayout(垂直方向)的onMeasure源码,系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,通过这个方法会调用子元素的measure方法
系统通过mTotalLength来存储LinearLayout在竖直方向上的初步高度,测量每个子元素都会累加(子元素高度+竖直的margin),最后测量LinearLayout自己的大小
View的measure完成后,通过getMeasuredWidth方法就可以获得View测量的宽
一个比较好的习惯是在onLayout方法中去获取测量值,比较准确
应用
假如我们打算在Activity启动的时候就做一件需要获取View的宽高的任务,并不能保证在onCreate、onStart、onResume中获取测量值
下面给出4中解决方法
- 使用onWindowFocusChanged(宽高已经改变好了),注意当Activity得到和失去焦点都会被调用一次
- view.post(runnable),使用post把一个runnable投递到消息队列的尾部,轮到的时候View已经初始化好了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
3.ViewTreeObserve中的OnGlobalLayoutListener接口,当View树状态发生改变或者内部的View可见性发生改变时,onGlobalLayout方法会被回调,这个时候来获取View的宽高
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
4.view.measure,这个方法比较复杂,不建议使用
4.3.2 layout过程
Layout的作用是确定元素的位置,当然ViewGroup的位置被确定后,它的onLayout会遍历所有子元素调用其layout方法,如此反复确定下所有子元素的位置
- 源码地址:View-layout
layout大致流程就是:确定四个顶点位置(确定好父容器位置),然后传入给onLayout方法(继续确定子容器位置)
- 源码地址:LinearLayout-onLayout-layoutVertical
使用for循环遍历子元素,其中一个childTop变量会慢慢增大(垂直效果)传入子元素应在位置的四个顶点给子元素的layout方法
注意:View的测量宽高和最终宽高有什么区别?
测量宽高形成于measure、最终宽高形成于layout,所以没有意外是完全相同的,除非重写layout的方法
4.3.3 draw过程
作用是将View按顺序绘制到屏幕上
- 绘制背景background.draw(canvas)
- 绘制自己、onDraw
- 绘制子元素、dispatchDraw
- 绘制装饰、onDrawScrollBars
- 源码地址:View-draw
View的一个特殊的方法setWillNotDraw
这是通过设置标记位来判断是否要进行绘制优化
默认情况下,View是关闭的,ViewGroup是启用的
当我们自定义控件继承于ViewGroup并且不具备绘制功能时,就开启这个功能进行优化.当确定要使用onDraw来绘制的时候,就要显式地关闭
4.4 自定义View
4.4.1 自定义View的分类
1.继承View重写onDraw方法
主要用于实现一些不规则的效果(不能通过布局组合的方式来实现),这种方法要支持wrap_content、和自己处理padding
2.继承ViewGroup派生特殊的Layout
用于实现自定义布局,除LinearLayout那种之外的.需要合适地处理ViewGroup的侧脸布局两个过程,并处理子元素的测量和布局过程
3.继承特定的View(例如TextView)
比较常用,扩展一些已有的View的功能,不需要自己支持warp_content和padding
4.继承特定的ViewGroup(例如LinearLayout)
比较常用,当某种效果看起来是几种View组合在一起的时候可以使用,不需要自己处理ViewGroup的测量和布局过程,比方法2更简便
4.4.2 自定义View须知
保证View的正常使用和防止内存泄漏
1. 让View支持warp_content
如果不在onMeasure中对wrap_content做特殊处理,那外界在布局中使用wrap_content时就无法达到预期的效果
2. 如果有必要,让自定义View支持padding
继承于View的需要在draw方法中处理padding
继承自ViewGroup的需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响
3. 尽量不要在View中使用Handler,没必要
View内部提供了post系列的方法,可以代替Handler.除非要使用Handler来发送消息(msg)
4. View中如果有线程或动画,需要及时停止
一般是在onDetachedFromWindow(在包含View的Activity退出或者remove这个View的时候回调用此方法)停止它们,与此对应的方法是onAttachedToWindow.所以当View变得不可见的时候就需要停止线程和动画防止内存泄漏
5. View带有滑动嵌套的时候,处理滑动冲突
看之前的
4.4.3 自定义View示例
1. 继承View重写onDraw方法
自定义一个圆,必须要考虑到wrap_content和padding
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
//这里是针对wrap_content问题的解决方法,指定一个默认的宽高就可以
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/*
先获取测量格式和测量值
*/
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//判断是不是wrap_content
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//这是针对padding无效问题的解决方式
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
//注意宽受padding的影响
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
//实现一个在自己中心点以宽高最小值为直径画一个红色实心圆
int radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2,
radius, mPaint);
}
}
自定义View的使用方法
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green" />
如何添加自定义属性?
1.在values目录下创建自定义属性的XML,比如attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
//自定义属性集合:CircleView
<declare-styleable name="CircleView">
//属性:格式为color,名字为circle_color
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
其他自定义属性还有,reference是指资源id,dimension是指尺寸以及其他···
- 在View的构造方法中解析自定义属性并处理
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//加载自定义属性集合CircleView
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//默认的是RED
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
//加载完后释放资源
a.recycle();
init();
}
3.在布局文件中使用
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green" />
注意,使用自定义属性必须在布局文件中添加声明:
xmlns:app=“http://schemas.android.com/apk/res-auto”
其中"app"是自定义名字,可随意.但是CircleView中的自定义属性的前缀必须和这里的一致1
2. 继承ViewGroup派生特殊的Layout
这里使用的例子是上一章处理滑动冲突的自定义View:HorizontalScrollViewEx,一个水平方向的ViewPager,他里面的子元素水平滑动的同时,子元素内部还可以竖直滑动
先来看看onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//先判断有没有子元素,如果没有直接将自己的宽高设0
if (childCount == 0) {
setMeasuredDimension(0, 0);
/*
接下来这部分是看宽和高是否用了wrap_content
*/
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
上面代码有不规范的地方
1.没有子元素的时候不应该直接设置宽高为0,而应根据LayoutParams中的宽高来做相应的处理
2.没有考虑到margin和padding
下面是onLayout方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
//从左上角开始放
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
上面的问题还是没有考虑padding和margin
4.4.4 自定义View的思想
- 自定义View的基本功:弹性滑动、滑动冲突、绘制原理等
- 根据分类选择实现思路