Android技能树 — View小结

5,642 阅读18分钟

前言

最近年底了,打算把自己的Android知识都整理一下。

Android技能树系列:

Android基础知识

Android技能树 — 动画小结

Android技能树 — View小结

Android技能树 — Activity小结

Android技能树 — View事件体系小结

Android技能树 — Android存储路径及IO操作小结

Android技能树 — 多进程相关小结

Android技能树 — Drawable小结

Android技能树 — Fragment总体小结

数据结构基础知识

Android技能树 — 数组,链表,散列表基础小结

Android技能树 — 树基础知识小结(一)

算法基础知识

Android技能树 — 排序算法基础小结

这次是相对View做个小结,主要是View的工作原理,绘制流程等。为什么要总结这块,因为平时自定义View的情况多多少少都会遇到,如果能深刻了解这块知识,对自定义View的掌握才能更透彻。有些人可能会说那我肯定不会的,我也不用看这个总结文章了,没关系,我这次写的很简单,基本大家都能理解。看完后,大家应该都会自己写效果不复杂的自定义View和自定义ViewGroup。

PS: 非广告。我本身View的相关知识也是以前从其他地方学到的。我比较推荐这块内容看(Android开发艺术探索 和 扔物线的View相关内容。所以文中有些的知识点也会引用这二块地方。)

如下图所示:我主要是整理了这些相关知识:

脑图下载链接

View小结


我们可以看大分类:

我们知道一个View要绘制好,是要有三步的(我估计百分之99.9的人都知道这三步): measure测量,layout确定位置,然后draw画出来。所以我这次也是主要这三步来说明的。而大家可能看到这里有一个额外的ViewRoot的知识点,主要是给前面的三步做个补充知识。

ViewRoot(补充知识)

ps:不看其实问题也不大,不想了解的直接看本文的主要的measure,layout,draw三步曲。

ViewRoot字面意思是不是让你感觉是整个ViewTree的根节点。错!ViewRoot不是View,它的实现类是ViewRootImpl,它是DecorViewWindowManager之间的纽带。所以ViewRoot更恰当来说是DecorView的“管理者”。

(PS:下次面试官问你ViewRoot是啥,你可别说是ViewTree的根节点。哈哈。)

所以这时候既然开始整个界面要绘制了。明显就是ViewRoot开始发起调用方法,毕竟“管理者”么。所以View的绘制流程是从ViewRootperformTraversals方法开始的。所以performTraversals方法依次调用performMeasure,performLayoutperformDraw三个方法。因为这三个方法及后面的方法调用都差不多,我们以performMeasure为例,performMeasure会调用measure方法,而measure方法又会调用onMeasure方法(PS:是不是就发现了为啥我们平时都是重写onMeasure方法了。),然后又会在onMeasure方法里面去调用所有子View的measure过程。

我们可以看到思维脑图中有提到顶级View就是DecorView,那DecorView是什么呢? DecorView是一个FrameLayout,里面包含了一个竖向的LinearLayout,一般来说这个LinearLayout是有上下二部分(这里具体跟Android SDK和主题有关):

是不是看到了熟悉的Content这个名字,没错。我们在Activity里面设置布局setContentView就是把我们的布局加到这个id为android.R.id.contentFrameLayout里面。

我们现在正式进入View整个绘制流程:

View的大小

大家可以看到,为了方便大家理解,我写了二个现实生活场景故事对比。

故事对比<1>

我们可以看到,我们的气球放到柜子里面,决定气球大小的因素有二个:柜子给它的限制,还有它自身的因素(质量好坏,好的能吹的很大)。而我们的View也是一样的,首先我们用MeasureSpec来决定我们的View大小,那我们的MeasureSpec和气球一样,也受到二个因素的影响:

  1. ViewGroup的影响
  2. 自身的LayoutParams

总结起来就是一句话:在测量过程中,系统会将View的LayoutParams根据父容器ViewGroup所施加的规则下,转换得出相对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的高/宽。

可能大家会问什么是MeasureSpec,别急,我们马上就来介绍

MeasureSpec知识

其实直接看脑图,应该就能看得懂吧,主要是这么几个知识点:

  1. MeasureSpec是由SpecMode和SpecSize组合成的。
  2. SpecMode的种类:UNSPECIFIED,EXACTLY,AT_MOST。
  3. 普通的View是由父容器限制和自身的LayoutParams来生成相应的MeasureSpec,而DecorView因为是顶层View了。我们可以想象哪来的父容器啊,在外面一层就直接是屏幕了,所以是由屏幕的尺寸和自身的LayoutParams决定。

