第3章 画笔Paint详解:颜色与样式设置、抗锯齿与抖动、StrokeCap与StrokeJoin、Shader与颜色渐变

各位同学,欢迎来到第三章。上一章我们把Canvas的基本操作捋了一遍,今天来聊聊Paint——这个在自定义View里出镜率最高的对象。

说实话,我刚接触Android绘图那会儿,总觉得Paint不就是个画笔画笔嘛,设个颜色、设个粗细就完事了。后来才发现,这里面的门道真不少。你想想看,同样一个圆,用不同的Paint配置画出来,效果天差地别。

3.1 颜色与样式设置

Paint最基础的能力,就是设置颜色和填充样式。咱们先看最常用的几个方法:

val paint = Paint()
paint.color = Color.RED          // 设置颜色
paint.alpha = 128                // 设置透明度 0-255
paint.style = Paint.Style.FILL   // 填充模式

这里有个细节我特别想强调——coloralpha的关系。很多人以为设置color的时候就能把透明度带进去,比如Color.parseColor("#80FF0000")。但如果你后面又调用了paint.alpha = 255,那之前设的透明度就被覆盖了。

我在项目中遇到过这个坑:画一个半透明的遮罩层,明明颜色值里带了透明度,画出来却是实心的。排查了半天,发现是某处代码顺手设了个alpha(255)。嗯,这个顺序问题,大家一定要留意。

样式设置就三种:

样式 说明 效果
FILL 填充内部 只填充,不描边
STROKE 仅描边 只画轮廓,内部透明
FILL_AND_STROKE 填充+描边 既填充又画轮廓
小技巧:如果你需要描边和填充用不同的颜色,可以准备两个Paint对象。一个设FILL,一个设STROKE,分两次绘制。这是最灵活的做法。

3.2 抗锯齿与抖动

抗锯齿,英文叫Anti-Alias。说白了就是让图形的边缘看起来更平滑,不那么「狗啃」。

paint.isAntiAlias = true   // 开启抗锯齿
paint.isDither = true      // 开启抖动

这两个开关,我建议你默认都打开。为什么?

抗锯齿的原理是在边缘像素上做半透明过渡。比如一条斜线,如果不抗锯齿,边缘就是一个个锯齿状的方块。开了抗锯齿,边缘的像素会变成半透明,视觉上就平滑了。

抖动呢?它解决的是颜色深度的问题。在低色深设备上,颜色不够用,抖动通过像素点的排列组合,模拟出更多颜色。现在主流设备都是32位色深,抖动的效果不那么明显了。但我个人习惯还是开着,反正性能开销极小,有备无患。

注意:抗锯齿对性能有一定影响,尤其是在大量小图形绘制的场景下。我曾经在一个实时绘制数千个点的应用中,关闭抗锯齿后帧率从30fps提升到了55fps。所以,如果对性能有极致要求,可以考虑按需开启。

3.3 StrokeCap与StrokeJoin

这两个属性,专门管线条的两头和拐角长什么样。

StrokeCap:线的帽子

有三种取值:

paint.strokeCap = Paint.Cap.BUTT   // 平头,不延伸
paint.strokeCap = Paint.Cap.ROUND  // 圆头,半圆延伸
paint.strokeCap = Paint.Cap.SQUARE // 方头,矩形延伸

BUTT和SQUARE的区别在哪?我刚开始也搞混过。你看:BUTT就是线画到哪就停在哪,端面是平的。SQUARE呢,它会在端点的基础上再延伸出半个线宽的长度,端面也是平的。所以SQUARE比BUTT长出一截。

ROUND最好理解,就是给线头戴个半圆形的帽子。

StrokeJoin:线的关节

paint.strokeJoin = Paint.Join.MITER  // 尖角
paint.strokeJoin = Paint.Join.ROUND  // 圆角
paint.strokeJoin = Paint.Join.BEVEL  // 平角

MITER是默认值,两条线相交的地方会形成一个尖角。但如果角度太小,尖角会伸出去很长,这时候可以用strokeMiter来限制。

