05-Paint详解

一、前言

在上一节《Canvas绘图基础详解》我们说到Canvas绘图有三个基本要素:Canvas绘图坐标系以及Paint,上一节我们详细介绍了Canvas绘图坐标系的使用。这一节我们就来详细讲讲Paint的一些使用方式。Paint就是画笔的意思,用于设置绘制风格,如:线宽(笔触粗细)、颜色、透明度和填充风格等等。

二、常用api

  • setFlags(@PaintFlag int flags): 添加标识,用以实现特定的效果
  • reset():重置Paint的所有属性为默认值。相当于重新new一个,不过性能当然高一些。
  • set(Paint src):把 src 的所有属性全部复制过来
  • setAntiAlias(boolean aa): 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。
  • setDither(boolean dither): 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰
  • setStyle(Paint.Style style): 设置画笔的样式,为FILL,FILL_AND_STROKE,或STROKE
  • setStrokeWidth(float width): 当画笔样式为STROKE或FILL_AND_STROKE时,设置笔刷的粗细度
  • setStrokeCap(Paint.Cap cap): 当画笔样式为STROKE或FILL_AND_STROKE时,设置线头的形状, 如圆形样Cap.ROUND,或方形样式Cap.SQUARE
  • setStrokeJoin(Paint.Join join): 设置结合处的样子,Miter:结合处为锐角, Round:结合处为圆弧:BEVEL:结合处为直线
  • setStrokeMiter(float miter):它用于设置 MITER 型拐角的延长线的最大值。
  • setXfermode(Xfermode xfermode): 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果
  • setColor(int color): 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色。
  • setARGB(int a,int r,int g,int b): 设置绘制的颜色,a代表透明度,r,g,b代表颜色值。
  • setAlpha(int a): 设置绘制图形的透明度。
  • setShader(Shader shader): 设置图像效果,使用Shader可以绘制出各种渐变效果
  • setColorFilter(ColorFilter colorfilter): 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
  • setMaskFilter(MaskFilter maskfilter): 设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等
  • setShadowLayer(float radius ,float dx,float dy,int color):在图形下面设置阴影层,产生阴影效果, radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色
  • clearShadowLayer( ):清除阴影层
  • setFilterBitmap(boolean filter): 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作, 加快显示速度,本设置项依赖于dither和xfermode的设置
  • setPathEffect(PathEffect effect) 设置绘制路径的效果,如点画线等

  • setTextSize(float textSize): 设置绘制文字的字号大小
  • setTypeface(Typeface typeface): 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
  • setFakeBoldText(boolean fakeBoldText): 模拟实现粗体文字,设置在小字体上效果会非常差
  • etSubpixelText(boolean subpixelText): 设置该项为true,将有助于文本在LCD屏幕上的显示效果
  • setTextAlign(Paint.Align align): 设置绘制文字的对齐方向
  • setUnderlineText(boolean underlineText): 设置带有下划线的文字效果
  • setStrikeThruText(boolean strikeThruText): 设置带有删除线的效果
  • setTextScaleX(float scaleX): 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果
  • setTextSkewX(float skewX): 设置斜体文字,skewX为倾斜弧度
  • setLetterSpacing(float letterSpacing): 设置字符间的间距

  • float ascent( ):测量baseline之上至字符最高处的距离
  • float descent():baseline之下至字符最低处的距离
  • float getFontSpacing():获取推荐的行间距。
  • getTextBounds(String text, int start, int end, Rect bounds):获取文字的显示范围。
  • float measureText(String text):测量文字的宽度并返回。
  • getTextWidths(String text, float[] widths):获取字符串中每个字符的宽度,并把结果填入参数 widths。
  • int breakText(char[] text, int index, int count, float maxWidth, float[] measuredWidth): 在给出宽度上限的前提下测量文字的宽度。

三、用例测试

绘制效果相关

setFlags(int flags)

常用的flag有如下几种:

1
2
3
4
5
6
7
8
9
    Paint.ANTI_ALIAS_FLAG   //抗锯齿标志
    Paint.FILTER_BITMAP_FLAG    //使位图双线性过滤的标志
    Paint.DITHER_FLAG   //有利抖动
    Paint.UNDERLINE_TEXT_FLAG   //下划线
    Paint.STRIKE_THRU_TEXT_FLAG //删除线
    Paint.FAKE_BOLD_TEXT_FLAG   //加粗
    Paint.LINEAR_TEXT_FLAG  //文本平滑线性缩放
    Paint.SUBPIXEL_TEXT_FLAG    //文本的亚像素定位标志
    Paint.EMBEDDED_BITMAP_TEXT_FLAG //允许在绘制文本时使用位图字体

举个栗子:

设置抗锯齿 mPaint.setFlags(Paint.ANTI_ALIAS_FLAG) 它等价于 mPaint.setAntiAlias(true)

设置多个flag mPaint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.UNDERLINE_TEXT_FLAG) 同时设置抗锯齿和下划线。

设置抗锯齿 setAntiAlias(boolean aa)

抗锯齿是指在图像中,物体边缘总会或多或少的呈现三角形的锯齿,而抗锯齿就是指对图像边缘进行柔化处理,使图像边缘看起来更平滑,更接近实物的物体。

  • true:柔化处理
  • false:不柔化处理

抗锯齿

设置防抖动 setDither(boolean dither)

这个api现在用的不多了,因为现在的 Android 版本的绘制,默认的色彩深度已经是 32 位的 ARGB_8888 ,效果已经足够清晰了。只有当你向自建的 Bitmap 中绘制,并且选择 16 位色的 ARGB_4444 或者 RGB_565 的时候,开启它才会有比较明显的效果。

引用wiki的一张图来展示它的效果:

防抖动

设置Paint的样式 setStyle(Paint.Style style)

画笔样式有三种,默认是Paint.Style.FILL模式:

  • Paint.Style.FILL:填充内部(例如这种模式画一个圆圆内部颜色是被填充的)
  • Paint.Style.STROKE :描边(内部不被填充,可以用来绘制圆环)
  • Paint.Style.FILL_AND_STROKE :填充内部和描边(展示效果和FILL看起来是一样的)
1
2
3
4
5
6
7
8
    mPaint.style = Paint.Style.FILL //设置填充模式
    canvas.drawCircle(200f,300f,100f,mPaint)
    
    mPaint.style = Paint.Style.STROKE   //设置描边模式
    canvas.drawCircle(500f,300f,100f,mPaint)
    
    mPaint.style = Paint.Style.FILL_AND_STROKE  //设置描边加填充模式
    canvas.drawCircle(800f,300f,100f,mPaint)

setStyle

设置画笔的粗细 setStrokeWidth(float width)

设置线条的宽度,单位为像素,默认值是 0。

1
2
3
4
5
6
7
8
9
10
    mPaint.style = Paint.Style.STROKE   //设置描边模式

    mPaint.strokeWidth = 1f
    canvas.drawCircle(200f,300f,100f,mPaint)

    mPaint.strokeWidth = 10f
    canvas.drawCircle(500f,300f,100f,mPaint)

    mPaint.strokeWidth = 40f
    canvas.drawCircle(800f,300f,100f,mPaint)

setStrokeWidth

