这可能是第二好的自定义 View 教程之绘制

简介: 面试系列 不继续了吗?知道我的人都知道,之前我写了这个 面试系列宣言,如今好像一直都没有连载,而是隔三差五地来一篇,其实也是因为笔者也能力有限,构思一篇文章需要足够的时间去印证其准确性,而之前的部分就因为印证不够造成了勘误。

面试系列 不继续了吗?

知道我的人都知道,之前我写了这个 面试系列宣言,如今好像一直都没有连载,而是隔三差五地来一篇,其实也是因为笔者也能力有限,构思一篇文章需要足够的时间去印证其准确性,而之前的部分就因为印证不够造成了勘误。

值得注意的是,本系列不会停止的。面试的很多知识点在于平时的积累,但自定义 View 这个东西,就得牢牢掌握了。自定义 View 将分为几期,本期我们只讲绘制。

为什么我们要学自定义 View?

大多数时候,我们都可以采用官方自带或者 GitHub 上的三方开源库实现各种各样炫酷的效果。但,需求却是五花八门的,你永远无法改变设计师们的想象力和创造力。而我们要做的,就是把他们的想象力和创造力变成现实。

图片来自扔物线

这期怎么变成第二好了?

对,我没有写错,本期自定义 View 教程再也不是最好的了,因为这期基本是 HenCoder 的浓缩总结版。

HenCoder,给高级 Android 工程师的进阶手册 ,笔者也是一直在像追剧一样的追。好像这里确实有了给我凯哥打广告的嫌疑,但把好东西,分享给大家,才是最最重要的。

笔者也是七进七出自定义 View,确实是看了不少教程和书籍,都没有一个很好的自定义 View 能力。而作为 Android 开发中必不可少的能(装)力(逼)手段,也是一个很好的可以让我们在面试以及开发中脱颖而出。

废话不能太多,我要开始啦!

自定义 View 可以简单的分为三步,绘制、布局、触摸反馈。本期,我们首先讲绘制。

自定义 View 绘制的重中之重

自定义的绘制就是重写绘制方法,其中最常用的就是 onDraw()。(当然有其它的,后面会提及,这里先卖个关子。)而绘制的关键就是 Canvas 的使用:

  • Canvas 的绘制类方法:drawXXX() (关键参数:Paint)
  • Canvas 的辅助类方法:范围裁切和几何变换。

一切的开始:onDraw()

自定义绘制的上手非常容易:提前创建好 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() 系列方法的使用

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 真的很重要,在自定义绘制中充当关键角色:画笔,所以我们自然可以为「画笔」做很多操作,比如设置颜色、绘制模式、粗细等。

  • 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 设置着色器。

  • 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 诠释的很棒。

再其它也就没啥好说的,感兴趣直接去看 HenCoder

这里可以重点说一下:Paint.setStrokeCap(Paint.Cap cap),设置线头的形状。线头形状有三种:BUTT 平头、ROUND 圆头、SQUARE 方头。默认为 BUTT

图片来自扔物线

虚线是额外加的,虚线左边是线的实际长度,虚线右边是线头。有了虚线作为辅助,可以清楚地看出 BUTT 和 SQUARE 的区别。

Canvas 的文字绘制

Canvas 的文字绘制方法有三个:

  • drawText()
  • drawTextRun()
  • drawTextOnPath()

我们大多数情况用不了那么多,所以同样这里不做详解,对于始终想追根到底的同学,同样给你提供了 凯哥的链接

下面只对部分需要注意的重点总结一下。

drawText()

  • 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 对文字绘制的辅助

  • 设置文字大小: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」有三个值:LEFTCENTERRIGHT,默认值是 LEFT
  • 设置绘制所使用的 Locale:Paint.setTextLocale(Locale locale) / Paint.setTextLocales(LocaleList locales)

实际上,这些方法基本都在我们 TextView 里面的。

自定义 View 之范围裁切

范围裁切主要采用两个方法:

  • 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 自带方法进行二维变换

  • Canvas.translate(float dx, float dy)
    平移,其中,dx 和 dy 分别表示横向和纵向的位移。
  • Canvas.rotate(float degrees, float px, float py)
    旋转,其中 degrees 是旋转角度,顺时针为正向,pxpy 代表轴心坐标。
  • 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 对象;
  • 调用 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 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。比如你在重叠的位置先画圆再画方,和先画方再画圆所呈现出来的结果肯定是不同的:


图片来自扔物线

到底放在 super.onDraw() 上面还是下面?

通常如果我们继承的是 View 的话,super.onDraw() 只是一个空实现,所以它的位置放在哪儿都没事,甚至直接不要也没事,但反正加上也没啥影响,尽量还是加上吧。

由于 Android 的绘制顺序性,当你继承自已经有绘制的其他 View(比如 TextView)的时候,放在 super.onDraw() 上面就意味着绘制代码会被控件的原内容盖住。

dispatchDraw():绘制子 View 的方法

还记得我上面卖的关子吗?自定义绘制其实不止 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 ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~


nanchen
目录
相关文章
|
4月前
|
XML 前端开发 Java
Android Studio App自定义控件中自定义视图的绘制讲解及实战(附源码 包括自定义绘制各种图形)
Android Studio App自定义控件中自定义视图的绘制讲解及实战(附源码 包括自定义绘制各种图形)
37 1
|
定位技术 API
基于Leaflet.draw的自定义绘制实战
本文介绍了如何基于leaflet.draw进行自定义绘制,同时获取对象的bbox和geojson信息。
514 0
基于Leaflet.draw的自定义绘制实战
|
1月前
|
前端开发
自定义View绘制基础之Canvas
自定义View绘制基础之Canvas
41 0
|
4月前
|
存储 数据可视化 测试技术
[Qt5] QGraphics图形视图框架概述(Item、Scene和View)
[Qt5] QGraphics图形视图框架概述(Item、Scene和View)
151 0
|
Android开发
自定义 View | 画板
自定义 View | 画板
自定义 View | 画板
|
前端开发 Android开发
Android 开发进阶: 自定义 View 1-1 绘制基础
Android 开发进阶: 自定义 View 1-1 绘制基础
108 0
|
程序员
Flutter:如何使用 CustomPaint 绘制心形
作为程序员其实也有浪漫的一幕,今天我们一起借助CustomPaint和CustomPainter绘制心形,本文将带您了解在 Flutter 中使用CustomPaint和CustomPainter绘制心形的端到端示例。闲话少说(比如谈论 Flutter 的历史或它有多华丽),让我们深入研究代码并制作一些东西。
169 0
Flutter:如何使用 CustomPaint 绘制心形
PyQt5 技术篇-QWidget、Dialog设置界面固定大小、不可拉伸方法实例演示
PyQt5 技术篇-QWidget、Dialog设置界面固定大小、不可拉伸方法实例演示
597 0
PyQt5 技术篇-QWidget、Dialog设置界面固定大小、不可拉伸方法实例演示
|
前端开发 vr&ar 容器
Flutter 115: 图解自定义 View 之 Canvas (四) drawParagraph
0 基础学习 Flutter,第一百一十五节:自定义 Canvas 第四节,文本绘制小结!
579 0
Flutter 115: 图解自定义 View 之 Canvas (四) drawParagraph