ROUND就是圆润的过渡,BEVEL则是把尖角切掉,形成一个斜面。

实战经验:画折线图的时候,我建议用ROUND的Join和Cap。这样线条看起来更流畅、更现代。如果你画的是建筑图纸或者UI边框,MITER更合适,显得硬朗、精确。

3.4 Shader与颜色渐变

好了,终于到了Shader。这是Paint里最有意思的部分之一。

Shader说白了就是「着色器」。它告诉Paint:别用单一颜色了,用这个渐变或者图片来填充吧。

Android提供了几种内置Shader:

  • LinearGradient:线性渐变
  • RadialGradient:径向渐变
  • SweepGradient:扫描渐变
  • BitmapShader:图片着色
  • ComposeShader:组合着色

LinearGradient 线性渐变

最常用的渐变方式。从点A到点B,颜色逐渐变化。

val shader = LinearGradient(
    0f, 0f,                    // 起点x, y
    width.toFloat(), 0f,       // 终点x, y
    intArrayOf(Color.RED, Color.BLUE, Color.GREEN),  // 颜色数组
    floatArrayOf(0f, 0.5f, 1f), // 位置数组,对应颜色的位置
    Shader.TileMode.CLAMP       // 平铺模式
)
paint.shader = shader

这里有个参数叫TileMode,它决定了渐变范围之外的区域怎么处理:

模式 行为
CLAMP 边缘颜色向外延伸
REPEAT 重复渐变
MIRROR 镜像重复

RadialGradient 径向渐变

从一个中心点向外辐射的渐变。适合做光晕、聚光灯效果。

val shader = RadialGradient(
    centerX, centerY,          // 圆心
    radius,                    // 半径
    Color.WHITE,               // 中心颜色
    Color.TRANSPARENT,         // 边缘颜色
    Shader.TileMode.CLAMP
)

SweepGradient 扫描渐变

这个比较特殊,它是围绕中心点旋转的渐变。做仪表盘、色轮的时候特别好用。

val shader = SweepGradient(
    centerX, centerY,
    intArrayOf(Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.RED),
    null  // 传null表示均匀分布
)
我的经验:SweepGradient的颜色数组最后一个颜色最好和第一个相同,这样首尾能无缝衔接。不然你会看到一条明显的「断痕」。

BitmapShader 图片着色

用图片来填充图形。比如你想画一个圆形头像,就可以用这个。

val bitmap = BitmapFactory.decodeResource(resources, R.drawable.avatar)
val shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
paint.shader = shader
// 然后画一个圆,图片就会以圆形裁剪显示
canvas.drawCircle(centerX, centerY, radius, paint)

ComposeShader 组合着色

把两个Shader组合起来,通过Xfermode混合。

val shader1 = LinearGradient(...)
val shader2 = BitmapShader(...)
val composeShader = ComposeShader(shader1, shader2, PorterDuff.Mode.SRC_OVER)
paint.shader = composeShader

这个玩法就多了。你可以做出纹理叠加渐变、图片叠加渐变等各种效果。

注意:ComposeShader在API 28以下有硬件加速的问题。我曾经在Android 7.0的设备上遇到过,组合着色画出来是纯黑色。解决方案是关闭硬件加速:view.setLayerType(View.LAYER_TYPE_SOFTWARE, null)

小结

这一章我们聊了Paint的核心配置。从颜色样式到抗锯齿,从线条端点到渐变着色。这些东西单独看都不难,但组合起来就能画出千变万化的效果。

我个人建议你动手试试:创建一个自定义View,用不同的Shader画同一个图形,看看效果差异。尤其是LinearGradient和RadialGradient,在实际项目中用的频率非常高。

下一章,我们会深入Path——那个能画出任意形状的利器。到时候你会发现,配合今天学的Shader,能玩出更多花样。

好,今天就到这里。有问题欢迎交流。


公众号:蓝海资料掘金营,微信deep3321