线条宽度 0 和 1 的区别
默认情况下,线条宽度为 0,但你会发现,这个时候它依然能够画出线,线条的宽度为 1 像素。那么它和线条宽度为 1 有什么区别呢?
其实这个和「几何变换」有关:你可以为 Canvas 设置 Matrix 来实现几何变换(如放大、缩小、平移、旋转),在几何变换之后 Canvas 绘制的内容就会发生相应变化,包括线条也会加粗,例如 2 像素宽度的线条在 Canvas 放大 2 倍后会被以 4 像素宽度来绘制。而当线条宽度被设置为 0 时,它的宽度就被固定为 1 像素,就算 Canvas 通过几何变换被放大,它也依然会被以 1 像素宽度来绘制。Google 在文档中把线条宽度为 0 时称作「hairline mode(发际线模式)」。

设置线头的形状 setStrokeCap(Paint.Cap cap)

线头形状有三种,默认为 Paint.Cap.BUTT :

  • Paint.Cap.BUTT: 平头
  • Paint.Cap.ROUND: 圆头
  • Paint.Cap.SQUARE: 方头

当线条的宽度是 1 像素时,这三种线头的表现是完全一致的,全是 1 个像素的点;而当线条变粗的时候,它们就会表现出不同的样子:

1
2
3
4
5
6
7
8
9
    mPaint.strokeWidth = 40f
    mPaint.strokeCap = Paint.Cap.BUTT
    canvas.drawLine(100f,100f,800f,100f,mPaint)

    mPaint.strokeCap = Paint.Cap.ROUND
    canvas.drawLine(100f,200f,800f,200f,mPaint)

    mPaint.strokeCap = Paint.Cap.SQUARE
    canvas.drawLine(100f,300f,800f,300f,mPaint)

setStrokeCap

图上的红色直线是额外加上便于理解的,有了红线作为辅助,可以清晰的看出线的三种线头的区别。

设置拐角的形状 setStrokeJoin(Paint.Join join)

拐角形状有三种,默认为 Paint.Join.MITER :

  • Paint.Join.MITER:尖角
  • Paint.Join.BEVEL:平角
  • Paint.Join.ROUND:圆角
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 40f
    //画一个三角形
    val path = Path().apply {
        moveTo(100f,100f)
        lineTo(400f,100f)
        lineTo(400f,300f)
        close()
    }

    mPaint.strokeJoin = Paint.Join.MITER    //尖角
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,300f)
    mPaint.strokeJoin = Paint.Join.BEVEL    //平角
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,300f)
    mPaint.strokeJoin = Paint.Join.ROUND    //圆角
    canvas.drawPath(path,mPaint)

setStrokeJoin

setStrokeMiter(float miter)

这个方法是对于 setStrokeJoin() 的一个补充,它用于设置 MITER 型拐角的延长线的最大值。所谓「延长线的最大值」,是这么一回事:

当线条拐角为 MITER 时,拐角处的外缘需要使用延长线来补偿:

setStrokeMiter1

而这种补偿方案会有一个问题:如果拐角的角度太小,就有可能由于出现连接点过长的情况。比如这样:

setStrokeMiter2

所以为了避免意料之外的过长的尖角出现, MITER 型连接点有一个额外的规则:当尖角过长时,自动改用 BEVEL 的方式来渲染连接点。例如上图的这个尖角,在默认情况下是不会出现的,而是会由于延长线过长而被转为 BEVEL 型连接点:

setStrokeMiter3

setStrokeMiter(miter) 方法中 miter 参数是对于转角长度的限制,具体来讲,是指尖角的外缘端点和内部拐角的距离与线条宽度的比:

setStrokeMiter4

用几何知识很容易得出这个比值的计算公式:如果拐角的大小为 θ ,那么这个比值就等于 1/sin(θ/2) 。

这个 miter limit 的默认值是 4,对应的是一个大约 29° 的锐,默认情况下,大于这个角的尖角会被保留,而小于这个夹角的就会被「削成平头」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 40f

    //画一个等边直角三角形
    val path = Path().apply {
        moveTo(100f,100f)
        lineTo(300f,100f)
        lineTo(100f,300f)
        close()
    }
    //1度=π/180≈0.01745弧度,1弧度=180/π≈57.3度
    mPaint.strokeMiter = (1 / sin(20 * Math.PI/180)).toFloat()  //大于这个40°的尖角会被保留,小于的就被削成平头
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,300f)
    mPaint.strokeMiter = (1/sin(30 * Math.PI/180)).toFloat()    //大于这个60°的尖角会被保留,小于的就被削成平头
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,300f)
    mPaint.strokeMiter = (1/sin(60*Math.PI/180)).toFloat()  //大于这个120°的尖角会被保留,小于的就被削成平头
    canvas.drawPath(path,mPaint)

setStrokeMiter

setXfermode(Xfermode xfermode)

在SDK中 Xfermode 有三个子类:AvoidXfermode, PixelXorXfermodePorterDuffXfermode,前两个类在API 16被遗弃了,这里不作介绍。PorterDuffXfermode 类主要用于图形合成时的图像过渡模式计算,其概念来自于1984年在ACM SIGGRAPH计算机图形学出版物上发表了”Compositing digital images(合成数字图像)”的Tomas Porter和Tom Duff,合成图像的概念极大地推动了图形图像学的发展,PorterDuffXfermode 类名就来源于这俩人的名字组合PorterDuff。而这篇论文所论述的操作,都是关于 Alpha 通道(也就是我们通俗理解的「透明度」)的计算的,后来人们就把这类计算称为Alpha 合成 ( Alpha Compositing ) 。

PorterDuffXfermode 只有这一个构造方法 PorterDuffXfermode(PorterDuff.Mode mode),里面传入了一个PorterDuff.Mode,下面是android SDK中PorterDuffMode枚举类型定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public enum Mode {
    //清除模式[0,0],即最终所有点的像素的alpha 和color 都为 0,所以画出来的效果只有白色背景
    CLEAR       (0),
    //只保留源图像的 alpha 和 color ,所以绘制出来只有源图 (先绘制的是目标图,后绘制的是源图)
    SRC         (1),
    //只保留目标图像的 alpha 和 color,所以绘制出来的只有目标图
    DST         (2),
    //在目标图片顶部绘制源图像,从命名上也可以看出来就是把源图像绘制在上方
    SRC_OVER    (3),
    //将目标图像绘制在上方
    DST_OVER    (4),
    //在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响
    SRC_IN      (5),
    //在两者相交的地方绘制目标图像,并且绘制的效果会受到源图像对应地方透明度的影响
    DST_IN      (6),
    //在不相交的地方绘制源图像,相交处根据目标alpha进行过滤,目标色完全不透明时则完全过滤,完全透明则不过滤
    SRC_OUT     (7),
    //在不相交的地方绘制目标图像,相交处根据源图像alpha进行过滤,完全不透明处则完全过滤,完全透明则不过滤
    DST_OUT     (8),
    //源图像和目标图像相交处绘制源图像,不相交的地方绘制目标图像,并且相交处的效果会受到源图像和目标图像alpha的影响
    SRC_ATOP    (9),
    //源图像和目标图像相交处绘制目标图像,不相交的地方绘制源图像,并且相交处的效果会受到源图像和目标图像alpha的影响
    DST_ATOP    (10),
    //在不相交的地方按原样绘制源图像和目标图像,相交的地方受到对应alpha和色值影响
    XOR         (11),
    //该模式处理过后,会感觉效果变暗,即进行对应像素的比较,取较暗值,如果色值相同则进行混合
    DARKEN      (12),
    //该模式处理过后,会感觉效果变亮,如果在均完全不透明的情况下 ,色值取源色值和目标色值中的较大值
    LIGHTEN     (13),
    //正片叠底,即查看每个通道中的颜色信息,并将基色与混合色复合。结果色总是较暗的颜色。任何颜色与黑色复合产生黑色。任何颜色与白色复合保持不变。当用黑色或白色以外的颜色绘画时,绘画工具绘制的连续描边产生逐渐变暗的颜色。
    MULTIPLY    (14),
    //滤色
    SCREEN      (15),
    //饱和度叠加
    ADD         (16),
    //叠加
    OVERLAY     (17);
    Mode(int nativeInt) {
        this.nativeInt = nativeInt;
    }
    /**
     * @hide
     */
    public final int nativeInt;
}

