自定义View(六)--Paint

构造方法

  1. 直接构造方法
    // Create a new paint with default settings
    public Paint()
    // 使用指定的 flags 初始化 paint. 后续也可以通过 setFlags() 去改变这些 flags
    public Paint(int flags)
    // 通过一个已存在的 paint, 创建一个新的 paint
    public Paint(Paint paint)
    
  2. 间接构造方法
    // 将 paint 重置为默认状态 (相当于重新 new一个, 不过效率更高)
    public void reset()
    // 复制 src的所有属性
    public void set(Paint src)
    

flags

给 paint 设置 flag标记, 可以选择是否开启某些特殊的效果. flags可以通过构造方法传入, 可以通过 setFlags() 设置.

// 方法签名
public void setFlags(int flags)
// 可以一次设置多个 flag, 使用 | 连接
mPaint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

flags 可以取以下值:

// 开启 抗锯齿
public static final int ANTI_ALIAS_FLAG     = 0x01;
// 在缩放位图上启用 双线性采样(通过缩放图片像素来减少图片占用内存大小, 绘制图片时建议开启)
public static final int FILTER_BITMAP_FLAG  = 0x02;
// 设置放抖动的 (不懂什么意思, 通过实际对比可以发现启用后色彩更柔和, 建议开启)
public static final int DITHER_FLAG         = 0x04;
// 绘制文本时, 添加下划线
public static final int UNDERLINE_TEXT_FLAG = 0x08;
// 绘制文本时, 添加贯穿线
public static final int STRIKE_THRU_TEXT_FLAG = 0x10;
// 绘制文本时, 加粗
public static final int FAKE_BOLD_TEXT_FLAG = 0x20;
// 使文本平滑线性扩展的标志 (基本没用了)
public static final int LINEAR_TEXT_FLAG    = 0x40;
// 是否打开亚像素设置来绘制文本. 
// 亚像素: 将相邻两个像素之间的距离再细分, 再插入一些像素(即亚像素). 总结就是通过程序计算的方式增加像素, 可增强文本清晰度, 但更耗性能 (红米的 4800万像素?)
public static final int SUBPIXEL_TEXT_FLAG  = 0x80;
// 绘制文本时启用位图字体的绘制标志. (来自百度翻译)
public static final int EMBEDDED_BITMAP_TEXT_FLAG = 0x400;

对于这些 flags, Paint也提供了一些快捷的方法去设置

// 抗锯齿
mPaint.setAntiAlias(true);  == mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
// 设置双线性过滤: 优化 Bitmap 放大绘制的效果
mPaint.setFilterBitmap(true); == mPaint.setFlags(Paint.FILTER_BITMAP_FLAG);
// 设置图像的抖动: 优化色彩深度降低时的绘制效果
mPaint.setDither(true); == mPaint.setFlags(Paint.DITHER_FLAG);
// ... 一般只有这三种 flag 较常用, 其它就不一一列出了

设置填充模式

通过 paint.setStyle() 可以设置填充模式

public void setStyle(Style style)

Paint.Style 是个枚举

public enum Style {
    // 填充
    FILL            (0),
    // 描边
    STROKE          (1),
    // 填充加描边
    FILL_AND_STROKE (2);
    Style(int nativeInt) {
        this.nativeInt = nativeInt;
    }
    final int nativeInt;
}

以上三种取值的区别如下:

设置线条样式

paint提供了一些设置线条样式的方法:

// 设置线条宽度, 单位px, 默认0, 绘制结果为1px, 且不受 canvas的几何变换影响
public void setStrokeWidth(float width)
// 设置线帽的形状
public void setStrokeCap(Cap cap)
// 设置线条设置拐角的形状 
public void setStrokeJoin(Join join)
// 当拐角为尖角时, 如果 (线条内外交点距离 / 线宽 > miter), 则自动切为平角
public void setStrokeMiter(float miter)

