有过自定义控件经历的朋友都知道,自定义View的时候所经历的三个方法 onMeasure()、onLayout()、onDraw(), 分别对应 测量(要在多大的地方绘制)、布局(确定位置)、绘制(具体绘制的内容) ;
这个和现实生活中作画是完全能对应上的。
这里先来张过程图:
一个前辈的blog,如果觉得这篇文章分析太浅可以去看看。
这里我弄了个自己理解的草图:
这里省略了很多步骤,不过一个View 显示在Activity或者Fragment上这几个方法肯定是要走的。
onMeasure()
作画的第一步:选择要画在哪里 如:画在A4纸上、墙壁上····
这些东西都有个统一属性,那就是有个范围,比如说在A4纸上画一颗树,如果你画出去就没意义了;
在View绘制中的 measure 也是如此,而在我们的程序界面也不只可能只有一颗树这么一个元素,树上可能有树叶、果实;
如一个View中包含了TextView,EditText、Button 等,所以,需要对View中的每一个元素进行测量,才能更好的对这些元素进行 排版、控制、使用… (想想如果你画一棵树,而树的果实有整颗树那么大的画面吧~是不是很美 O(∩_∩)O哈哈~)
先来个概念性的东西:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
...
}
onMeasure()这个方法中的widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:
- EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 //(match_parent or 200dp;) - AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 // (wrap_content) - UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
上面的解释是不是看得稀里糊涂的,没看懂没关系,下面这个例子一目了然。
一般在自定义控件的时候:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
switch (modeWidth){
case MeasureSpec.EXACTLY: //match_parent or 指定具体值 如:android:layout_width="200dp";
sizeWidth = 200;//这里是随便写的~
break;
case MeasureSpec.AT_MOST: //wrap_content
sizeWidth = 300;
break;
case MeasureSpec.UNSPECIFIED: //该视图大小按照自己的意愿任意大小,没有任何限制
sizeWidth = 400;
break;
}
}
这个想必用过的都不会陌生了,这段代码的意思是:
如果控件的宽度 设置模式为 android:layout_width="match_parent"
或者 android:layout_width="100dp"
就把 该控件的宽度设置为 200px;
而设置为 android:layout_width="warp_content"
则把控件宽度设置为 300px;
至于第三个 case MeasureSpec.UNSPECIFIED 这个我也不知道怎么进去(主要是没用到过,在ScrollView或者ListView这种控件上应该有用到,我就懒得去查了~~)
为了更直观点,来张图吧:
xml:
<?xml version="1.0" encoding="utf-8"?>
<viewdrawprocess.fmr.com.viewdrawprocesstest.MyView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent"
>
</viewdrawprocess.fmr.com.viewdrawprocesstest.MyView>
Java 代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
switch (modeWidth){
case MeasureSpec.EXACTLY: //match_parent or 指定具体值 如:android:layout_width="200dp";
sizeWidth = 200;//这里是随便写的~
break;
case MeasureSpec.AT_MOST: //wrap_content
sizeWidth = 300;
break;
case MeasureSpec.UNSPECIFIED: //该视图大小按照自己的意愿任意大小,没有任何限制
sizeWidth = 400;
break;
}
setMeasuredDimension(sizeWidth, 200);
}
这里只设置了宽度,并且只设置了match_parent 一种模式,其他的模式各位可以自己试试,是否像上面说的一样~
onLayout()
测量完成之后接着就是确定视图的位置了,现在是这样一种情景:
我拿到了一张 1米长,1米宽的纸,想在离纸最上方20厘米,最左边20厘米的地方开始作画;
而这一行为正是onLayout要做的事。
注意:确定位置是对于 子View 而言的;如同作画,画的元素位置是相对于纸张的。
在ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来继续执行:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()方法接收四个参数,分别代表着left、top、right、buttom的坐标(Android只要涉及到四个坐标的,我看过的都是这个顺序~~),而这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把onMeasure测量出的宽度和高度传到了layout()方法中。
继续来看layout();
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
在layout()中,首先会调用setFrame()来判断视图的大小是否发生过变化,来确定有没有必要对当前的视图进行重新layout,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。
接着会调用onLayout回调方法,具体实现由重写了onLayout方法的ViewGroup的子类去实现。
为什么这么说呢? 来看代码
View中的onLayout:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
throw new RuntimeException("Stub!");
}
啥事都没干!
而ViewGroup:
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()方法是一个抽象方法,所以ViewGroup的子类都必须重写这个方法。而像LinearLayout、RelativeLayout这种继承ViewGroup的,都重写了这个方法,然后在内部按照各自的规则对子视图位置进行计算。
好了原理分析完毕,接着来个例子看看是不是如上分析的一样。
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0); //得到第一个添加到 MyViewGroup 的View (这应该很容易理解了~)
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
/**
* @param changed 当前View的大小和位置改变了
* @param left 左部位置(相对于父视图)
* @param top 顶部位置(相对于父视图)
* @param right 右部位置(相对于父视图)
* @param bottom 底部位置(相对于父视图)
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
//childView.getMeasuredWidth() 这个值是是在 onMeasure 中测量出来的值,childView.getMeasuredHeight() 同~。
childView.layout(100, 100, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
逻辑就很简单了,onMeasure 就不多说了;来看onLayout 判断是否有子视图,有则调用子视图的layout(),这里为了有效果,我给了离父视图左、上、分别100px的间距(是不是和margin很像~)。
ok 来看看我们的布局:
<viewdrawprocess.fmr.com.viewdrawprocesstest.MyViewGroup
android:id="@+id/myViewGroup"
android:layout_below="@+id/myView"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:src="@mipmap/ic_launcher" />
</viewdrawprocess.fmr.com.viewdrawprocesstest.MyViewGroup>
效果:
有点小~ 不过不影响,可以看见,我在ImageView 这个控件里没有设置margin 值,它依然有个左、上边距。
onDraw()
measure 和 layout 之后 就是 draw 了 话不多说,直接来看看draw 干了些什么:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
...
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
...
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
}
1、如果有设置背景,则绘制背景
2、保存canvas层
3、绘制自身内容
4、如果有子元素则绘制子元素
5、绘制效果
6、绘制装饰品(scrollbars)
这里只给出了结果,并没有去分析了,因为我觉得源码这东西,看人家分析还不如自己看一遍来着深刻
可以看见 draw() 这个方法有一个 canvas 对象,这个就是“纸张”对象,我们要做的就是往这上面画东西;
老规矩:
public class MyView extends View{
private Paint mPaint;
public MyView(Context context) {
super(context);
init();
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
canvas.drawRect(0 ,0 ,getWidth() / 2 ,getHeight() / 2 ,mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(40);
canvas.drawText("Hello!",0,getHeight() / 4 ,mPaint);
}
}
这里的 Paint 是Android提供给我们的画笔,它提供了丰富的API (反正多到我都不想看~) ,这里只是在 画布上画了一个矩形,,然后再写了个“Hello”,代码很简单。
使用:
<viewdrawprocess.fmr.com.viewdrawprocesstest.MyView
android:id="@+id/myView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent" />
效果:
ok~ 一个简单的自定义控件就这么愉快的完成了,不过看完之后是不是觉得数学不好已经hold不住了~
而且要写好一个自定义控件用到交互需要对View触摸事件进行处理,如果想牛逼点还要加动画…说到这里又是一部,从入门到放弃史~~~!
这篇blog是个人对View绘制的理解,虽然现在各种Blog都对它分析很透彻了,但是那永远是别人的!