1.前&ensp言
对View的绘制流程设计思想,运用场景来次梳理。以前学只是套公式用,比较浅。
2.ViewRoot与DecorView
ViewRoot是链接WindowManager和DecorView的纽带,View绘制的三大流程从ViewRoot来开始到完成.它为抽象类,具体实现算ViewRootImpl.
Activity创建完毕会将DecorView添加到WindowManager,同时创建ViewRootImpl与DecorView(顶级View)关联.
ViewRoot的performTraversals方法按顺序调用三个方法,三个方法分别调用顶级View的三大绘制流程,顶级View调用子View的绘制流程.层层下发,完成一个Activity中Window下所有的View的绘制流程
(1)measure得出View的测量宽高。
This is called to find out how big a view should be.
(2)layout得出view的位置与实际宽高,即四个定点坐标与View的最终宽高。
(3)draw完成具体图像绘制。
绘制流程就是这三步的顺序执行:先(1)测量完毕才好确定(2)具体位置,才能做(3)具体图像绘制.
3.MeasureSpec
理解View的测量原理就要先明白MesaureSpec测量参数,或者叫测量规格。
MesaureSpec一个32位的int值,前2位表示MeasureMode,后30位表示MeasureSize。MeasureSize是宽高数值。这样设计的一个好处是节约内存空间,但需要封包与解包.
MeasureMode有3种标识:
UNSPECIFIED标识
父View对自身无限制.一般用于系统内部,实际应用中我们可以忽略此标识。
AT_MOST
表示MeasureSize不确定。
当自身的LayoutParamters指定为wrap_content的时候,就为此标识。MeasureSize值需要View自己处理(可以是固定值也可以是变量),不能超过父view的MeasureSize。不设置为父View的MeasureSize。如TextView的测量值会根据TextSize来计算得出。具有变化性.
EXACTLY
表示MeasureSize是确定的。当自身的LayoutParamters指定为match_parent或者准确值的时候,就为是此标记。
match_parent时MeasureSize为父View的MeasureSize.准确值时MeasureSize就是准确值。
MesaureSpec受自身的LayoutParamters的宽高,margin与父View限制(父的MesaureSpec,padding,已用空间)影响,可以从源码 ViewGroup#measureChildWithMargins看出。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
int widthUsed,int parentHeightMeasureSpec, int heightUsed)
{
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
4.View与ViewGroup的measure过程
View的measure方法带final修饰,子类无法修改。measure中调用onMeasure方法,从源码看是否调用onMesure要经过一些判断条件,常规的View绘制都会满足调用onMeasure方法。
源码的注释表示,真实的测量过程在onMeasure方法。onMeasure可以被子类覆写。
The actual measurement work of a view is performed in
{@link #onMeasure(int, int)}, called by this method. Therefore, only
{@link #onMeasure(int, int)} can and must be overridden by subclasses.
onMeasure()的源码描述覆写此方法必须要调用setMeasureDimension().
When overriding this method, you
* must call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* IllegalStateException
, thrown by
setMeasuredDimension()设置测量值。父View的measureChildlWithMargin调用child的measure方法传入允许的测量值(DecorView由ViewRootImpl给出,为屏幕长宽).子View调用getDefaultSize得出测量值作为setMeasuredDimension的参数
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}```
getDefaultSize()也没干啥事,将MeasureSpec解包,如果是UNSPECIFIED,返回getSuggestedMinimumWidth().有背景返回背景宽高与android:miniWith较大的值,否返回在布局文件中设置的那个从不起作用的android:miniWith。
如果是 AT_MOST与EXACTLY,返回解包的size值。
ViewGroup继承自View,继承了measure与onMeasure方法,未对onMeasure覆写。而是由它的继承者根据自己的布局特性去覆写。比如LinearLayout.
ViewGroup中增加了measureChildren,measureChild,,measureChildWithMargins三个方法,继承者在onMeasure中调用这些方法达到层层测量的效果。measureChildWithMargins与measureChild的区别在于子View的margin值对子View测量是否影响。
看LinearLayout的源码知道,onMesure中测量完子View,再来设置自己的测量值。因为在AT_MOST模式(wrap_content)的时候测量值会受到子View的测量值影响。我们在自定义View直接继承View或者ViewGroup覆写onMeasure方法时也要学LinearLayout,在模式是AT_MOST(wrap_content)的时候给出获取测量值的方式。不做处理测量值将会为父的测量值,也就是wrap_content等同于match_parent.
总结下测量流程:
![2017-04-14 09-57-13屏幕截图.png](http://upload-images.jianshu.io/upload_images/2492300-226786ab2a94078a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
综上有我们在自定义View覆写onMeasure的时候有二个注意点:
(1)自定义View覆写onMeasure的时候不要忘记调用setMeasureDimension().
(2)AT_MOST(wrap_content)的时候给出获取测量值的方式。否则wrap_content效果同match_parent.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
int widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
int heightMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMeasureMode == MeasureSpec.AT_MOST && heightMeasureMode == MeasureSpec.AT_MOST) {
//设置成默认的大小
setMeasuredDimension(mWrapWidth, mWrapHeight);
} else if (widthMeasureMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWrapWidth, heightMeasureSize);
} else if(heightMeasureMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthMeasureSize, mWrapHeight);
}
}
##5.layout过程
ViewRootImpl#performMeasure完成后,执行performLayout过程,确定View树下View们的位置参数,从而View的实际宽高也会在这过程中得出.
**layout方法:**
它的四个参数l,t,r,b.方法描述是相对父View四个顶点的偏移坐标。
public修饰,子类可以覆盖,类的描述中不建议这么做。ViewGroup覆写layout方法,并且加上了final修饰。
layout方法调用setFrame.四个顶点与全局变量保存的顶点比较,判断是否改变.改变则保存成全局变量顶点,将View的宽高size更新,返回true.
setFrame()如果返回true则调用onLayout。
**onlayout方法**
onLayout方法在View中是空实现。ViewGroup中也是空实现,由继承者根据自己的布局特性去覆写。
LinearLayout的onLayout根据布局方向的不同调用layoutVertical与layoutHorizontal,将子View的位置确定(排列好子View)。
看完layout与onLayout的源码设计,知道,要改变View的layout过程,可以直接重写layout,也可以对setFrame重写.
onLayout的用途是父View确定子View的位置.如果自定义ViewGroup就要注意对OnLayout重写实现.在ViewGroup中layout方法是final的.
以layoutVertical()为例。layoutVertical传入4个参数(int left, int top, int right, int bottom)。
1.layoutVertical先处理padding和gravity对子View顶点坐标的影响。
2.遍历子View,处理子View的measure宽高,layoutParams对子View顶点坐标的影响。记录下childTop,childLeft给下个子View使用
3.执行setChildFrame,调用子View的layout()
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
```
一层层下来就完成一个Activity中所有View的layout过程。
在setChildFrame()中传入的width与height是测量宽高。测量宽高在layout之前的measure过程完成赋值。child.layout调用setFrame给mLeft,mTop,mRight,mBottom赋值。实际宽等于mLeft-mRight。正常情况,测量宽高与实际宽高是相等的。但是如果覆写View的layout(l.,t,,r + 100, b + 100)就会导致测量宽高不等于实际宽高。或者某些极端的情况下,需要多次测量,也会导致不一致。
所以,在layout过程中才会确定View的实际宽高.
6.draw过程
分如下几步:
(1)绘制背景background.draw(canvas)
(2)If necessary, save the canvas' layers to prepare for fading
(3)绘制自己(onDraw)
(4)绘制children(dispatchDraw)
(5) If necessary, draw the fading edges and restore layers
(6)绘制装饰(onDrawScrollBars)
源码的英文注释也是按照这个步骤来的,一般只要看1,3,4,6
此方法不建议被覆盖,如果覆盖记得调用super.draw.建议覆盖的是onDraw.onDraw会在第三步调用.第四步如果是ViewGroup,则调用dispatchDraw调用drawChild。而View的dispatchDraw的实现是空的,ViewGroup对dispatchDraw进行了实现。符合是ViewGroup才需要画子View的思想.
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
setWillNotDraw这个标记为true系统会认为View不需要绘制即执行onDraw,那么就会进行相应的优化.View除ViewGroup默认开启的此标记位为true,其他都默认关闭.
如果ViewGroup要绘制要显示关闭这个优化标记位.
7.自定义View的分类
(1)继承View
多用于实现不规则图形,无法通过组合现有的View.需要覆写onDraw()画出想要的效果.因为直接继承View,需要覆写onMeasure,处理wrap_content失效问题.padding也需要在onDraw中处理
(2)继承ViewGroup
现有的布局方式不满足需求才会这样,或者几个View组成一个特定的布局,很少会这样做,
需要覆写onMeasure,处理wrap_content失效问题.,测量完子View再给出自己的测量值.布局过程中根据布局特性对子View进行布局.
(3)继承特定的View
对已有的实现View进行功能扩充.因为被继承的View已经有对绘制流程做处理.如果没有涉及到对绘制流程的修改,则不需要关注绘制流程.有修改则考虑修改对绘制流程的影响,按需灵活处理.
(4)继承特定的ViewGroup
几种View组合会使用这种方式.和(3)一样,绘制过程已有处理,按需灵活处理.
注意事项汇总
(1)wrap_content失效处理
直接继承View与ViewGroup,需要在onMeasure中对wrap_content做处理.
(2)padding与margin处理
直接继承View的控件,需要在onDraw中处理padding,否则padding属性不起作用.
直接继承ViewGroup的需要在onMeasure与onLayout中对padding与子元素的margin做处理,否则属性失效.
(3)尽量不要在View中使用Handler
View内部本身提供了post系列方法,可以替代Handler.
(4)View中有线程或者动画,需要及时停止.
当包含View的Activity退出或者当前View被remove的时,View的onDetachedFromWindow会被调用.在此方法中停止线程或者动画.当View不可见的时候也需要停止线程或者动画.
如果不及时处理,可能会造成内存泄露.
(5)有滑动嵌套,注意滑动冲突
实战自定义View
(1)直接继承View的CricleView
注意处理wrap_content与padding
有时候希望在xml布局文件中给View设置自定义属性.自定义xml属性步骤:
一:在res->values下新建xml.名字随意.我的是:attr.xml
二:在attr.xml中声明属性集合与自定义属性.下面我声明了一个叫"CircleView"的属性集合,定义了circle_color与circle_radius属性
三:在xml布局文件中使用自定义属性
布局xml根节点需要加上
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="missingPrefix"
为什么要加tools:ignore="missingPrefix":
Lint, Android's code analysis tool, doesn't seem to know about support 自定义的属性 , yet. You can safely ignore the error by addingtools:ignore="MissingPrefix"to theViewtag.
使用自定义属性:
四:在View的构造方法中解析获得自定义属性,在绘制流程中使用属性.
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView, defStyleAttr, 0);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
mRadius = a.getDimension(R.styleable.CircleView_circle_radius, 0);
mWrapWidth = mWrapHeight = 2 * (int)mRadius;
a.recycle();
也可以这么写
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout, defStyleAttr, 0);
int count = ta.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = ta.getIndex(i);
//如果引用成AndroidLib 资源都不是常量,无法使用switch case
if (attr == R.styleable.SwipeMenuLayout_swipeEnable) {
isSwipeEnable = ta.getBoolean(attr, true);
} else if (attr == R.styleable.SwipeMenuLayout_ios) {
isIos = ta.getBoolean(attr, true);
} else if (attr == R.styleable.SwipeMenuLayout_leftOrRight) {
isLeftSwipe = ta.getBoolean(attr, true);
}
}
ta.recycle();
}
源码参考
https://github.com/xwpeng/TViewDesign/blob/master/app/src/main/java/android/xwpeng/tviewdesign/view/CircleView.java
(2)直接继承ViewGroup
直接继承ViewGroup处理的地方较多教麻烦.像子View的measureSpe的处理,子View的layout,gravity属性等,都要一一处理好.一般继承现成的ViewGroup就能满足需求.github练习源码中HorizontalScrollViewEx,MyScollView是差不多的,只要看一个熟悉一下继承ViewGroup的一些Layout都要做写什么处理就行.
(3)(4)继承特定的View与ViewGroup,比较简单,绘制流程已经处理过,按需求修改扩充.
后记
github练习源码:https://github.com/xwpeng/TViewDesign
接下来对SwipeMenuLayout与StickLayout进行探索学习,实际运用巩固知识.