具体来说, PorterDuff.Mode 一共有 18 个,可以分为两类:

  1. Alpha 合成 (Alpha Compositing)
  2. 混合 (Blending)

第一类,Alpha 合成,其实就是 PorterDuff 这个词所指代的算法,一共描述了 12 种将两个图像共同绘制的操作(即算法)。
看下效果图(引用自Google的官方文档):

source_dest

Alpha 合成:

Alpha合成

第二类,混合,也就是 Photoshop 等制图软件里都有的那些混合模式(multiply darken lighten 之类的)。这一类操作的是颜色本身而不是 Alpha 通道,并不属于 Alpha 合成,所以和 PorterDuff 这两个人也没什么关系,不过为了使用的方便,它们同样也被 Google 加进了 PorterDuff.Mode 里。

效果图:

Blending_modes

还有个 ADD 模式不知道为什么没有加在图上,我们待会儿用代码直接演示一下。

“Xfermode” 其实就是 “Transfer mode”,用 “X” 来代替 “Trans” 是一些美国人喜欢用的简写方式。严谨地讲, Xfermode 指的是你要绘制的内容和 Canvas 的目标位置的内容应该怎样结合计算出最终的颜色。但通俗地说,其实就是要你以绘制的内容作为源图像,以 View 中已有的内容作为目标图像,选取一个 PorterDuff.Mode 作为绘制内容的颜色处理方案。就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    val source = BitmapFactory.decodeResource(resources, R.mipmap.source)   //源图
    val dst = BitmapFactory.decodeResource(resources, R.mipmap.destination) //目标图
    val mode = PorterDuff.Mode.ADD  //PorterDuff 混合模式 剩下的自己修改mode执行代码
    val xfermode = PorterDuffXfermode(mode)

    val rect = RectF(0f,0f,canvas.width.toFloat(),canvas.height.toFloat())
    val saveCount = canvas.saveLayer(rect, mPaint)  //将绘制操作保存到新的图层 (离屏缓冲)

    val bitmapRect = RectF(0f,0f,source.width.toFloat(),source.height.toFloat())
    canvas.drawBitmap(dst,null,bitmapRect,mPaint) //绘制目标图
    mPaint.setXfermode(xfermode)    //设置混合模式
    canvas.drawBitmap(source,null,bitmapRect,mPaint) //绘制源图

    mPaint.setXfermode(null)    //清除混合模式
    canvas.restoreToCount(saveCount)    //还原画布

PorterDuff_ADD

Xfermode 使用很简单,不过需要注意的是必须要使用离屏缓冲(Off-screen Buffer),使用离屏缓冲有两种方式:

  1. Canvas.saveLayer() 可以做短时的离屏缓冲。使用方法很简单,在绘制代码的前后各加一行代码,在绘制之前保存,绘制之后恢复,见上面示例代码。

  2. View.setLayerType() 是直接把整个 View 都绘制在离屏缓冲中。 setLayerType(LAYER_TYPE_HARDWARE) 是使用 GPU 来缓冲, setLayerType(LAYER_TYPE_SOFTWARE) 是直接直接用一个 Bitmap 来缓冲.

如果没有特殊需求,一般选用第一种方法 Canvas.saveLayer() 来设置离屏缓冲,以此来获得更高的性能。

我们使用混合模式做一个简单的loading:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    private val mBitmap = BitmapFactory.decodeResource(resources, R.mipmap.loading)
    private val mXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)  //SRC_IN混合模式
    private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        isFilterBitmap = true //双线性过滤
        color = Color.parseColor("#ff7400")
    }

    private val mSrcRect = Rect(0,0,mBitmap.width,mBitmap.height)
    private val mDestRect = RectF(0f,0f,mBitmap.width.toFloat(),mBitmap.height.toFloat())
    private val mDynamicRect = RectF(0f,mBitmap.height.toFloat(),mBitmap.width.toFloat(),mBitmap.height.toFloat())
    private var mTotalWidth = 0
    private var mTotalHeight = 0
    private var mCurrentTop = mBitmap.height.toFloat() //矩形当前高度

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(mBitmap.width+20,mBitmap.height+20)
    }
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mTotalWidth = w
        mTotalHeight = h
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //存为新图层
        val saveCount = canvas.saveLayer(0f, 0f, mTotalWidth.toFloat(), mTotalHeight.toFloat(), mPaint)
        //绘制目标图
        canvas.drawBitmap(mBitmap,mSrcRect,mDestRect,mPaint)
        //设置混合模式
        mPaint.setXfermode(mXfermode)
        //绘制源图
        canvas.drawRect(mDynamicRect,mPaint)
        //清除混合模式
        mPaint.setXfermode(null)
        //恢复图层
        canvas.restoreToCount(saveCount)

        mCurrentTop -= 1
        if (mCurrentTop <= 0) {
            //从底往上循环
            mCurrentTop = mBitmap.height.toFloat()
        }
        mDynamicRect.top = mCurrentTop
        postInvalidate()
    }

效果如下:

PoterDuffLoading

利用 Xfermode 可以实现很多效果,例如橡皮擦效果等,看大家脑洞了。

设置基本颜色 setColor(int color)

方法名和使用方法都非常简单直接:

1
2
3
4
5
6
7
8
9
    mPaint.strokeWidth = 40f
    mPaint.color = Color.RED
    canvas.drawLine(100f,100f,800f,100f,mPaint)

    mPaint.color = Color.parseColor("#FF9300")
    canvas.drawLine(100f,200f,800f,200f,mPaint)

    mPaint.color = Color.BLUE
    canvas.drawLine(100f,300f,800f,300f,mPaint)

setColor

设置基本颜色 setARGB(int a, int r, int g, int b)

1
2
3
4
5
6
7
8
9
    mPaint.strokeWidth = 40f
    mPaint.setARGB(100,255,0,0)
    canvas.drawLine(100f,100f,800f,100f,mPaint)

    mPaint.setARGB(100,0,255,0)
    canvas.drawLine(100f,200f,800f,200f,mPaint)

    mPaint.setARGB(255,0,0,255)
    canvas.drawLine(100f,300f,800f,300f,mPaint)

setARGB

设置着色器 setShader(Shader shader)

除了直接设置颜色, Paint 还可以使用 Shader