对比故事<2>

没错,通过对比,我们可以发现规律原来很简单。因为我们脑子里面可以用这个气球的对比故事更好的理解。

我做一个总结表格:(要理解上面的分析过程,而不是背下这个表格,背下来没啥意思)

View的测量

通过上面我们已经知道MeasureSpec是用来确定View的测量的,也已经能根据不同的情况来获得相应的MeasureSpec了。那我们的到底应该在哪里去创建MeasureSpec呢?然后给子View去约束呢?

其实奥秘就在我们平时重写的onMeasure()方法中:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}        

我们是不是看到了onMeasure方法里面传入了(int widthMeasureSpec, int heightMeasureSpec),没错,这里传入的二个参数,就是当前你重写这个方法的所在的View(子View或者ViewGroup)的进行过一系列的操作最后获得的MeasureSpec。

那我们拿到这二个参数后,View还是不知道我们到底给它的宽和高是多少。应该肯定最后是我们调用类型:view.setMeasureWidth(XX),view.setMeasureHeight(XX)这样,它才能被设置测量的宽和高。没错,setMeasuredDimension(int measuredWidth, int measuredHeight)方法就是我们用来设置view的测量宽和高。

当然你可能会问,那我如果直接调用这个方法来设置view的宽和高,那我感觉我不用MeasureSpec都没关系啊。比如下面的代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    //没有使用相应的MeasureSpec
    setMeasuredDimension(100,100);
}

没错,我们可以不是通过正规的测量过程来决定测量的宽和高,我们就是任性的直接定了宽高是100。但是这样就不符合规则流程了。而且做出来的东西也不会特别好。比如这时候,你在xml中对你的view设置match_parent,wrap_content,200dp就会都无效,因为代码最后都是用了100。

onMeasure()方法的构成

我们前面提过,自定义View是要重写onMeasure()方法的,我们再仔细分析下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    //我们一般会自己写的代码
    ........
    ........
    .......
    
}

我们可以看到,主要分为二块:

  1. super.onMeasure(),
  2. 自己写的代码。

我们根据不同的情况一步步来看这些代码的作用。

直接继承View.java

super.onMeasure() 分析1 :比如我们的自定义View直接继承了View.java:

public class DemoView extends View {
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    }
}

我们可以查看super.onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),      
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}

我们看到果然调用了setMeasureDimension方法来进行宽高的设置了。


PS:接下来的源码这个分析可以不看,直接看结论。嘿嘿。嘿嘿。我知道很多人都不想看。

我们可以看到主要是三个方法(我们这里就看width的测量):

  1. 先getSuggestedMinimumWidth方法获取了某个值。
  2. 通过getDefaultSize方法来对第一步获取到的值和约束一起处理后,得到最终值。
  3. 通过setMeasuredDimension方法把我们最终的值给赋值进去。

1和2的方法先不看,我们起码知道了。我们最终确定一个View的测量大小,是通过setMeasuredDimension来设置的(其实我感觉我说的废话,看这个方法的名字就很明确了)。

我们再回头来看1中的方法:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

如果我们的View没有设置background,则返回的最小值为mMinWidth(啥是mMinWidth?????就是我们在xml设置的android:minWidth的值)。如果我们设置了background,则获取mBackground.getMinimumWidth()(其实这个方法就是返回Drawable的原始宽度)。最后返回max(mMinWidth, mBackground.getMinimumWidth())二者中的最大值。

我们再来看2中的方法:

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;
}

其实上面我们的MeasureSpec的创建规则会的话,其实应该就能看的懂。如果是specMode是UNSPECIFIED,则返回我们1中的方法getSuggestedMinimumWidth获取到的值,如果是AT_MOSTEXACTLY,则直接返回specSize。(View源码这里的宽度的创建规则和我们前面讲的测量的规则区别就在于,当specMode是UNSPECIFIED的时候,返回的是getSuggestedMinimumWidth的值,而我们是返回了0。)