Paint.Cap 指线帽的形状; Paint.Join 指线段拐角处的形状

public enum Cap {
    // 平头, 默认值
    BUTT    (0),
    // 圆头
    ROUND   (1),
    // 方形头
    SQUARE  (2);
    private Cap(int nativeInt) {
        this.nativeInt = nativeInt;
    }
    final int nativeInt;
}
...
public enum Join {
    // 尖角, 默认值
    MITER   (0),
    // 圆角
    ROUND   (1),
    // 平角
    BEVEL   (2);
    private Join(int nativeInt) {
        this.nativeInt = nativeInt;
    }
    final int nativeInt;
}

看看效果

设置路径效果

通过 paint.setPathEffect() 可以设置路径的显示效果.

public PathEffect setPathEffect(PathEffect effect)

PathEffect本身没有什么效果, 一般使用它的子类

/**
 * 将路径的转角变得圆滑
 * @param radius  圆角半径
 */
public CornerPathEffect(float radius)
/**
 * 离散路径效果: 将原路径分割为指定长度的线段, 每条线段都随机偏移一段距离
 * @param segmentLength  分割后线段的长度
 * @param deviation      线段的偏移距离
 */
public DiscretePathEffect(float segmentLength, float deviation)
/**
 * 虚线效果 
 * @param intervals  用于描述虚线的特征: 虚线长度、缝隙宽度、虚线长度、缝隙宽度、...  (数组长度最少为2,且必须为偶数)
 * @param phase      沿着路径方向, 偏移多少才开始绘制
 */
public DashPathEffect(float intervals[], float phase)
/**
 * 和虚线效果类似, 不过它的每一段组成都是 自定义的形状 shape
 * @param advance    两个 shape 之间的间隔距离
 * @param phase      沿着路径方向, 偏移多少才开始绘制
 * @param style      转角处的样式: Style.TRANSLATE(平移转角处的 shape);  Style.ROTATE(旋转转角处的 shape);  Style.MORPH(转角处的 shape 拉伸变形)
 */
public PathDashPathEffect(Path shape, float advance, float phase, Style style)
// 分别对原始路径使用 first 和 second 效果, 然后将两条路径合并输出
public SumPathEffect(PathEffect first, PathEffect second)
// 先对原始路径使用 innerpe 效果, 接着在此基础上又使用 outerpe 效果, 最后输出结果
public ComposePathEffect(PathEffect outerpe, PathEffect innerpe)

上图片 (图片来自网络, 第一条为原始路径)

获取path (没验证过, 内容截取至网络)

获取实际的path: 指的就是 drawPath() 的绘制内容的轮廓,要算上线条宽度和设置的 PathEffect

// src指原来的path, dst用于保存实际的path
public boolean getFillPath(Path src, Path dst)

获取文本的path

public void getTextPath(String text, int start, int end, float x, float y, Path path)

设置阴影

通过 paint.setShadowLayer() 可以设置阴影.

/**
 * 设置阴影 
 * @param radius        阴影模糊半径
 * @param dx            阴影的水平偏移
 * @param dy            阴影的垂直偏移
 * @param shadowColor   阴影的颜色, shadowColor是半透明时, 阴影透明度为 shadowColor自己的透明度, 否则 阴影透明度为 paint的透明度
 */
public void setShadowLayer(float radius, float dx, float dy, int shadowColor)
// 清除阴影效果
public void clearShadowLayer()

发现除了文字外, 圆角矩形还有图片的阴影都没有生效? 这是因为只有文字阴影支持硬件加速, 所以需要关闭硬件加速才能看到其它的阴影效果.

阴影都出现了, 不过图片的阴影似乎有点不对? 这是因为系统在处理图片的 shadowLayer时, 是将图片复制一份, 然后再进行边缘模糊, 所以看上去和预想的有点不一样.

设置遮罩滤镜 (不支持硬件加速)

通过 paint.setMaskFilter() 可以对整个画面进行过滤