Shader 的中文叫做「着色器」,也是用于设置绘制颜色的。「着色器」不是 Android 独有的,它是图形领域里一个通用的概念,它和直接设置颜色的区别是,着色器设置的是一个颜色方案,或者说是一套着色规则。当设置了 Shader 之后,Paint 在绘制图形和文字时就不使用 setColor/ARGB() 设置的颜色了,而是使用 Shader 的方案中的颜色。

Android 的绘制里使用 Shader ,并不直接用 Shader 这个类,而是用它的几个子类。具体来讲有 LinearGradient RadialGradient SweepGradient BitmapShader ComposeShader 这么几个:

线性渐变 LinearGradient

设置两个点和两种颜色,以这两个点作为端点,使用两种颜色的渐变来绘制颜色。

构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    /**
     * @param x0       渐变线开始的x坐标
     * @param y0       渐变线开始的y坐标
     * @param x1       渐变线结束的x坐标
     * @param y1       渐变线结束的y坐标
     * @param color0   渐变开始的颜色
     * @param color1   渐变结束的颜色
     * @param tile     渐变开始和结束点范围之外的着色规则
     */
    public LinearGradient(float x0, float y0, float x1, float y1,int color0, int color1,TileMode tile)

    /**
     * @param x0       渐变线开始的x坐标
     * @param y0       渐变线开始的y坐标
     * @param x1       渐变线结束的x坐标
     * @param y1       渐变线结束的y坐标
     * @param colors   颜色数组
     * @param positions    颜色数组中每个颜色对应的相对位置,取值范围(0~1),positions数组元素的个数一定colors数组的个数相同如果为null,则沿渐变线均匀分布
     * @param tile         渐变开始和结束点范围之外的着色规则
     */
    public LinearGradient(float x0, float y0, float x1, float y1, @NonNull int[] colors,@Nullable float[] positions, @NonNull TileMode tile)

    ...

第一种构造方法演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    //CLAMP 渐变开始和结束点范围之外 用相邻位置的颜色填充
    val shader = LinearGradient(100f,100f,500f,200f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.CLAMP)
    mPaint.shader = shader
    canvas.drawRect(100f,100f,900f,200f,mPaint)

    //MIRROR    镜像填充
    val shader1 = LinearGradient(100f,300f,500f,400f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.MIRROR)
    mPaint.shader = shader1
    canvas.drawRect(100f,300f,900f,400f,mPaint)

    //REPEAT    重复填充
    val shader2 = LinearGradient(100f,500f,500f,600f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.REPEAT)
    mPaint.shader = shader2
    canvas.drawRect(100f,500f,900f,600f,mPaint)

    //DECAL api >= 31 仅填充渐变开始和结束点范围。如果超出其原始边界,则绘制透明黑色。
    val shader3 = LinearGradient(100f,700f,500f,800f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.DECAL)
    mPaint.shader = shader3
    canvas.drawRect(100f,700f,900f,800f,mPaint)

LinearGradient

我们用第二种构造方法做一个文字效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

    private var mTextPaint: TextPaint? = null
    private lateinit var mLinearGradient: LinearGradient
    private var deltax = 20
    private var mTranslate = 0f
    private val mMatrix = Matrix()

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)

        //拿到TextView的paint
        mTextPaint = paint
        val text = text.toString()
        //测量文本宽度
        val textWidth = mTextPaint!!.measureText(text)
        val gradientSize = textWidth/text.length * 3

        //从左边-gradientSize开始,即左边距离文字gradientSize开始渐变
        mLinearGradient = LinearGradient(-gradientSize,0f,0f,0f, intArrayOf(0x22ffffff,
            0xffffffff.toInt(), 0x22ffffff),null,Shader.TileMode.CLAMP)
        mTextPaint!!.shader = mLinearGradient
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        mTranslate += deltax
        val textWidth = mTextPaint!!.measureText(text.toString())
        if (mTranslate > textWidth + 1 || mTranslate < 1) {
            //先从前往后渐变 再从后往前
            deltax = -deltax
        }
        mMatrix.reset()
        mMatrix.setTranslate(mTranslate,0f)
        mLinearGradient.setLocalMatrix(mMatrix)
        postInvalidateDelayed(50)
    }

LinearGradient2

辐射渐变 RadialGradient

辐射渐变很好理解,就是从中心向周围辐射状的渐变。

构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
    /**
     * @param centerX     辐射中心的x坐标
     * @param centerY     辐射中心的y坐标
     * @param radius      辐射半径
     * @param centerColor 辐射中心的颜色
     * @param edgeColor   辐射边缘的颜色
     * @param tileMode    辐射范围之外的着色模式
     */
    public RadialGradient(float centerX, float centerY, float radius,
            @ColorInt int centerColor, @ColorInt int edgeColor, @NonNull TileMode tileMode)

    ...

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    //CLAMP
    val shader = RadialGradient(300f,300f,150f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.CLAMP)
    mPaint.shader = shader
    canvas.drawCircle(300f,300f,150f,mPaint)

    //MIRROR
    val shader1 = RadialGradient(300f,600f,50f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.MIRROR)
    mPaint.shader = shader1
    canvas.drawCircle(300f,600f,150f,mPaint)

    //REPEAT
    val shader2 = RadialGradient(300f,900f,50f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.REPEAT)
    mPaint.shader = shader2
    canvas.drawCircle(300f,900f,150f,mPaint)

    //DECAL
    val shader3 = RadialGradient(300f,1200f,50f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.DECAL)
    mPaint.shader = shader3
    canvas.drawCircle(300f,1200f,150f,mPaint)

RadialGradient

扫描渐变 SweepGradient

构造方法:

1
2
3
4
5
6
7
8
9
    /**
     * @param cx       扫描的中心x坐标
     * @param cy       扫描的中心y坐标
     * @param color0   扫描的起始颜色
     * @param color1   扫描的终止颜色
     */
    public SweepGradient(float cx, float cy, @ColorInt int color0, @ColorInt int color1)

    ...

示例:

1
2
3
4
    val shader = SweepGradient(300f,300f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"))
    mPaint.shader = shader
    canvas.drawCircle(300f,300f,200f,mPaint)

SweepGradient

BitmapShader

Bitmap 来着色,其实也就是用 Bitmap 的像素来作为图形或文字的填充.

1
2
3
4
5
6
7
8
9
10
    val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.beauty1)
    //CLAMP 当所画图形的尺寸大于bitmap的尺寸,会用bitmap和剩余空间相邻位置的颜色填充剩余空间
    //当所画图形的尺寸小于bitmap的尺寸,会对bitmap进行裁剪,利用这个原理,我们可以去制造圆形头像
    val shader = BitmapShader(bitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP)
    mPaint.shader = shader
    canvas.drawCircle(400f,400f,300f,mPaint)

    //另外几种填充方式和之前渐变一样,这儿不再赘述
//        val shader = BitmapShader(bitmap,Shader.TileMode.MIRROR,Shader.TileMode.MIRROR)
//        canvas.drawRect(0f,0f,canvas.width.toFloat(),canvas.height.toFloat(),mPaint)

BitmapShader

组合着色器 ComposeShader