结论1:如果写的自定义View是直接继承View的,而且写了super.measure(),则会默认给这个View设置了一个测量宽和高(这个宽高是多少?如果没有设置背景,则是xml里面设置的android:minWidth/minHeight(这个属性默认值是0),如果有背景,则取背景Drawable的原始高宽值和android:minWidth/minHeight二者中的较大者。)

继承现有控件

super.onMeasure() 分析2 :比如我们的自定义View继承了现有的控件,比如ImageView.java:

public class Image2View extends ImageView {

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

这时候我们的super.onMeasure()方法调用的就是ImageView里面的onMeasure方法了:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //ImageView 的一大堆计算宽高的代码。
    ......
    ......
    ......
    
    //当然最终肯定要把算好的宽高告诉View
    setMeasuredDimension(widthSize, heightSize);
}

我们发现如果我们的View直接继承ImageView,ImageView已经运行了一大堆已经写好的代码测出了相应的宽高。我们可以在它基础上更改即可。

比如我们的Image2View是一个自定义的正方形的ImageView,:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //这里已经帮我们测好了ImageView的规则下的宽高,并且通过了setMeasuredDimension方法赋值进去了。
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    //我们这里通过getMeasuredWidth/Height放来获取已经赋值过的测量的宽和高
    //然后在ImageView帮我们测量好的宽高中,取小的值作为正方形的边。
    //然后重新调用setMeasuredDimension赋值进去覆盖ImageView的赋值。
    //我们从头到位都没有进行复杂测量的操作,全靠ImageView。哈哈
    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    if (width < height) {
        setMeasuredDimension(width, width);
    } else {
        setMeasuredDimension(height, height);
    }
}

结论2:如果写的自定义View是继承现有控件的,而且写了super.measure(),则会默认使用那个现有控件的测量宽高,你可以在这个已经测量好的宽高上做修改,当然也可以全部重新测过再改掉。

自己写的代码与super.measure的前后位置

super.onMeasure() 分析3:我们写的自己的代码与super.measure的前后位置关系

我们可以看到,不管你是继承View还是现有的控件(比如ImageView),super.onMeasure()中都默认会按照自己的逻辑测量一个宽和高,然后调用setMeasuredDimension()方法赋值进去。

  1. 如果我们的自己的代码写在super.measure前面,那么你写的测量的逻辑测定好宽高,并且赋值后,最终都会再次被super.measure中的setMeasuredDimension()所覆盖。
  2. 如果我们的自己的代码写在super.measure后面,你可以在你继承的父类的测量结果的基础进行更改(当然你不用父类的测量结果也是没关系的),然后再次调用setMeasuredDimension()赋值。
  3. 如果你的测量宽高的逻辑,不是基于你继承的控件的测量的基础上进行,完全由你来重新测定的话,super.onMeasure()不写也不会有问题。

具体实现自定义View的测量

1. 比如我们直接是继承现有的控件,比如ImageView,实现一个正方形的ImageView(上面已经提到过了):
public class Image2View extends ImageView {

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //这里的super.onMeasure()方法里面,已经是调用了ImageView的onMeasure()方法。
        //所以已经进行了测量了。并且在这个方法最后调用了setMeasuredDimension(widthSize, heightSize);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //所以你不写任何东西,这个测量结果都已经确定过了,因为已经执行过了setMeasuredDimension。
        //但比如你想要在ImageView的基础上,让这个ImageView变成一个正方形的ImageView。
        //因为测出来的宽高可能不同,是一个矩形。我们就需要手动的再去设置一次宽和高。
        int width = getMeasuredWidth();//获取ImageView源码里面已经测量好的宽度
        int height = getMaxHeight();//获取ImageView源码里面已经测量好的高度
        if (width < height) {
            setMeasuredDimension(width, width);
        } else {
            setMeasuredDimension(height, height);
        }
    }
}

我们发现,我们是在已经我们继承的现有的控件帮我们测量好宽高后,可以再次在这个已经测量好的宽高的基础上进行更改。我们并没有用到我们前面学到的MeasureSpec的知识,因为super.onMeasure()中已经帮我们把MeasureSpec处理好了。

2. 比如我们自己直接继承了View:
public class CircleView extends View {
    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
        //View测量宽高的三步曲
    
        //1.设置默认值,wrap_content的情况下的值。
        //因为wrap_content只是说不超过某个最大值,如果不设置默认值,效果与Match_parent一样了。
        int defaultWidthSize = 200;
        int defaultHeightSize = 200;
        