// 注意和 setColorFilter 的区别. setColorFilter 是对每个像素进行过滤, 这里是过滤整个画面
public MaskFilter setMaskFilter(MaskFilter maskfilter)

MaskFilter 的两个子类

/**
 * 模糊效果
 * @param radius       模糊半径 (基于高斯模糊)
 * @param style        模糊类型, 是个枚举: NORMAL(内外都模糊)、 SOLID(内部正常绘制,外部模糊)、 INNER(内部模糊,外部不绘制)、 OUTER(外部模糊,内部不绘制)
 */
public BlurMaskFilter(float radius, Blur style) 
/**
 * 浮雕效果  (过时了, 不再演示)
 * @param direction      包含3个元素的数组,指光的方向
 * @param ambient        指环境光的强度 范围是 0 ~ 1
 * @param specular       指炫光的系数
 * @param blurRadius     指应用光线的范围
 */
public EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)

下面是 BlurMaskFilter 的四种特效

色彩处理

在绘制内容时, 对色彩的处理大概可以分为三个步骤: 基本颜色 -> 色彩过滤 -> 颜色混合(合成)

色彩处理第一步: 基本颜色

  • 颜色的来源
    • 颜色来自直接绘制
      canvas.drawColor();
      canvas.drawRGB()
      canvas.drawARGB()
      
    • 颜色来自图片
      // 一般情况下, 绘制图片时, paint上设置的颜色对 bitmap 没有影响
      // 但如果 bitmap 中只有 alpha值, 而没有 r, g, b , 那么该图片的颜色将由 paint 决定
      // ps: bitmap.extractAlpha() 就可以以原 bitmap 为基础, 创建一个新的只包含 alpha值 的空白图片
      canvas.drawBitmap();
      
    • 颜色来自 paint
      // 直接给 paint 设置颜色
      public void setColor(@ColorInt int color)
      public void setARGB(int a, int r, int g, int b)
      // 通过 Shader(着色器) 设置颜色.  -- 设置 Shader 后, setColor/setARGB 都将不再起作用
      public Shader setShader(Shader shader)
      
  • Shader 的子类
    /**
    * 线性渐变: 其坐标是相对于当前 View的, 坐标更多的作用是设置线性渐变的角度, '线' 本身是无限宽的
    * @param (x0, y0), (x1, y1)        线性渐变的起点坐标 和 终点坐标
    * @param color0, color1            线性渐变的起点颜色 和 终点颜色    
    * @param tile   绘制区域超出线性渐变所指定的范围时, 所采用的重复策略: CLAMP(用边缘色彩填充多余空间);  MIRROR(镜像重复);  REPEAT(平铺重复)
    */
    public LinearGradient(float x0, float y0, float x1, float y1, @ColorInt int color0, @ColorInt int color1,@NonNull TileMode tile)
    /**
    * @param colors      渐变颜色的数组
    * @param positions   与 colors数组对应, 指定每个颜色在整个渐变中所占的百分比.  取值范围 0 ~ 1
    */
    public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[], @Nullable float positions[], @NonNull TileMode tile)
    /**
    * 辐射渐变
    * @param centerX, centerY          渐变的中心点
    * @param radius                    渐变的半径
    * @param centerColor, edgeColor    渐变的中心颜色 和 边缘颜色
    */
    public RadialGradient(float centerX, float centerY, float radius, @ColorInt int centerColor, @ColorInt int edgeColor, @NonNull TileMode tileMode) 
    public RadialGradient(float centerX, float centerY, float radius, @NonNull @ColorInt int colors[], @Nullable float stops[], @NonNull TileMode tileMode)
    /**
    * 扫描式渐变
    * @param cx, cy           扫描中心点坐标
    * @param color0, color1   起始颜色 和 终点颜色
    */
    public SweepGradient(float cx, float cy, @ColorInt int color0, @ColorInt int color1)
    public SweepGradient(float cx, float cy, @NonNull @ColorInt int colors[], @Nullable float positions[])
    /**
    * 图片着色器:    BitmapShader 会从当前控件的左上角开始摆放图片, 如果图片尺寸不够, 则按照对应方向上的 TileMode 处理空白区域
    * @param bitmap           要在着色器中使用的位图. 使用BitmapShader后, paint就可以用bitmap去绘制指定的图形 
    * @param tileX, tileY     水平 和 垂直方向上的重复模式
    */
    public BitmapShader(@NonNull Bitmap bitmap, @NonNull TileMode tileX, @NonNull TileMode tileY)
    /**
    * 混合着色器.        ComposeShader 会把 shaderA 和 shaderB 按照 mode 进行混合
    * @param shaderA    ComposeShader 会把它当做   目标图 -- "dst"
    * @param shaderB    ComposeShader 会把它当做   源图 -- "src" 
    * @param mode       一般使用其子类 PorterDuffXfermode 
    */
    public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode)
    public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull PorterDuff.Mode mode)
    
  • PorterDuffXfermode & PorterDuff.Mode

    public PorterDuffXfermode(PorterDuff.Mode mode)
    public enum PorterDuff.Mode {
      CLEAR, SRC, DST, SRC_OVER, DST_OVER, SRC_IN, DST_IN, 
      SRC_OUT, DST_OUT, SRC_ATOP, DST_ATOP, XOR, DARKEN, 
      LIGHTEN, MULTIPLY, SCREEN, ADD, OVERLAY
    }
    

    这是 Google 的演示效果图 (注意图片周围的透明范围)

    这是实践后的效果图 (去掉了透明区域)

    PorterDuffXfermode的逻辑是这样的: 先绘制 DST中与SRC没有相交的部分, 而对于两者相交的区域, 会先清除该部分的图片, 然后将采用PorterDuff.Mode计算后的结果绘制上去.

  • 给 Shader 设置矩阵变换

    // Matrix类中包含一个 3x3 的矩阵, 可用于转换坐标
    // 给 Shader 设置 Matrix 后, 通过不断转换 Matrix 坐标, 可以实现一些动画效果
    public void setLocalMatrix(@Nullable Matrix localM)
    
  • SweepGradient + matrix.setRotate() 实现雷达效果
  • LinearGradient + matrix.setTranslate() 实现文字发光效果
  • BitmapShader + canvas.drawCircle() 实现圆形图片

