知道个人人都知道,以前我写了这个 面试系列宣言,现在好像一直都没有连载,而是隔三差五地来一篇,其实也是由于笔者也能力有限,构思一篇文章须要足够的时间去印证其准确性,而以前的部分就由于印证不够形成了勘误。java
值得注意的是,本系列不会中止的。面试的不少知识点在于平时的积累,但自定义 View 这个东西,就得紧紧掌握了。自定义 View 将分为几期,本期咱们只讲绘制。git
大多数时候,咱们均可以采用官方自带或者 GitHub 上的三方开源库实现各类各样炫酷的效果。但,需求倒是五花八门的,你永远没法改变设计师们的想象力和创造力。而咱们要作的,就是把他们的想象力和创造力变成现实。github
对,我没有写错,本期自定义 View 教程不再是最好的了,由于这期基本是 HenCoder 的浓缩总结版。面试
HenCoder,给高级 Android 工程师的进阶手册 ,笔者也是一直在像追剧同样的追。好像这里确实有了给我凯哥打广告的嫌疑,但把好东西,分享给你们,才是最最重要的。canvas
笔者也是七进七出自定义 View,确实是看了很多教程和书籍,都没有一个很好的自定义 View 能力。而做为 Android 开发中必不可少的能(装)力(逼)手段,也是一个很好的可让咱们在面试以及开发中脱颖而出。微信
废话不能太多,我要开始啦!ide
自定义 View 能够简单的分为三步,绘制、布局、触摸反馈。本期,咱们首先讲绘制。布局
自定义的绘制就是重写绘制方法,其中最经常使用的就是 onDraw()
。(固然有其它的,后面会说起,这里先卖个关子。)而绘制的关键就是 Canvas
的使用:post
Canvas 的绘制类方法:drawXXX() (关键参数:Paint)学习
Canvas 的辅助类方法:范围裁切和几何变换。
自定义绘制的上手很是容易:提早建立好 Paint
对象,重写 onDraw()
,把绘制代码写在 onDraw()
里面,就是自定义绘制最基本的实现。大概就像这样:
Paint paint = new Paint(); @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制一个圆 canvas.drawCircle(300, 300, 200, paint); }
就这么简单。因此关于 onDraw()
其实没什么好说的,一个很普通的方法重写,惟一须要注意的是别漏写了 super.onDraw()
。你可能会点击进去查看到 super.onDraw()
实际上是一个空实现,那可能只是由于你继承的是 View
吧,你继承 View 的其它子类试试?
Canvas
下面的 drawXXX() 系列的方法真没啥好讲的,你想画什么图形直接画就行了。而参数其实也给的很是的明了。你必定要所有了解学习的话,直接能够去看官方文档或者凯哥的 自定义View 1-1
填充颜色:Canvas.drawColor(@ColorInt int color)
画圆:drawCircle(float centerX, float centerY, float radius, Paint paint)
画矩形:drawRect(float left, float top, float right, float bottom, Paint paint)
画点:drawPoint(float x, float y, Paint paint)
批量画点:drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint)
画椭圆:drawOval(float left, float top, float right, float bottom, Paint paint)
画线:drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
画弧线或者扇形:drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
画自定义图形:drawPath(Path path, Paint paint)
画 Bitmap:drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
画文字:drawText(String text, float x, float y, Paint paint)
其中能够看到有很多的坐标值参数,你只须要明白的一点是,在 Android 的绘制中,坐标系是这样的。
值得注意的是:
在画弧线或者扇形中的角度 angle,x 轴正方向为 0°,顺时针方向为正角度,逆时针为负角度。
画弧线或者扇形中的 sweepAngle
参数,表明的是绘制的角度,不要被其它方法误导成了觉得是绘制结束时候的角度,官方为什么在这里作了个变换,其实我也不知道。
drawPath()
方法可能相对其它较难,但倒是自定义 View 实际应用中最多的。很是须要了解其三类方法。这里直接摘抄凯哥的 自定义 View 1-1。
drawBitmap()
方法中有个参数是 Bitmap,友情提示:Bitmap 能够经过 BitmapFactory.decodeXXX()
得到。
Path 能够描述直线、二次曲线、三次曲线、圆、椭圆、弧形、矩形、圆角矩形。把这些图形结合起来,就能够描述出不少复杂的图形。Path 能够归结为两类方法:
直接描述路径,也能够分为两组:
添加子图形:
addXXX()
, 此类方法在特定状况下几个Canvas.drawPath()
等同于Canvas.drawXXX()
。画直线或曲线:
xxxTo()
: 这一组和第一组addXxx()
方法的区别在于,第一组是添加的完整封闭图形(除了addPath()
),而这一组添加的只是一条线。辅助设置或计算,由于应用场景不多,凯哥也只讲了其中一个方法:
Path.setFillType(Path.FillType ft)
设置填充方式
上面有比较多的提到 Paint 这个参数,实际上它是真的很好用,直接在下面讲解。
Paint 真的很重要,在自定义绘制中充当关键角色:画笔,因此咱们天然能够为「画笔」作不少操做,好比设置颜色、绘制模式、粗细等。
Paint.setStyle(Style style) 设置绘制模式
Paint.setColor(int color) 设置颜色
Paint.setStrokeWidth(float width) 设置线条宽度
Paint.setTextSize(float textSize) 设置文字大小
Paint.setAntiAlias(boolean aa) 设置抗锯齿开关
嗯,对,抗锯齿开关还能够直接在 Paint 初始化的时候直接做为构造参数:Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG)
Paint 的 API 大体能够分为 4 类:
颜色
效果
drawText() 相关
初始化
凯哥专门拿了一期对 Paint 作了重点讲解,依然在实际场景应该用处不大,因此须要的直接点击 这里 跳转。
若是你想先知道凯哥都讲了什么,我这里也单独给你总结一下:
Paint.setShader(Shader shader):设置着色器,实际上咱们通常传递的参数不会直接传递
Shader
,而会选择直接传递它的子类,具体效果下面给出。
线性渐变:LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,TileMode tile)
辐射渐变:RadialGradient(float centerX, float centerY, float radius,int centerColor, int edgeColor, @NonNull TileMode tileMode)
扫描渐变:SweepGradient(float cx, float cy, int color0, int color1)
还有不少,就不一一给图了。
BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)
混合着色:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
其中须要注意的是:
Paint.setShader()
优先级高于 Paint.setColor()
系列方法。
最后一个 tile 参数,表明的是断点范围以外的着色规则。它是一个枚举类型,有三种参数。
CLAMP : 直译是「夹子模式」,会在端点以外延续端点处的颜色。
MIRROR : 镜像模式。
REPEAT : 重复模式。
设置颜色过滤能够采用 Paint.setColorFilter(ColorFilter colorFilter)
方法。它的名字已经足够解释它的做用:为绘制设置颜色过滤。颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略,而后 Canvas.drawXXX()
方法会对每一个像素都进行过滤后再绘制出来。
这个其实貌似在拍照或者照片整理类应用上用的比较多,其它方面貌似我还不多遇到过,GitHub 上的库 StyleImageView 诠释的很棒。
这里能够重点说一下:Paint.setStrokeCap(Paint.Cap cap)
,设置线头的形状。线头形状有三种:BUTT
平头、ROUND
圆头、SQUARE
方头。默认为 BUTT
。
虚线是额外加的,虚线左边是线的实际长度,虚线右边是线头。有了虚线做为辅助,能够清楚地看出 BUTT 和 SQUARE 的区别。
Canvas 的文字绘制方法有三个:
drawText()
drawTextRun()
drawTextOnPath()
咱们大多数状况用不了那么多,因此一样这里不作详解,对于始终想追根到底的同窗,一样给你提供了 凯哥的连接。
下面只对部分须要注意的重点总结一下。
drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
其中的参数很简单:text 是文字内容,x 和 y 是文字的坐标。但须要注意:这个坐标并非文字的左上角,而是一个与左下角比较接近的位置。大概在这里:
而若是你像绘制其余内容同样,在绘制文字的时候把坐标填成 (0, 0),文字并不会显示在 View 的左上角,而是会几乎彻底显示在 View 的上方,到了 View 外部看不到的位置:
canvas.drawText(text, 0, 0, paint);
大概是这样:
另外,Canvas.drawText()
只能绘制单行的文字,而不能换行。就算显示不完,也会直接绘制到屏幕外面去。
那若是要换行,得 drawText()
不少次吗?并无,还有一个 StaticLayout
能够完美达到咱们的效果。对于详细使用,这里也很少提了。
对 drawTextRun()
和 drawTextOnPath()
,运用的可能并很少,这里就不说了。
简单提一下设置效果辅助类吧,这个可能直接就有用。
设置文字大小:Paint.setTextSize(float textSize)
设置字体:Paint.setTypeface(Typeface typeface)
,其中的 Typeface 里面涵盖了相关字体。另外,还能够经过 Typeface.createFromAsset(AssetManager mgr, String path)
来设置自定义字体,其中 mgr
能够给 getResources().getAssets()
,path
给文件名字,须要把字体文件 .ttf 放在工程的 res/assets 下,「assets」是新建的专用目录。
设置文字是否加粗: Paint.setFakeBoldText(boolean fakeBoldText)
设置文字是否加删除线:Paint.setStrikeThruText(boolean strikeThruText)
设置文字是否加下划线:Paint.setUnderlineText(boolean underlineText)
设置字体倾斜度:Paint.setTextSkewX(float skewX)
「skewX」 向左倾斜为正。
设置文字横向放缩:Paint.setTextScaleX(float scaleX)
设置字体间距,默认值为 0:Paint.setLetterSpacing(float letterSpacing)
这个不是行间距哦。
设置文字对齐方式:Paint.setTextAlign(Paint.Align align)
,其中「align」有三个值:LEFT
、CENTER
和 RIGHT
,默认值是 LEFT
。
设置绘制所使用的 Locale:Paint.setTextLocale(Locale locale)
/ Paint.setTextLocales(LocaleList locales)
实际上,这些方法基本都在咱们 TextView 里面的。
范围裁切主要采用两个方法:
clipRect()
clipPath()
clipRect()
很简单,只须要传递和 RectF
同样的参数便可。你能够除了裁剪矩形,还想作其它样式的裁剪,惋惜这里只有经过 path 的方法了(我也很奇怪为啥没有看到其它方法),再一次印证了 path 的重要性有木有。
值得注意的是:咱们一般会在范围裁切先后加上 Canvas.save()
和 Canvas.restore()
来及时恢复绘制范围。大概代码是这样。
canvas.save(); canvas.clipRect(left, top, right, bottom); canvas.drawBitmap(bitmap, x, y, paint); canvas.restore();
另外一个值得注意的点是:必定是先作范围裁切操做,再作 Canvas.drawXXX()
操做,顺序放反的话你会发现毛效果都没有。除了裁切,几何变换也是如此。
几何变换的使用大概分为三类:
使用 Canvas
来作常见的二维变换;
使用 Matrix
来作常见和不常见的二维变换;
使用 Camera
来作三维变换
Canvas.translate(float dx, float dy)
平移,其中,dx 和 dy 分别表示横向和纵向的位移。
Canvas.rotate(float degrees, float px, float py)
旋转,其中 degrees
是旋转角度,顺时针为正向,px
和 py
表明轴心坐标。
Canvas.scale(float sx, float sy, float px, float py)
放缩,其中 sx,sy 分别是横向和纵向的放缩倍数,px 、py 为放缩的轴心,这里千万不要受到重载方法 Canvas.scale(float sx,float sy)
的影响。
skew(float sx, float sy)
错切。这里的 sx 和 sy 分别是 x 方向和 y 方向的错切系数。值得注意的是,这里 sx 和 sy 值为 0 的时候表明本身的方向不错切。
再次重申,须要先作了二位变换,再执行 「drawXXX」操做,重要的事情必定会说三遍。
用 Matrix
作常见变换的基本套路
建立 Matrix 对象;
调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas。
Matrix matrix = new Matrix(); ... matrix.reset(); matrix.postTranslate(); matrix.postRotate(); canvas.save(); canvas.concat(matrix); canvas.drawBitmap(bitmap, x, y, paint); canvas.restore();
把 Matrix 应用到 Canvas
有两个方法: Canvas.setMatrix(matrix)
和 Canvas.concat(matrix)
。
Canvas.setMatrix(matrix)
:用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换(注:根据凯哥收到的反馈,不一样的系统中setMatrix(matrix)
的行为可能不一致,因此仍是尽可能用concat(matrix)
吧);
Canvas.concat(matrix)
:用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。
其中须要注意的是:当多个 Matrix
须要用到的时候,你并不须要初始化多个 Matrix
,而能够直接经过调用 Matrix.reset()
对 Matrix
进行重置。
对于采用 Matrix
来实现不规则变换以及采用 Camera
实现三维变换这里也就很少说了,实际遇到的时候,你也能够 点击这里 复习一下呀。
前面讲了一大堆绘制方法,以及范围裁切和变换,咱们这里再说说绘制顺序。
Android 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。好比你在重叠的位置先画圆再画方,和先画方再画圆所呈现出来的结果确定是不一样的:
一般若是咱们继承的是 View 的话,super.onDraw() 只是一个空实现,因此它的位置放在哪儿都没事,甚至直接不要也没事,但反正加上也没啥影响,尽可能仍是加上吧。
因为 Android 的绘制顺序性,当你继承自已经有绘制的其余 View(好比 TextView)的时候,放在 super.onDraw()
上面就意味着绘制代码会被控件的原内容盖住。
还记得我上面卖的关子吗?自定义绘制其实不止 onDraw()
一个方法。onDraw()
只是负责自身主体内容绘制的。而有的时候,你想要的遮盖关系没法经过 onDraw()
来实现,而是须要经过别的绘制方法。
凯哥这块真的写的是太有意思了,因此我也是直接 copy 了过来。
例如,你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你本身的绘制代码,使它可以在内部绘制一些斑点做为点缀:
public class SpottedLinearLayout extends LinearLayout { ... protected void onDraw(Canvas canvas) { super.onDraw(canvas); ... // 绘制斑点 } }
看起来确实没有问题,可是你会发现,当你添加了子 View 以后,你的斑点不见了:
形成这种状况的缘由是 Android 的绘制顺序:在绘制过程当中,每个 ViewGroup 会先调用本身的 onDraw()
来绘制完本身的主体以后再去绘制它的子 View。对于上面这个例子来讲,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成以后,先前绘制的斑点就被子 View 盖住了。
具体来说,这里说的「绘制子 View」是经过另外一个绘制方法的调用来发生的,这个绘制方法叫作:dispatchDraw()
。也就是说,在绘制过程当中,每一个 View 和 ViewGroup 都会先调用 onDraw()
方法来绘制主体,再调用 dispatchDraw()
方法来绘制子 View。
注:虽然 View 和 ViewGroup 都有
dispatchDraw()
方法,不过因为 View 是没有子 View 的,因此通常来讲dispatchDraw()
这个方法只对 ViewGroup(以及它的子类)有意义。
回到刚才的问题:怎样才能让 LinearLayout 的绘制内容盖住子 View 呢?只要让它的绘制代码在子 View 的绘制以后再执行就行了。因此直接执行在 super.dispatchDraw()
的下面便可。
凯哥确实强势,在文章的最后,直接贴图,不能再清晰了,因此我也是直接跳过了其中 N 个环节,直接上图。
注意:
在 ViewGroup 的子类中重写除
dispatchDraw()
之外的绘制方法时,可能须要调用setWillNotDraw(false)
;在重写的方法有多个选择时,优先选择
onDraw()
。
本期的自定义 View 之绘制就到这里结束了,强烈推荐 点击连接 跟着凯哥操,不得挨飞刀。
作不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注个人微信公众号,目前多运营 Android ,尽本身所能为你提高。若是你喜欢,为我点赞分享吧~