        //2.调用resolveSize()方法,把MeasureSpec和我们的默认值放进去
        //这个方法返回一个最终根据你传入的默认值及MeasureSpec共同作用后的最终结果
        defaultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
        defaultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
        
        //调用setMeasuredDimension方法赋值宽和高
        setMeasuredDimension(defaultWidthSize, defaultHeightSize);

    }
}

是不是超级超级超级简单。大家可能就会问,那个resolveSize()方法是什么,怎么这么神奇。

PS:下面的resolveSize()源码分析不看也没啥关系,反正会用就行了。哈哈,不影响使用。

我们可以来看下它的源码:

public static int resolveSize(int size, int measureSpec) {
    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}


public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {

    //1.拿到specMode 和 specSize

    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    
    //2.根据不同的specMode来进行判断最终值是什么
    switch (specMode) {
    
        
        case MeasureSpec.AT_MOST:
            /*
                2.1如果specMode是AT_MOST模式,我们本来应该直接是specSize
                但是如果我们的默认值比我们的specSize大就很尴尬了。气球默认的大小都装不进柜子了。这时候我们View的大小要设置成specSize,如果默认大小比我们的specSize小就没关系,直接为默认值。
            */
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
            
        /*
            2.2如果是EXACTLY,直接就是specSize    
        */
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
            
         /*
           2.3如果是UNSPECIFIED模式,则直接就是我们设的默认值
         */
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
3. ViewGroup的测量

在讲ViewGroup的测量前面,我要提问个问题,大家应该知道了某个View的MeasureSpec在是在onMeasure()方法的参数里面传进来的。我们是直接拿来用了。那又是那里调用了onMeasure()方法帮忙把这二个参数带进来的呢。这二个参数又是哪里生成的呢?

答案就是这个子View的父容器给它的。父容器在他自己的onMeasure()方法里面会根据自己的onMeasure()传进来的MeasureSpec,及这个子View的自身的LayoutParams情况,生成相应的childMeasureSpec,然后调用子View的measure()传递进去的(前面提过,measure()方法会调用onMeasure()方法。)

比如我们写一个圆形排布的ViewGroup(LinearLayout是一排的排布)。

public class CircleLayout extends ViewGroup {
    
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        
        //1.父容器的onMeasure()传进来的二个参数widthMeasureSpec和 heightMeasureSpec
        //2.还差子View的LayoutParams,获取子View的LayoutParams
        //3.通过二者产生新的MeasureSpec然后给子View。
        //4.而产生的新的ChildMeasureSpec的规则就是我们前面表格总结过的规则。
        
        /*
        PS:下面这段是我写的代码,并不是正确的,因为父容器可能包含多个子View,
        所以到某个子View的时候,给它的specSize应该是父容器的剩余空间,
        所以传入的父容器的可用空间本来是不停的减少的,外加还有margin,padding值也要减去。
        我就是主要意思下,让大家懂得原理。
        */
        
        
        //先判断初始时候父容器的大小,因为父容器也是个View,所以也是三步曲。
        //设置默认值(可以是0,因为父容器一般默认不会占有空间)
        int defaultWidthSize = 500;
        int defaultHeightSize = 500;
        //resolveSize处理获取宽和高
        int resultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
        int resultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
        
        //比如我们这里以width为例子:
        //我们前面提过了,最终给子View的MeasureSpec是由父View的MeasureSpec与子View的LayoutParam共同确定。
        //先获取父View的MeasureSpec的mode和size
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
            
        //根据不同的SpecMode及子View的LayoutParams来产生新的ChildMeasureSpec。
        
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            
            LayoutParams params = view.getLayoutParams();
            int childWidthSpec, childHeightSpec;
            
            //先根据父View的MeasureSpec来进行大分类:
            switch (specMode) {
        
                case MeasureSpec.EXACTLY:
                    //说明是固定值,比如100dp等
                    if (params.width >= 0) {
                        resultSize = params.width;
                        resultMode = MeasureSpec.EXACTLY;
                    } else if (childDimension == LayoutParams.MATCH_PARENT) {
                        
                        resultSize = specSize;
                        resultMode = MeasureSpec.EXACTLY;
                    } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              
                        resultSize = specSize;
                        resultMode = MeasureSpec.AT_MOST;
                    }
                
                    break;
                
                case MeasureSpec.AT_MOST:
                    .....
                    .....
                    break;
                    
                case MeasureSpec.UNSPECIFIED:
                    .....
                    .....
                    break;
                
                
            }
            
             childWidthSpec = MeasureSpec.makeMeasureSpec(resultWidthSize, MeasureSpec.EXACTLY);
             
             getChildAt(i).measure(childWidthSpec, childHeightSpec);
            
        }
        
        
        
        /*
            可能有人说,生成新的规则我都懂,但是每次都要写上面一大段的代码,
            我不想写自定义ViewGroup了。我还是放弃吧,别急,大家也发现上面的规则的确是固定的。
            那有没有类似我们在上面设置自己宽高时候的类似resolveSize的方法呢。
            如果没有特定的需求,的确我们不需要写上面一大段。
            有二种方法。
         
        */
        
        
        //方法1:可以通过调用measureChildren()一下子把所有的子View测量好
         measureChildren(widthMeasureSpec, heightMeasureSpec);
        
        //方法2:通过measureChild()一个个来测量。
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            measureChild(view , widthMeasureSpec,heightMeasureSpec);
        }    
        
        //设置父容器的大小
        setMeasuredDimension(XXXX,XXXX)
    
    }
}