色彩处理第二步: 过滤

通过 paint.setColorFilter() 可以对基础颜色进行过滤

// 使用 ColorFilter 对paint当前的颜色进行过滤
public ColorFilter setColorFilter(ColorFilter filter) 

看看 ColorFilter 的几个子类

/**
 * LightingColorFilter 会将目标颜色的 RGB通道乘以一种颜色(mul), 然后再加上另一种颜色(add).   公式如下:
 *  新R = R * mul.R / 0xff + add.R
 *  新G = G * mul.G / 0xff + add.G
 *  新B = B * mul.B / 0xff + add.B
 */
public LightingColorFilter(@ColorInt int mul, @ColorInt int add)
// 以paint当前颜色为 dst, 以这里的 color 为 scr, 按照 mode 进行混合
public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode)
/**
 * ColorMatrixColorFilter 使用一个颜色矩阵(ColorMatrix)进行颜色过滤. 
 * ColorMatrix 通过一个长度为20的数组构建, 该数组表示一个 4 * 5 的颜色矩阵.
 */
public ColorMatrixColorFilter(@NonNull ColorMatrix matrix)
public ColorMatrixColorFilter(@NonNull float[] array)

ColorMatrixColorFilter 内部依赖一个 ColorMatrix 来完成颜色过滤. ColorMatrix的构造方法如下:

public ColorMatrix(float[] src)
public ColorMatrix()

ColorMatrix 使用一个 4 * 5 的矩阵进行颜色换算, 公式如下:

// 假设是这样一个矩阵:
[ a, b, c, d, e,
  f, g, h, i, j,
  k, l, m, n, o,
  p, q, r, s, t ]
// 那么颜色的计算公式如下 
新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;

直接使用 ColorMatrixColorFilter(@NonNull float[] array) 或者 ColorMatrix(float[] src) 肯定是比较困难的. 因此通常可以直接使用 ColorMatrix() 初始化 ColorMatrix 实例, 其默认矩阵如下:

[ 1, 0, 0, 0, 0,
  0, 1, 0, 0, 0,
  0, 0, 1, 0, 0,
  0, 0, 0, 1, 0 ]

套用上面的计算公式, 可以知道默认的矩阵不会影响原来的颜色. ColorMatrix 中封装了一些矩阵变换的快捷方法, 可以帮助我们进行颜色过滤.

// 将矩阵还原到默认状态 
public void reset()
// 色彩缩放.  四个参数分别表示对应颜色通道的缩放倍数
public void setScale(float rScale, float gScale, float bScale, float aScale) 
/**
 * 围绕某个颜色轴进行旋转
 * @param axis      0 绕R轴旋转;  1 绕G轴旋转;  2 绕B轴旋转
 * @param degrees   旋转的角度
 */
public void setRotate(int axis, float degrees)
/**
 * 相关矩阵相乘:   matA * matB
 */
public void setConcat(ColorMatrix matA, ColorMatrix matB)
// 等价于 setConcat(this, prematrix)
public void preConcat(ColorMatrix prematrix)
// 等价于 setConcat(postmatrix, this)
public void postConcat(ColorMatrix postmatrix)
// 设置整体的饱和度:   0 无色彩;  1 原色彩;  > 1 饱和度增加
public void setSaturation(float sat)
// Set the matrix to convert RGB to YUV
public void setRGB2YUV()
// Set the matrix to convert from YUV to RGB
public void setYUV2RGB()

看看 ColorMatrixColorFilter 的作用

色彩处理第三步: 颜色混合

相关方法

// 以当前图层中已有的内容为 目标图片(dst), 以下一步要绘制的内容为 源图片(src), 按照 xfermode 进行混合
public Xfermode setXfermode(Xfermode xfermode)

关于 Xfermode 可以回去看看上面的图片. 先来个简单的案例

private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
private Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
private String text = "CHOICE";
private Paint.FontMetrics metrics = new Paint.FontMetrics();
private float textWidth;
private float textHeight;
{
    paint.setTextSize(200);
    paint.setStyle(Paint.Style.FILL_AND_STROKE);
    paint.setTextAlign(Paint.Align.CENTER);
    paint.getFontMetrics(metrics);
    textWidth = paint.measureText(text);
    textHeight = metrics.bottom - metrics.top;
}
@Override
protected void onDraw(Canvas canvas) {
    float halfHeight = getHeight() * 1f / 2;
    // 计算文字原点, 让其居中显示
    float x = getWidth() * 1f / 2;
    float y = halfHeight + textHeight / 2 - metrics.bottom;
    int layer = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
    paint.setColor(0xfff25555);
    canvas.drawText(text, x, y, paint);
    paint.setXfermode(xfermode);
    paint.setColor(0xff18a2ff);
    canvas.drawRect(x - textWidth / 2, halfHeight, x + textWidth / 2, y + metrics.bottom, paint);
    paint.setXfermode(null);
    canvas.restoreToCount(layer);
}

再看一个案例

public class RoundImageView extends AppCompatImageView {
    ...
    // SCR_IN
    private Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    {
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setStrokeWidth(0F);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        int saveLayer = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);  // 新建一个空白图层
        float x = getWidth() * 1f/ 2;
        float y = getHeight() * 1f/ 2;
        canvas.drawCircle(x, y, Math.min(x, y), paint);        // 画一个圆形, 作为 DST
        paint.setXfermode(xfermode);                           // 以这里为分界. 之前的步骤是绘制 DST; 以下则是绘制 SRC
        canvas.saveLayer(null, paint, Canvas.ALL_SAVE_FLAG);   // 注意一定要传入 paint, 才会让后面的绘制受 Xfermode 影响
        super.onDraw(canvas);                                  // 绘制 SRC
        paint.setXfermode(null);                               // 清除 Xfermode
        canvas.restoreToCount(saveLayer);                      
    }
}