构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    /**
     * @param shaderA  目标shader
     * @param shaderB  源shader
     * @param mode     两个Shader的PorterDuff结合模式,即shaderA和shaderB应该怎样共同绘制
     *
     */
    public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB,
            @NonNull PorterDuff.Mode mode)


    /**
     * 需要api >= 29
     * @param shaderA  目标shader
     * @param shaderB  源shader
     * @param blendMode 两个Shader的BlendMode结合模式,即shaderA和shaderB应该怎样共同绘制
    */
    public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB,
            @NonNull BlendMode blendMode)

shaderAshaderB 参数都很简单, PorterDuff.Mode我们在上面已经讲过了,这儿不再赘述。而 BlendMode 需要在API 29 以上才能使用,这儿不做介绍,需要了解的自行查看官方文档

注意:ComposeShader 内部想使用相同类型的着色器(比如说使用两个BitmapShader混合)需要api26以上才支持硬件加速功能,如果api26以下需要关闭硬件加速才能看到效果。使用不同的着色器不影响。

示例:

1
2
3
4
5
6
7
8
9
10
11
    val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.beauty1)
    //第一个bitmapShader
    val bitmapShader = BitmapShader(bitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP)
    //第二个辐射渐变shader
    val radialShader = RadialGradient(400f,300f,100f,Color.parseColor("#FF7400"),
        Color.parseColor("#BB86FC"),Shader.TileMode.CLAMP)
    // ComposeShader:结合两个 Shader
    val shader = ComposeShader(bitmapShader,radialShader,PorterDuff.Mode.MULTIPLY)

    mPaint.shader = shader
    canvas.drawCircle(400f,400f,300f,mPaint)

ComposeShader

setColorFilter(ColorFilter colorFilter)

为绘制设置颜色过滤,颜色过滤的意思就是为绘制的内容设置一个统一的过滤策略,然后 Canvas.drawXXX() 方法会对每个像素都进行过滤后再绘制出来,常用来实现图片滤镜效果。

ColorFilter 并不直接使用,需要使用它的子类。它共有4个子类:LightingColorFilter PorterDuffColorFilterBlendModeColorFilterColorMatrixColorFilter

LightingColorFilter

一个颜色滤镜,可以用来模拟简单的光照效果。LightingColorFilter 的构造方法是 LightingColorFilter(int mul, int add) ,参数里的 muladd 都是和颜色值格式相同的 int 值,其中 mul 用来和目标像素相乘,add 用来和目标像素相加:

1
2
3
R' = R * mul.R / 0xff + add.R
G' = G * mul.G / 0xff + add.G
B' = B * mul.B / 0xff + add.B

基于上面的计算规则,可以知道,一个保持原样的 LightingColorFiltermul0xffffffadd0x000000(也就是0),那么对于一个像素,它的计算过程就是:

1
2
3
R' = R * 0xff / 0xff + 0x0 = R // R' = R
G' = G * 0xff / 0xff + 0x0 = G // G' = G
B' = B * 0xff / 0xff + 0x0 = B // B' = B

我们知道了计算规则,那么就可以稍微修改下传入值,做出各种各样的filter:

1
2
3
4
5
6
//移除红色 R' = R * 0x0 / 0xff + 0x0 = 0
val lightingColorFilter = LightingColorFilter(0x00ffff, 0x000000)
//移除绿色 G' = G * 0x00 / 0xff + 0x0 = 0
val lightingColorFilter = LightingColorFilter(0xff00ff, 0x000000)
//移除蓝色 B' = B * 0x00 / 0xff + 0x0 = 0
val lightingColorFilter = LightingColorFilter(0xffff00, 0x000000)

或者增强原来的色彩效果:

1
2
3
4
5
6
7
8
    val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.beauty1)
    //绘制原图
    canvas.drawBitmap(bitmap,100f,100f,mPaint)
//        val lightingColorFilter = LightingColorFilter(0x00ffff, 0x000000)   //移除红色
    val lightingColorFilter = LightingColorFilter(0xffffff, 0x404040)   //原图所有颜色效果增强
    mPaint.setColorFilter(lightingColorFilter)
    //绘制过滤颜色后的图
    canvas.drawBitmap(bitmap,100f,150f+bitmap.height,mPaint)

setColorFilter

PorterDuffColorFilter

这个 PorterDuffColorFilter 的作用是使用一个指定的颜色和一种指定的 PorterDuff.Mode 来与绘制对象进行合成。它的构造方法是 PorterDuffColorFilter(int color, PorterDuff.Mode mode) 其中的 color 参数是指定的颜色作为源(SRC), mode 参数是 PorterDuff.Mode,和上面讲的一样。

1
2
3
4
5
6
7
8
    //这里需要注意 这里我们的bitmap属于目标图片(DST),Color.parseColor("#300000ff")属于源图片(SRC)
    val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.beauty1)
    canvas.drawBitmap(bitmap,100f,100f,mPaint)

    val porterDuffColorFilter = PorterDuffColorFilter(Color.parseColor("#300000ff"), PorterDuff.Mode.SRC_OVER)  //SRC_OVER混合模式
    mPaint.setColorFilter(porterDuffColorFilter)
    //绘制过滤颜色后的图
    canvas.drawBitmap(bitmap,100f,150f+bitmap.height,mPaint)

PorterDuffColorFilter

BlendModeColorFilter

BlendModeColorFilter 和 上面的PorterDuffColorFilter使用方式几乎一样,不过BlendMode 有API >= 29 的使用限制,可以通过官方文档查询各种混合效果,这儿不做展开。

ColorMatrixColorFilter

要了解颜色的矩阵转换,首先需要大家掌握一些矩阵运算的基本知识:

矩阵乘法

再看一下颜色矩阵乘法:

颜色矩阵乘法

该矩阵意思就是将图中颜色(RGBA)透明度变成它原来的一半,但是我们会发现,这种算法只能乘,如果我们有相加的需求,这种明显是不适用的。所以,应该在四阶色彩变换矩阵上增加一个“哑元坐标”,来实现所列的矩阵运算:

哑元坐标

ColorMatrixColorFilter 使用一个 ColorMatrix 来对颜色进行处理。 ColorMatrix 这个类,内部是一个 4x5 的矩阵:

1
2
3
4
[ a, b, c, d, e,
  f, g, h, i, j,
  k, l, m, n, o,
  p, q, r, s, t ]

由上面的矩阵计算公式可以得出,对于颜色 [R, G, B, A] ,转换算法是这样的:

1
2
3
4
R' = a*R + b*G + c*B + d*A + e;
G' = f*R + g*G + h*B + i*A + j;
B' = k*R + l*G + m*B + n*A + o;
A' = p*R + q*G + r*B + s*A + t;

滤镜的原理在于设置的颜色过滤器——矩阵变换,知道了原理,其它应用就简单了,我们来看几个简单的滤镜效果:

  1. 反相效果(曝光)

常见的照相机中的曝光也就是矩阵运算中的反向,即设原先的ARGB值为100,200,250,用最大值255减去原来的值,结果为155,55,5,就是“曝光”效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.beauty1)
    canvas.drawBitmap(bitmap,100f,100f,mPaint)

    // 反相效果 -- 底片(曝光)效果(就是将每个像素都变成它的相反的值)
    val colorMatrix = ColorMatrix(
        floatArrayOf(
            -1f, 0f,0f,0f,255f,
            0f,-1f,0f,0f,255f,
            0f,0f,-1f,0f,255f,
            0f,0f,0f,1f,0f
        )
    )