没错,最后我们可以用measureChildren(widthMeasureSpec, heightMeasureSpec);measureChild(view , widthMeasureSpec,heightMeasureSpec);方法来,我们也知道它的内部肯定也是根据相应的规则,生成对应的childMeasureSpec,然后调用child的measure方法。

我们可以看下源码(PS:不想看还是没关系,可以跳过):

//measureChildren其实只是帮我们遍历了所有的View,帮我们把可见的View分别调用measureChild方法来处理。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

//而measureChild方法里面就是获取子View的LayoutParams和传进来的MeasureSpec,
//把这二者通过getChildMeasureSpec方法获得一个新的childMeasureSpec,然后传给child.measure方法。
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
           mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

如果具体想看getChildMeasureSpec做了什么,可有再去看下源码,但是他们生成的规则跟我们前面讲的还是一样的。我这里不多说了。

测量完后获取View的宽和高

这个就十分简单了。直接看脑图即可。

View的位置

这块比较简单,我也不多说了。(别吐槽我,这文章太多了。写太多没人会耐心看完。)

View的绘制

我们都知道View的大小和位置都确定好了,肯定就差绘画了。

View 绘画draw()

我们都知道是通过draw()方法来绘制的。

而draw()方法具体做了什么呢,我们可以看源码这个方法的工作过程的介绍:


draw()源码里面的介绍:

/*
 * Draw traversal performs several drawing steps which must be executed
 * in the appropriate order:
 *
 *      1. Draw the background
 *      2. If necessary, save the canvas' layers to prepare for fading
 *      3. Draw view's content
 *      4. Draw children
 *      5. If necessary, draw the fading edges and restore layers
 *      6. Draw decorations (scrollbars for instance)
 */

分别是先绘制背景,然后绘制自己的内容,然后绘制子View的内容,最后画装饰和前景。

推荐大家看扔物线大佬的文章,讲的很清楚,我就不花大篇幅写基础了。

HenCoder Android 自定义 View 1-5: 绘制顺序

Canvas的使用

我们知道不管是onDraw(Canvas canvas),dispatchDraw(Canvas canvas),onDrawForeground(Canvas canvas)等都是参数是Canvas(画布)。所以我们知道了是用Canvas来绘画。

这里也是推荐扔物线大佬的相关文章,讲的很细,我也不再大篇幅的写各种基础使用知识。

HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础

HenCoder Android 开发进阶:自定义 View 1-4 Canvas 对绘制的辅助

Canvas怎么使用呢: 主要分为二大块:

Canvas绘制类方法

这块很简单,直接用Canvas来画颜色,画矩形,画圆形,画直线等各种图形。虽然简单,但毕竟这才是基本的绘制,用的最多。

Canvas的辅助类方法

其中几何变化又分为二维变换和三维变换:

二维变换

三维变换

Paint相关

我们知道Paint是画笔,我们可以设置颜色,画笔粗细等。

继续推荐扔物线大佬的相关文章(基础我就不写了):

HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解

颜色相关

效果

绘制文字相关

Paint初始化部分相关


结语

有错误的地方,请大家轻点喷,我胆子很小的。。。。