硬件加速

  1. android中, 硬件加速是指将绘制的计算工作交给GPU来处理.
    • 未开启硬件加速时: 调用 canvas.drawXX()时, 由CPU将绘制的内容, 转换为具体的像素信息, 保存在 bitmap 中, 最后在渲染到屏幕上.
    • 开启硬件加速时: 调用 canvas.drawXX()时, 由CPU将绘制的内容, 转换为GPU操作保存下来, 最后由GPU来完成渲染.
  2. 硬件加速的优点
    • 未开启硬件加速时: 绘制的内容直接被CPU转换为像素信息保存到bitmap中. 因此当某个View重绘时, 为了正确的计算出bimap中的像素, 会导致整个页面的重绘.
    • 开启硬件加速时: 绘制的内容被转换为GPU操作, 最后由GPU转换为具体的像素. 因此当某个View重绘时, 只会更新其对应的那部分GPU操作, 而不会引起整个页面的重绘.
  3. 硬件加速的限制: 受GPU绘制方式的限制, 某些API会在开启硬件加速的时候失效, 此时需要我们主动关闭硬件加速. 下面是一些API支持硬件加速的情况:
  4. 关闭硬件加速的方式
    • 针对整个应用程序
      // AndroidManifest.xml文件为application标签
      <application android:hardwareAccelerated="true" ...> 
      
    • 针对当前页面
      <activity android:hardwareAccelerated="false" /> 
      
    • 针对具体的View
      // 方式一: 在布局属性中设置
      <MyView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layerType="software"
        ... >
      // 方式二: 代码设置
      view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);  
      
  5. setLayerType 真正的作用是设置 “离屏缓冲”, 只是当参数为 View.LAYER_TYPE_SOFTWARE 时, 顺便又可以关闭硬件加速. “离屏缓冲” 是指用一个单独的地方, 将最终要渲染到屏幕上的像素信息缓存下来, 然后再由它渲染到屏幕上. “离屏缓冲” 的优点是可以缓存像素信息, 针对某些像素不发生改变的刷新场景(比如动画平移、旋转)可以利用缓存提升效率; 缺点是在需要重绘的场景下, 会因为增加额外的工作而影响效率. 因此一定要慎用 setLayerType(), 且在开启 “离屏缓存后”, 在必要时一定要及时关闭.

setLayerType() 可接受以下参数:

// 使用一个 bitmap 来缓冲, 同时会关闭硬件加速
public static final int LAYER_TYPE_SOFTWARE = 1;
// 在硬件加速开启的情况下, 会使用 GPU 来缓冲; 在已经关闭硬件加速的情况下, 并不会开启硬件加速, 而且此时该参数的作用和 LAYER_TYPE_SOFTWARE 一样
public static final int LAYER_TYPE_HARDWARE = 2;
// 不使用缓冲
public static final int LAYER_TYPE_NONE = 0;

 上一篇
Handler消息机制 Handler消息机制
说起Android的消息处理机制, Handler肯定是无法被忽略的一个点. 通过Handler收发消息可以非常方便的实现线程间通信功能. Handler的基本使用方式实际开发中, Handler的使用方式通常是这样的: Handler h
2019-03-23
下一篇 
自定义View(五)--drawText 自定义View(五)--drawText
Canvas除了能绘制基础集合图形, 绘制图片以外, 还有一个非常重要, 同时也相对复杂一点的功能 – 绘制文字. drawText 之 Canvascanvas中关于绘制文字的方法public void drawText(@NonNull
2019-03-18
  目录