//        colorMatrix.setSaturation(2f)   //设置饱和度
//        colorMatrix.setScale(1.2f,1.2f,1.2f,1)   //设置缩放
    val colorMatrixColorFilter = ColorMatrixColorFilter(colorMatrix)
    mPaint.setColorFilter(colorMatrixColorFilter)
    //绘制颜色过滤后的图
    canvas.drawBitmap(bitmap,100f,150f+bitmap.height,mPaint)

曝光效果

  1. 美白效果

矩阵运算解析:首先需要知道1f是图像原色,即不改变图像滤镜。若要增强颜色达到一种美白的效果,只需要将RGB值稍加增大即可。

1
2
3
4
5
6
7
8
9
    //美白效果
    val colorMatrix = ColorMatrix(
        floatArrayOf(
            1.2f, 0f,0f,0f,0f,
            0f,1.2f,0f,0f,0f,
            0f,0f,1.2f,0f,0f,
            0f,0f,0f,1.2f,0f
        )
    )

美白效果

  1. 复古效果

这是美颜相机中常见的一款滤镜形式,矩阵中有特定的算法模板。

1
2
3
4
5
6
7
8
9
    //复古效果
    val colorMatrix = ColorMatrix(
        floatArrayOf(
            1/2f,1/2f,1/2f,0f,0f,
            1/3f, 1/3f,1/3f,0f,0f,
            1/4f,1/4f,1/4f,0f,0f,
            0f,0f,0f,1f,0f
        )
    )

复古效果

setMaskFilter(MaskFilter filter)

上面的 setColorFilter(filter)是对每个像素的颜色进行过滤;而这里的 setMaskFilter(filter) 则是基于整个画面来进行过滤MaskFilter 有两种: BlurMaskFilterEmbossMaskFilter

BlurMaskFilter

模糊遮罩效果的 MaskFilter ,构造方法 BlurMaskFilter(float radius, Blur style)radius 参数是模糊的范围,style 是模糊的类型。一共有四种:

  • NORMAL: 模糊内外边框
  • SOLID: 在边界内部画实体,模糊外面
  • INNER: 模糊内部边框,外部不变
  • OUTER: 内部不变,模糊外部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.beauty1)
    val rectF = RectF(100f, 100f, 100f + bitmap.width / 2, 100f + bitmap.height / 2)
    canvas.drawBitmap(bitmap,null, rectF,mPaint)    //原图

    //NORMAL效果
    canvas.translate(0f,300f+bitmap.height/2)
    val normalMaskFilter = BlurMaskFilter(50f, BlurMaskFilter.Blur.NORMAL)
    mPaint.setMaskFilter(normalMaskFilter)
    //绘制过滤后的图
    canvas.drawBitmap(bitmap,null, rectF,mPaint)

    //SOLID效果
    rectF.offset(200f+bitmap.width/2f,0f)
    val solidMaskFilter = BlurMaskFilter(50f, BlurMaskFilter.Blur.SOLID)
    mPaint.setMaskFilter(solidMaskFilter)
    canvas.drawBitmap(bitmap,null, rectF,mPaint)

    //INNER效果
    canvas.translate(0f,300f+bitmap.height/2)
    rectF.offset(-(200f+bitmap.width/2f),0f)
    val innerMaskFilter = BlurMaskFilter(50f, BlurMaskFilter.Blur.INNER)
    mPaint.setMaskFilter(innerMaskFilter)
    canvas.drawBitmap(bitmap,null, rectF,mPaint)

    //OUTER效果
    rectF.offset(200f+bitmap.width/2f,0f)
    val outerMaskFilter = BlurMaskFilter(50f, BlurMaskFilter.Blur.OUTER)
    mPaint.setMaskFilter(outerMaskFilter)
    canvas.drawBitmap(bitmap,null, rectF,mPaint)

BlurMaskFilter

EmbossMaskFilter

浮雕遮罩效果的 MaskFilter。该类已被废弃,不推荐使用。构造方法 EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius) , 参数解析:

  • direction 指定光源的位置,长度为xxx的数组标量[x,y,z]
  • ambient 环境光的因子 (0~1),越接近0,环境光越暗
  • specular 镜面反射系数 越接近0,镜面反射越强
  • blurRadius 模糊半径 值越大,模糊效果越明显
1
2
3
4
5
6
7
8
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.beauty1)
    //绘制原图
    canvas.drawBitmap(bitmap,100f,100f,mPaint)
    //EmbossMaskFilter已被废弃  如果要看到效果需要关闭硬件加速
    val embossMaskFilter = EmbossMaskFilter(floatArrayOf(2f, 2f, 2f), 0.1f, 10f, 10f)
    mPaint.setMaskFilter(embossMaskFilter)
    //绘制过滤后的图
    canvas.drawBitmap(bitmap,100f,150f+bitmap.height,mPaint)

EmbossMaskFilter

setShadowLayer(float radius, float dx, float dy, int shadowColor)

给绘制内容下面加一层阴影效果。参数解析:

  • radius :阴影的模糊范围
  • dx :阴影在x轴方向偏移量
  • dy :阴影在y轴方向的偏移量
  • shadowColor :阴影的颜色
1
2
3
4
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 80f
    mPaint.setShadowLayer(4f,4f,4f,Color.RED)
    canvas.drawText(text, 100f, 300f, mPaint)

setShadowLayer

如果要清除阴影层,使用 clearShadowLayer()

注意:

  1. 在硬件加速开启的情况下, setShadowLayer() 在API28以下只支持文字的绘制,文字之外的绘制在API28以下必须关闭硬件加速才能正常绘制阴影。
  2. 如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。

setFilterBitmap(boolean filter)

设置是否使用双线性过滤来绘制 Bitmap 。图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。贴一张wiki的效果图:

setFilterBitmap

使用很简单:

1
    mPaint.setFilterBitmap(true)

setPathEffect(PathEffect effect)

使用 PathEffect 来给图形的轮廓设置效果。对 Canvas 所有的图形绘制有效,也就是 drawLine() drawCircle() drawPath() 这些方法。 PathEffect 有6种,单一效果的 CornerPathEffectDiscretePathEffectDashPathEffectPathDashPathEffect,和组合效果的 SumPathEffectComposePathEffect

注意:setPathEffect 方法AIP28以上才支持绘制线条(Canvas.drawLine() 和 Canvas.drawLines())的硬件加速。

CornerPathEffect

把所有拐角变成圆角。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    val path = Path().apply {
            moveTo(100f, 100f)
            lineTo(200f, 300f)
            rLineTo(100f, -200f)
            rLineTo(100f, 200f)
            rLineTo(150f, -250f)
            rLineTo(200f, 200f)
        }
    //绘制原path
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,400f)
    val cornerPathEffect = CornerPathEffect(50f)
    mPaint.setPathEffect(cornerPathEffect)
    //绘制CornerPathEffect效果
    canvas.drawPath(path,mPaint)

CornerPathEffect

DiscretePathEffect

