为什么选择自定义View来做起头?可能是因为我最有成就感的事情除了大一刚接触编程时就进入了ACM之外,就是大二第一次用画布画出了一整套的游戏并拿得了第一了。所以那就用自定义View来做起笔吧。
前言: 自定义View
什么是自定义View呢?说到底就是一款画布,一直画笔。我们知道大部分的界面控件,无论是LinearLayout,TextView,ListView最终都是继承于View,那么我们对这些控件的种种设置,最终都会被不同控件中的不同逻辑进行处理,然后绘制到我们的画布上。一般系统自带的控件我们是无法操控其绘制的过程,也无法直接修改其内部触发和处理触摸的逻辑,那么只要我们继承重写这个控件,拿到其绘制的过程,理论上我们就可以绘制任何的界面。
一. 初始化一个自定义View
我们从最基础的View来自定义,当然你也可以选择更高一层的诸如TextView或者LInearLayout来进行自定义,那样你就可以在系统控件的基础上进行自定义修改,但是别忘了在重写的方法内调用 super.xxxxx() 来告诉父控件执行原有逻辑哦,还有要记得调用的顺序,这个我们在后面会接着提到。
我们在一个Activity内部添加了一个自定义View —— CustomView,其内部未做任何事情,为了和Activity完全区分开,我们设置其背景色为#dddddd。
可以看到,我们在CustomVIew中未做任何事情,只是实现了其几个关键的构造函数,而且各个构造函数间用 this 链起来,这样就可以在统一的构造方法中进行初始化处理。具体各构造函数和参数的意义可以参见该博客: http://blog.csdn.net/wzy_1988/article/details/49619773
运行后可以看到以下结果:
可以看到带背景的部分是我们自定义的VIew显示区域,目前除了背景外什么都没有。下面我们开始引入canvas和paint来进行绘制。
二、 第一次绘制
1. 规则图形
我们首先引入的绘制函数为 onDraw() 。 Like this :
我们看到引入了一个 Canvas 和 Paint , 其中Canvas为画布,而Paint为画笔。画布可以使用drawxxxx() 来绘制我们想要的东西,无论是线,规则和不规则图形,图像等等。而画笔可以为我们实现各种各样的效果,我们在后续会谈到。首先我们使用画笔在画布上绘制了一个矩形,其位置如注释所诉,那么我们的坐标系是什么呢?跟数学不一样,假设我们认为屏幕就是第一象限,那么在第一象限内,原点为左下角。而在Android中,原点为左上角,所以大家把屏幕放到 第二象限 内就可以了,但是沿着Y轴,向下是自增的。
结果如下:
当然上述的一些 drawxxxx() 方法和 paint 相关设置大家都可以在官方文档中或者直接猜测字面意思也可以了解个七七八八,在这里不再赘述。在开发中要注意这些方法的单位,包括draw的位置和设置的宽度等信息,是PX还是DP还是SP,还有一些圆的角度问题,是弧度还是角度,这些有时候会给我们开发带来一些意想不到的bug。
Paint 类的几个最常用的方法。
Paint.setStyle(Style style)设置绘制模式
Paint.setColor(int color)设置颜色
Paint.setStrokeWidth(float width)设置线条宽度
Paint.setTextSize(float textSize)设置文字大小
Paint.setAntiAlias(boolean aa)设置抗锯齿开关
绘制模式中有 FILL,STROKE,FILL_AND_STROKE三种模式可选,分别对应了 填充,描边,填充并描边 三种模式。我们分别使用三种模式来绘制之前的图形。
设置颜色可以修改我们画笔的颜色,从而绘制出不同颜色的图形,例如这样:
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setColor(Color.RED);
canvas.drawRect(10, 10, 100, 100, mPaint);
效果如下:
设置线条宽度 主要用于我们在绘制描边时,可以设置其宽度大小,例如这样:
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(20);
canvas.drawRect(10, 10, 100, 100, mPaint);
效果如下,这样看起来就比之前的描边线条粗了许多:
掌握了以上一些方法,基本上你就可以进行绘制一些简单的图形图像了。
2.不规则的图形
我们在日常开发中遇到的图形肯定不都是规则的图形,肯定有一些高大炫酷吊炸天的不规则图形让我们去绘制,这时候我们就可以使用到Path来去处理。Path是什么?顾名思义,就是路线,就好像我们在最开始接触绘画程序时拿到的那根铅笔一样,铅笔的轨迹就是我们想要的轨迹。OK,让我们试一下:
path.moveTo(50,50); //移动到 50,50 处
path.lineTo(100,100); // 连线到100,100处
path.lineTo(0,100); // 连线到0,100处
path.close(); //封闭图形,回到第一个点处
canvas.drawPath(path,mPaint); // 绘制路径
效果如下:
如果我们把style改为Fill呢?如果图形不封闭呢?
我们看到,如果类型是FILL,画笔会把我们绘制的图形内部填充成我们设置的颜色,这点跟我们普通的图形一致,而当我们把 path.close() 去掉, 改为 path.lineTo(50,70) ,我们发现其终点会自动跟起点进行连线,然后把封闭的图形填充成我们设置的颜色。
OK,那我们来个复杂的图形,比如我想绘制一个简单的心形 ❤ ,那我们需要先分析一下,心形的组成。为了方便,我们把心简化掉,把其下半部曲线的部分看为直线,上半部分看为两个半圆,这样我们就有了个简单的绘制Path
path.arcTo(new RectF(0,0,200,200),-225,225);
path.arcTo(new RectF(200,0,400,200),-180,225);
path.lineTo(200,300);
canvas.drawPath(path,mPaint);
效果如下:
OK ,看到这里我们就大致了解了自定义VIew绘制的一大部分,绘制规则图形和不规则图形,这些可以帮助你解决80%的自定义VIew需求了。但是如果你没有太多的接触自定义View的理论,可能看起来会比较吃力,特别是不规则图形那一块。那么下面我们就来进一步 的介绍我们上述所使用的一些东西。
二、进阶内容
1. 简单的图层蒙版
在canvas中,我们可以使用drawColor来进行简单的图层蒙版,比如在上一届阶段最后的心形上,加上一层简单的绿色半透明模板,要想人生过得去,程序哪能不带绿!!!
canvas.drawColor(Color.parseColor("#5500ff00"));
于是我们就得到了:
2. 绘制弧形和扇形
我们之前讲到了规则图形的 drawxxxx() 方法 ,其中基础的图形中,扇形和弧形是比较难理解的两部分。在画心的过程中我们有用到弧形的半圆,但我们是使用path来绘制的,与直接的draw是有些许区别。我们先看下draw的函数
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)
public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)
我们看到两个方法中只有前面几个参数不同,其中一个是传入RectF 的变量,一个是传入详细的上左下右的坐标。其实两种可以是等价的,其中RectF也就是表示一个 可以设置上左下右 的矩形。这个矩形就是我们所需要绘制的弧线的所在圆的外切矩形。如图所示:
所以我们在这里可以传入绿色矩形的坐标作为RectF,然后startAngle代表了弧线开始的角度,sweepAngle代表了弧线跨越的角度,useCenter代表了是否与圆心连线闭合。 我们把x轴的正方向作为弧度起始为0的角度,顺时针为正,逆时针为负,比如上图假设我们绘制的是一个120°的弧线,当我们顺时针绘制这个图形时,我们传入为 startAngle 为 -180 , sweepAngle为 120。当我们逆时针绘制这个图形时,startAngle 为 -60 , sweepAngle为 -120。
PS: 注意以上使用的都是角度,也就是我们常说的0~360°代表一圈。而如果大家用到三角函数去计算时,一般传入传出的都是弧度,也就是我们所说的0~2π为一圈。所以别忘了换算。
3. Path的进阶理解
我们在绘制心形的时候使用了Path这个东西,大家可以点出来Path下面有很多的方法,大家可以看到其大多分为了两组,一组是addXXX一组是XXXTo,那这两种有什么区别呢? 顾名思义,一种是add something,这种和上下文是没有太大关系的,而一种是 to something,这种一般需要考虑上下文的关系。当然我们这里没有上下文的概念,所以这里的上下文就是指上一笔的绘制,因为我们是Path,那么就是指上一个路径的终点。但是光说肯定很难理解,我们后面用程序去看一下就知道了。当然,在此之前,我们先看一些前提的东西。我们看到方法中有一些看起来一样的方法,只是前面会不会有一个r,比如 moveTo 和 rMoveTo ,lineTo 和 rLineTo。这里的r 指的是 relative ,也就是我们之前学习的相对布局的相对二字。那就不难理解了吧,比如我们的画笔目前是在 (100,100) 位置处,我们去画一条直线, 我们使用 lineTo(100,200) 和 rLineTo(100,200) 所绘制出来的终点是不一样的。其中lineTo的终点是100,200,而rLineTo的终点是200,300的位置。
mPaint.setColor(Color.RED);
path.moveTo(100,100);
path.lineTo(100,200);
canvas.drawPath(path,mPaint);
path.reset(); // 如果没有reset的话后面绘制的黑色Path会把前面的红色Path给覆盖掉
mPaint.setColor(Color.BLACK);
path.moveTo(100,100);
path.rLineTo(100,200);
canvas.drawPath(path,mPaint);
然后我们来理解addXXX和xxxTo的区别。代码如下:
path.moveTo(100, 100);
path.lineTo(100, 200);
path.addArc(new RectF(0, 0, 100, 100), -120, 120);
path.addCircle(300, 300, 100, Path.Direction.CW);
canvas.drawPath(path, mPaint);
结果如图:
我们看到我们用了一个path,分别绘制了直线,弧线,圆,虽然期间并没有使用moveTo去移动画笔,但是绘制出来的东西没有连笔。然后我们使用xxxTo来试一下:
mPaint.setColor(Color.RED);
path.moveTo(100, 100);
path.lineTo(100, 200);
path.arcTo(new RectF(0, 0, 100, 100), -120, 120);
path.arcTo(new RectF(200, 200, 400, 400), 0, 359);
canvas.drawPath(path, mPaint);
结果如下:
可以看到,xxxTo 方法绘制出来的图形有明显的画笔移动轨迹,直线终点移动到弧线起点,弧线终点移动到圆起点。需要知道的是,在xxxTo的方法中有一个参数为forceMoveTo的bool变量,当传入为true时,代表着强制移动到绘制位置,此时就与add相应的函数等价了。
在理解了Path的基础知识后,我们介绍一下更高一些的知识,在此之前我们先理解一下什么叫做 “非零环绕数原则” 和 “奇-偶规则” 。这是图形学中比较简便的区分点是否在图像内部的算法。其定义如下:
不自交的多边形:
多边形仅在顶点处连接,而在平面内没有其他公共点,此时可以直观的划分内-外部分。
自相交的多边形:
多边形在平面内除顶点外还有其他公共点,此时划分内-外部分需要采用以下的方法。
(1)奇-偶规则(Odd-even Rule):奇数表示在多边形内,偶数表示在多边形外 从任意位置p作一条射线,若与该射线相交的多边形边的数目为奇数,则p是多边形内部点,否则是外部点。
(2)非零环绕数规则(Nonzero Winding Number Rule):若环绕数为0表示在多边形内,非零表示在多边形外 首先使多边形的边变为矢量。将环绕数初始化为零。再从任意位置p作一条射线。当从p点沿射线方向移动时,对在每个方向上穿过射线的边计数,每当多边形的边从右到左穿过射线时,环绕数加1,从左到右时,环绕数减1。处理完多边形的所有相关边之后,若环绕数为非零,则p为内部点,否则,p是外部点。
介绍了这么多,主要用于Path中的setFillType 方法,其参数有WINDING, EVEN_ODD, INVERSE_WINDING,INVERSE_EVEN_ODD四个,其中INVERSE_WINDING,INVERSE_EVEN_ODD 分别为WINDING,EVEN_ODD的反选,所以这里只介绍前两个WINDING和EVEN_ODD。 其中WINDING就可以简要的理解为使用奇偶规则去处理,而EVEN_ODD可以理解为使用非零环绕数规则去处理。具体我们在代码中查看:
path.addCircle(90, 100, 40, Path.Direction.CW);
path.addCircle(110, 100, 40, Path.Direction.CW);
path.setFillType(Path.FillType.EVEN_ODD); // 1
path.setFillType(Path.FillType.WINDING); // 2
path.setFillType(Path.FillType.INVERSE_EVEN_ODD); // 3
path.setFillType(Path.FillType.INVERSE_WINDING); // 4
canvas.drawPath(path, mPaint);
然后我们改变其中一个圆的绘制方向。
path.addCircle(90, 100, 40, Path.Direction.CW);
path.addCircle(110, 100, 40, Path.Direction.CCW);
由此我们可以得出,当Type 为EVEN_ODD 时,绘制的方向并不影响填充,而当Type为 WINDING 时,绘制的方向会影响到最终的效果。我们对WINDING进行详细分析。