Android必知必会——Paint

4,270 阅读9分钟

作为Android开发,必然在项目中或多或少的会用到Paint,那么你是否有想过Paint的主要职责是什么吗?其实,Paint类保存有关如何绘制几何图形,文本和位图的样式和颜色信息。

下面就来具体看一下,Paint有哪些可用设置或方法。

Paint的一些setter方法

由于Paint中保存了很多“画笔”的相关配置,那么就先从它的setter类型的方法看一下,都是有哪些可进行设置的项。

基础设置

  • setAntiAlias(boolean) —— 抗锯齿

    表示是否打开抗锯齿。抗锯齿会根据特定的算法(插入特定像素,使不规则的图形边缘不会看起来有那么强的毛刺感),使绘制的不规则图形(如圆形、文字等)边缘看起来更平滑。在绘制棱角分明的图像(如矩形、位图等)时,是不需要打开抗锯齿的。

  • setStyle(Paint.Style) —— 设置填充方式

    Paint.Style.STROKE

    Paint.Style.FILL

    Paint.Style.FILL_AND_STROKE

  • setShadowLayer —— 绘制内容时在内容底部加阴影

    //radius 阴影的模糊范围
    //dx dy  阴影的偏移量
    //shadowColor 阴影颜色
    setShadowLayer(float radius, float dx, float dy, int shadowColor)
    

  • setMaskFilter —— 绘制内容时在内容上面蒙一层阴影

    setMaskFilter(MaskFilter maskfilter)
    

    MaskFilter的现有子类:

    BlurMaskFilter(float radius,//模糊半径 BlurMaskFilter.Blur style)//模糊类型

    BlurMaskFilter.Blur.INNER//图像范围内加模糊

    BlurMaskFilter.Blur.NORMAL//图像范围内外都加模糊

    BlurMaskFilter.Blur.OUTER//图像留白,绘制外部模糊

    BlurMaskFilter.Blur.SOLID//图像区域不受模糊影响,外部绘制模糊

    EmbossMaskFilter(

    float[] direction, //3个标量[x,y,z]的数组,指定光源的方向

    float ambient, //环境光强度 0到1

    float specular, //高光系数

    float blurRadius //光线范围

    )

  • setPathEffect(PathEffect) —— 绘制图形时轮廓效果(虚线、曲线、折线、波浪线等)

    PathEffect的现有子类:

    CornerPathEffect 拐角变圆角

    DiscretePathEffect 随机偏离

    DashPathEffect(float[] intervals, float phase) 虚线

    intervals 指定了虚线的格式:数组中元素必须为偶数(最少是 2 个),按照「画线长度、空白长度、画线长度、空白长度」……的顺序排列

    phase 是虚线的偏移量

    PathDashPathEffect 可以指定线段形状的虚线

    SumPathEffect(PathEffect first, PathEffect second) 组合效果,分别使用first和second绘制一遍

    ComposePathEffect(PathEffect outerpe, PathEffect innerpe) 组合效果,先应用innerpe绘制一遍,再用outerpe将innerpe绘制的结果转变一下

颜色

  • setAlpha —— 透明度

  • setARGB —— 设置颜色

  • setColor —— 设置颜色

  • setColorFilter(ColorFilter) —— 设置颜色过滤

  • setShader(Shader) —— 设置着色方案

    LinearGradient —— 线性渐变色

    (float x0, float y0, //线性起点

    float x1, float y1, //线性终点

    int color0, int color1, //起止颜色

    Shader.TileMode tile) //范围外处理方案

    Shader.TileMode有如下三种类型:

    Shader.TileMode.CLAMP 端点之外延续端点处的颜色

    Shader.TileMode.MIRROR 镜像模式

    Shader.TileMode.REPEAT 重复模式

    RadialGradient —— 辐射渐变

    (float centerX, float centerY, //辐射中心点

    float radius, //辐射半径

    int centerColor, int edgeColor, //起止颜色

    TileMode tileMode)

    SweepGradient —— 扫描渐变

    (float cx, float cy, //扫描中心

    int color0, int color1)//起止颜色

    BitmapShader —— Bitmap着色器

    (Bitmap bitmap,//着色用的bitmap

    Shader.TileMode tileX, //横向超出范围处理方案

    Shader.TileMode tileY)//纵向产出范围处理方案

    ComposeShader —— 混合着色器

    (Shader shaderA, //着色方案A

    Shader shaderB, //着色方案B

    PorterDuff.Mode mode)//B(后绘制为源图像SRC)对A(已经绘制的为目标图像DST)的叠加模式,稍后会具体介绍

线条相关

  • setStrokeWidth(float) —— 线条粗细

  • setStrokeCap(Paint.Cap) —— 线冒

    Paint.Cap.BUTT 平头,默认

    Paint.Cap.ROUND 圆头

    Paint.Cap.SQUARE 方头

  • setStrokeJoin(Paint.Join) —— 拐点处理方式

    Paint.Join.MITER 尖角,默认

    Paint.Join.ROUND 圆角

    Paint.Join.BEVEL 平角

  • setStrokeMiter(float ) —— 对Paint.Join.MITER的补充,极端情况下尖角会非常长又尖,通过设置此值,可以削掉太长的尖。

文字

  • fakeBoldText(boolean) —— 文字粗体

  • setFontFeatureSettings(String) —— 绘制文字使用的CSS样式

  • setLetterSpacing(float) —— 字符间距

  • setLinearText(boolean) —— 是否打开线性文本标识

    在Android中文本的绘制需要使用一个bitmap作为单个字符的缓存,既然是缓存必定要使用一定的空间,我们可以通过setLinearText (true)告诉Android我们不需要这样的文本缓存。

  • setStrikeThruText (boolean) —— 添加删除线

  • setTextAlign(Paint.Align) —— 文字对齐方式

    Paint.Align.LEFT 左对齐

    Paint.Align.CENTER 居中

    Paint.Align.RIGHT 右对齐

  • setTextLocale(Locale) —— 设置地区(Locale.getDefault使用默认地区)

  • setTextScaleX(float) —— 文字横向缩放因子

  • setTextSize(float pixel) —— 文字大小

  • setTextSkewX(float) —— 文字横向错切因子

  • setTypeface(Typeface) —— 设置字体

  • setUnderlineText(boolean) —— 设置下划线

  • setWordSpacing(float pixel) —— 设置词间距,默认0

  • setSubpixelText(boolean) —— true,有助于LCD文字显示

Bitmap相关

  • setDither(boolena) —— 是否开启抖动

    所谓抖动,是指把图像从较高色彩深度(即可用的颜色数)向较低色彩深度的区域绘制时,在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法。

  • setFilterBitmap(boolean) —— 是否使用双线性过滤来绘制 Bitmap

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

绘制重叠

  • setXfermode(Xfermode)

Xfermode的唯一子类是PorterDuffXfermode(PorterDuff.Mode),在初始化时需要传入PorterDuff.Mode

  • setBlendMode(BlendMode)

    在setXfermode的基础上,增加了几种混合模式

    合成规则分两大类

    蓝色为SRC(后绘制),红色为DST(已经绘制)

    Alpha合成模式:

    混合模式:

    BlendMode新增的几种方式:

Paint的一些其它方法

文字尺寸相关

  • getFontMetrics(FontMetrics)/getFontMetricsInt(FontMetricsInt)

    获取Paint(而非具体文字)的FontMetrics。

  • getFontSpacing

    获取推荐的行距。

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

    获取文字的显示(能看见的)范围。

  • float measureText(String text)

    测量文字占用(包含视觉上看不见)的宽度。

  • getTextWidths(String text, float[] widths)

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

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

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

  • ascent

    当前字体和字号下,Paint的ascent。

  • descent

    当前字体和字号下,Paint的descent。

光标相关

  • getRunAdvance

    对于一段文字,计算出某个字符处光标的 x 坐标。

  • getOffsetForAdvance

    给出一个位置的像素值,计算出文字中最接近这个位置的字符偏移量(即第几个字符最接近这个坐标)。

Path相关

  • getFillPath(Path src, Path dst)

    默认情况下(线条宽度为0、没有PathEffect),那么获取到的path即为绘制的path(drawPath);其他情况下path跟源path会不一致:

  • getTextPath

    获取目标文字对应的path。

关于FontMetircs

FontMetrics 是个相对专业的工具类,它提供了几个文字排印方面的数值:ascent, descent, top, bottom, leading。

图中有五条线,还有一个特殊的leading:

  • top/bottom:限制所有字形的顶部和底部范围;除了普通字符,有些字形的显示范围是会超过 ascent 和 descent 的,而 top 和 bottom 则限制的是所有字形的显示范围,包括这些特殊字形。由于是针对baseline的位移,所以top为负值,bottom为正值。

  • ascent/descent:限制普通字符的顶部和底部范围;普通的字符,上不会高过 ascent ,下不会低过 descent。 由于是针对baseline的位移,所以ascent为负值,descent为正值。

  • baseline:文字绘制的基线。

  • leading:上下相邻的两行,上行的 bottom 线和下行的 top 线的距离。

绘制文字时的纵向居中

在使用Canvas绘制文字时,使用方法:

//text 是文字内容,x 和 y 是文字的坐标
drawText(String text, float x, float y, Paint paint)

但是这里指定的y坐标为baseline的位置,所以如果想要文字根据某个y值纵向居中,那么就需要将baseline的位置适当下移,可以通过如下方式来计算需要下移的值:

//核心思想:计算出baseline需要偏移的距离,已使文字中心点在y值
//1.找到中心点位置(必定在baseline上方)
//2.中心点距离baseline有一个位移值(由于在baseline上方,所以是负值)
//3.绘制时移动这么一个位移值(减去负值即加正值,向下移动)

//方法1 不在乎绘制的文字是什么,统一方案居中,如果绘制内容为aaaa,那么会显得考下,因为aaaa本身高度就比较低
paint.getFontMetrics(fontMetrics)
deltY = (fontMetrics.ascent + fontMetrics.top)/2

//方法2 根据当前绘制的文字,精确的根据具体文字进行居中处理,效果会更好
paint.getTextBounds("Crazy Coder",0,"Crazy Coder".length(),rect)
deltY = (rect.top + rect.bottom) / 2

//那么纵向居中绘制时,就应该是这样的,减去负值
drawText("Crazy Coder",x,y - deltY,paint)

绘制文字时严格靠齐x坐标

在通过drawText绘制文字时,设置了x坐标,但是实际绘制之后的效果,其实并不是从x开始绘制文字,而是这样的效果:

可以看出,起始点(红点)在第一个字母H的前面一点,产生这种情况的原因是因为,每个字符都有左右边距,而且设置的字体越大,那么这个边距也就越大。

本身绘制文字时,文字占据的空间:

蓝色为本身占据的空间,橙色为getTextBounds获取到的空间。

那么关于横向绝对定义,可以通过如下方法处理:

paint.getTextBounds("Crazy Coder",0,"Crazy Coder".length(),rect)
deltX = rect.left
//横向绝对对齐,就应该是这样的
drawText("Crazy Coder",x - deltX,y,paint)

绘制文字时换行的处理

  • StaticLayout

    StaticLayout 并不是一个 View 或者 ViewGroup ,而是 android.text.Layout 的子类,它是纯粹用来绘制文字的。 StaticLayout 支持换行,它既可以为文字设置宽度上限来让文字自动换行,也会在 \n 处主动换行。

    它的构造方法:

    StaticLayout(
    CharSequence source, //要绘制的文本
    TextPaint paint, //绘制文本的画笔
    int width, //宽度限制
    Layout.Alignment align, //文字对齐方向
    float spacingmult, //行间距的倍数,通常为1
    float spacingadd, //行间距的额外增加值,通常为0
    boolean includepad//是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界
    )
    
  • Paint.breakText 实现图文混排

    使用此方法的核心在于:

    1.根据当前行可用宽度widthMax,通过breakText获知该行可以绘制的文字个数count

    int count = paint.breakText(
    "Crazy Coder Crazy Coder Crazy Coder Crazy Coder..."
    true,
    widthMax,
    cutWidth
    )
    

    2.绘制当前行文字,起始文字角标为上一行末尾文字角标index,终止角标为index + count

    3.绘制当前行文字时,需要计算好当前行的y坐标,每绘制一行,即currentY = currentY + piant.getFontSpace() * (lineCount - 1)

    canvas.drawText(
    "Crazy Coder Crazy Coder Crazy Coder Crazy Coder..."
    index,
    index + count,
    startX,
    currentY,
    paint
    )
    

参考资料