线条进行随机的偏离,让轮廓变得乱七八糟。具体的做法是,把绘制改为使用定长的线段来拼接,并且在拼接的时候对路径进行随机偏离。构造方法 DiscretePathEffect(float segmentLength, float deviation)。参数解析:

  • segmentLength :用来拼接的每个线段的长度
  • deviation :偏离量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    val path = Path().apply {
        moveTo(100f, 100f)
        lineTo(200f, 300f)
        rLineTo(100f, -200f)
        rLineTo(100f, 200f)
        rLineTo(150f, -250f)
        rLineTo(200f, 200f)
    }
    //绘制原path
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,400f)
    val discretePathEffect = DiscretePathEffect(10f,5f)
    mPaint.setPathEffect(discretePathEffect)
    //绘制DiscretePathEffect效果
    canvas.drawPath(path,mPaint)

DiscretePathEffect

DashPathEffect

使用虚线来绘制线条。构造方法 DashPathEffect(float[] intervals, float phase),参数解析:

  • intervals : 数组,它指定了虚线的格式:数组中元素必须为偶数(最少是 2 个),按照「画线长度、空白长度、画线长度、空白长度」……的顺序排列。
  • phase : 虚线的偏移量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    val path = Path().apply {
        moveTo(100f, 100f)
        lineTo(200f, 300f)
        rLineTo(100f, -200f)
        rLineTo(100f, 200f)
        rLineTo(150f, -250f)
        rLineTo(200f, 200f)
    }
    //绘制原path
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,400f)
    val dashPathEffect = DashPathEffect(floatArrayOf(20f,10f,5f,10f),5f)
    mPaint.setPathEffect(dashPathEffect)
    //绘制DashPathEffect效果
    canvas.drawPath(path,mPaint)

DashPathEffect

PathDashPathEffect

使用一个 Path 来绘制虚线。构造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style),参数解析:

  • shape : 用来绘制的虚线 Path。
  • advance : 两个相邻的 shape 段之间的起点间隔。
  • phase : 虚线的偏移量。
  • style : 类型为 PathDashPathEffect.Style ,是一个枚举 ,有三个值:「位移」 TRANSLATE、「旋转」 ROTATE、「变体」 MORPH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    val rect = RectF(100f,100f,400f,300f)
    val dashPath = Path().apply {
        lineTo(15f,20f)
        lineTo(-15f,20f)
        close()
    }
    //绘制原图
    canvas.drawRoundRect(rect,40f,40f,mPaint)

    canvas.translate(0f,300f)
    //TRANSLATE效果
    val translateDashPathEffect = PathDashPathEffect(dashPath,30f,0f,PathDashPathEffect.Style.TRANSLATE)
    mPaint.setPathEffect(translateDashPathEffect)
    canvas.drawRoundRect(rect,40f,40f,mPaint)

    canvas.translate(0f,300f)
    //ROTATE效果
    val rotateDashPathEffect = PathDashPathEffect(dashPath,30f,0f,PathDashPathEffect.Style.ROTATE)
    mPaint.setPathEffect(rotateDashPathEffect)
    canvas.drawRoundRect(rect,40f,40f,mPaint)

    canvas.translate(0f,300f)
    //MORPH效果
    val morphDashPathEffect = PathDashPathEffect(dashPath,30f,0f,PathDashPathEffect.Style.MORPH)
    mPaint.setPathEffect(morphDashPathEffect)
    canvas.drawRoundRect(rect,40f,40f,mPaint)

PathDashPathEffect

SumPathEffect

将两种 PathEffect 分别对目标进行绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    val path = Path().apply {
        moveTo(100f, 100f)
        lineTo(200f, 300f)
        rLineTo(100f, -200f)
        rLineTo(100f, 200f)
        rLineTo(150f, -250f)
        rLineTo(200f, 200f)
    }
    //绘制原path
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,400f)
    val discretePathEffect = DiscretePathEffect(10f,5f)
    val dashPathEffect = DashPathEffect(floatArrayOf(20f,10f,5f,10f),5f)
    //对原Path会先绘制dashPathEffect 再绘制discretePathEffect
    val sumPathEffect = SumPathEffect(dashPathEffect, discretePathEffect)
    mPaint.setPathEffect(sumPathEffect)
    //绘制SumPathEffect效果
    canvas.drawPath(path,mPaint)

SumPathEffect

ComposePathEffect

先对目标 Path 使用一个 PathEffect,然后再对这个改变后的 Path 使用另一个 PathEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    val path = Path().apply {
        moveTo(100f, 100f)
        lineTo(200f, 300f)
        rLineTo(100f, -200f)
        rLineTo(100f, 200f)
        rLineTo(150f, -250f)
        rLineTo(200f, 200f)
    }
    //绘制原path
    canvas.drawPath(path,mPaint)

    canvas.translate(0f,400f)
    val discretePathEffect = DiscretePathEffect(10f,5f)
    val dashPathEffect = DashPathEffect(floatArrayOf(20f,10f,5f,10f),5f)
    //在先绘制dashPathEffect的效果后,基于这个效果再绘制discretePathEffect
    val composePathEffect = ComposePathEffect(dashPathEffect, discretePathEffect)
    mPaint.setPathEffect(composePathEffect)
    //绘制ComposePathEffect效果
    canvas.drawPath(path,mPaint)

ComposePathEffect

绘制文字相关

我们回顾下之前讲的 Canvas 绘制文字方法 drawText(String text, float x, float y, Paint paint)

  • text : 文字内容
  • x : 文字从画布上开始绘制的x坐标
  • y : Baseline所在的y坐标
  • paint : 画笔

Baseline的概念

我们先看一行文字各区域的分布示意图:

文字各区域分布图1

从上图来看,Baseline不难理解,它就是E和h的下边界线。我们还可以得出一个结论:

文字的高度 = Descent + Ascent

然而,上面这个公式并不完全准确,我们再看一个图:

文字各区域分布图2

我们看到,如果文字的上方有一些特殊的符号,比如上图中的 ~ 或者是我们汉语拼音中的声调时,文字区域又会多出一部分 Leading

因此,完整的公式应该是:

文字的高度 = Descent + Ascent + Leading

那么,为什么第一幅图中没有说明 Leading 的存在呢,原因是我们通常在绘制一行英文或者中文时,Leading 的高度为0,如下图:

FontMetrics

所以我们在文字绘制时不需要考虑 Leading图中的数值都是距离 Baseline 的距离,在 Baseline 上方为负值,下方为正值。

我们知道文字是基于基线绘制的,那么如果设计稿中要求文字居中对齐(例如文字要在一个矩形框内垂直居中显示)时我们该如何确定基线的位置呢?我画了一张图帮助理解:

文字各区域分布图3

通过上图很容易可以理解,如果要文字在矩形框中垂直居中对齐,那么文字的中心线要和矩形的中心线对齐,我们只需要算出文字基线到中线的距离,结合已知的矩形坐标,自然而然的就能得到文字的基线坐标:

文字基线到中线的距离 = (Descent + Ascent ) / 2 - Descent
基线y坐标 = (rect.bottom + rect.top)/2 + 文字基线到中线的距离

注意:需要设置了Paint的文字大小后,才能得到 FontMetrics 中的各项值。

Ascent 在 基线上方,得到的值为负数,所以实际获取基线基线到中线的距离的方法为:

1
2
3
4
5
6
7
8
    /**
     * @param paint 画笔,计算前需要先设置文字大小textsize
     * @return 基线和centerY的距离
     */
    fun getBaseLine2CenterY(paint: Paint): Float {
        val fontMetrics = paint.fontMetrics
        return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent
    }

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    canvas.translate(canvas.width/3f,canvas.height/3f)
    val rect = RectF(100f,100f,600f,400f)
    canvas.drawRect(rect,mPaint)
    mPaint.textSize = 80f
    val fontMetrics = mPaint.fontMetrics
    //算出基线到中线的距离
    val h = (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent

    //中线y坐标为rect的中心Y坐标
    val centerY = (rect.bottom + rect.top) / 2
    //baseLine的y坐标
    val baseLineY = centerY + h
    val text = "Hello World!"
    //绘制和矩形居中对齐的文字
    canvas.drawText(text,100f,baseLineY,mPaint)

绘制居中对齐文字

setTextSize(float textSize)

设置文字大小。

1
2
3
4
5
6
7
8
9
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 18f
    canvas.drawText(text,100f,50f,mPaint)
    mPaint.textSize = 36f
    canvas.drawText(text,100f,105f,mPaint)
    mPaint.textSize = 54f
    canvas.drawText(text,100f,170f,mPaint)
    mPaint.textSize = 72f
    canvas.drawText(text,100f,250f,mPaint)

setTextSize

setTypeface(Typeface typeface)

设置字体。

1
2
3
4
5
6
7
8
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.typeface = Typeface.DEFAULT
    canvas.drawText(text, 100f, 100f, mPaint)
    mPaint.typeface = Typeface.SERIF
    canvas.drawText(text, 100f, 300f, mPaint)
    mPaint.typeface = Typeface.createFromAsset(context.assets, "RuiZiZhenYanTi.ttf")
    canvas.drawText(text, 100f, 500f, mPaint)

setTypeface

setFakeBoldText(boolean fakeBoldText)

是否使用伪粗体。之所以叫伪粗体( fake bold ),因为它并不是通过选用更高 weight 的字体让文字变粗,而是通过程序在运行时把文字给「描粗」了。

1
2
3
4
5
6
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.isFakeBoldText = false
    canvas.drawText(text, 100f, 100f, mPaint)
    mPaint.isFakeBoldText = true
    canvas.drawText(text, 100f, 300f, mPaint)

setFakeBoldText

setTextAlign(Paint.Align align)

设置文字的对齐方式。一共有三个值:LEFT CETNERRIGHT。默认值为 LEFT

1
2
3
4
5
6
7
8
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.textAlign = Paint.Align.LEFT
    canvas.drawText(text, 500f, 100f, mPaint)
    mPaint.textAlign = Paint.Align.CENTER
    canvas.drawText(text, 500f, 300f, mPaint)
    mPaint.textAlign = Paint.Align.RIGHT
    canvas.drawText(text, 500f, 500f, mPaint)

setTextAlign

setUnderlineText(boolean underlineText)

设置带有下划线的文字效果。

1
2
3
4
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.isUnderlineText = true
    canvas.drawText(text, 100f, 100f, mPaint)

setUnderlineText

setStrikeThruText(boolean strikeThruText)

设置带有删除线的效果。

1
2
3
4
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.isStrikeThruText = true
    canvas.drawText(text, 100f, 100f, mPaint)

setStrikeThruText

setTextScaleX(float scaleX)

设置文字横向缩放。

1
2
3
4
5
6
7
8
9
10
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.textScaleX = 1f
    canvas.drawText(text, 100f, 100f, mPaint)

    mPaint.textScaleX = 0.8f
    canvas.drawText(text, 100f, 300f, mPaint)

    mPaint.textScaleX = 1.2f
    canvas.drawText(text, 100f, 500f, mPaint)

setTextScaleX

setTextSkewX(float skewX)

设置文字横向错切角度(倾斜度)。

1
2
3
4
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.textSkewX = -0.4f
    canvas.drawText(text, 100f, 100f, mPaint)

setTextSkewX

setLetterSpacing(float letterSpacing)

设置字符间距。默认值是 0。

1
2
3
4
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 54f
    mPaint.letterSpacing = 0.2f
    canvas.drawText(text, 100f, 100f, mPaint)

setLetterSpacing

float getFontSpacing()

获取推荐的行间距。

即推荐的两行文字的 baseline 的距离。这个值是系统根据文字的字体和字号自动计算的。它的作用是当你要手动绘制多行文字(而不是使用 StaticLayout)的时候,可以在换行的时候给 y 坐标加上这个值来下移文字。

1
2
3
4
5
    val text = "Hello World!"
    mPaint.textSize = 80f
    canvas.drawText(text, 100f, 100f, mPaint)
    canvas.drawText(text, 100f, 100f+mPaint.fontSpacing, mPaint)
    canvas.drawText(text, 100f, 100f+2*mPaint.fontSpacing, mPaint)

getFontSpacing

FontMetircs getFontMetrics()

获取 PaintFontMetrics

FontMetrics 是个相对专业的工具类,它提供了几个文字排印方面的数值:ascent, descent, top, bottom, leading。 前面我们讲基线时讲过,如下图:

文字各区域分布图2

getTextBounds(String text, int start, int end, Rect bounds)

获取文字的显示范围。参数解析:

  • text:是要测量的文字
  • start:文字的起始位置
  • end:文字结束位置
  • bounds:是存储文字显示范围的对象,方法在测算完成之后会把结果写进 bounds
1
2
3
4
5
6
7
8
9
10
11
12
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 80f
    canvas.drawText(text,100f,100f,mPaint)

    val bounds = Rect()
    mPaint.getTextBounds(text,0,text.length,bounds)
    //偏移到文字绘制起始位置
    bounds.offset(100,100)

    mPaint.style = Paint.Style.STROKE
    mPaint.strokeWidth = 4f
    canvas.drawRect(bounds,mPaint)

getTextBounds

float measureText(String text)

测量文字的宽度并返回。

getTextWidths(String text, float[] widths)

获取字符串中每个字符的宽度,并把结果填入参数 widths

int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)

在给出宽度上限的前提下测量文字的宽度。如果文字的宽度超出了上限,那么在临近超限的位置截断文字。参数解析:

  • text :要测量的文字
  • measureForwards :文字的测量方向,true 表示由左往右测量
  • maxWidth :给出的宽度上限
  • measuredWidth :方法测量完成后会把截取的文字宽度(如果宽度没有超限,则为文字总宽度)赋值给 measuredWidth[0]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    val measuredWidth = floatArrayOf(0f)
    var measuredCount = 0
    val text = "人间忽晚,山河已秋"
    mPaint.textSize = 80f
    //宽度上限400 不够用,文字截断
    measuredCount = mPaint.breakText(text,0,text.length,true,400f,measuredWidth)
    canvas.drawText(text,0,measuredCount,100f,100f,mPaint)
    LogUtils.i("measuredWidth: ${measuredWidth[0]}")//400

    //宽度上限600 不够用,文字截断
    measuredCount = mPaint.breakText(text,0,text.length,true,600f,measuredWidth)
    canvas.drawText(text,0,measuredCount,100f,200f,mPaint)
    LogUtils.i("measuredWidth: ${measuredWidth[0]}")//560

    ////宽度上限800 够用
    measuredCount = mPaint.breakText(text,0,text.length,true,800f,measuredWidth)
    canvas.drawText(text,0,measuredCount,100f,300f,mPaint)
    LogUtils.i("measuredWidth: ${measuredWidth[0]}")//720

breakText

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值。