6.自定义View-绘制

绘制的基本要素

自定义绘制的最基本的步骤是,提前创建好Paint对象,重写onDraw(),把绘制代码写在onDraw里面.

一、Canvas.drawXXX() 和 Paint 基础

  1. drawXXX() 方法:在整个绘制区域统一涂上指定的颜色。

    1. Canvas.drawColor(@ColorInt int color) 颜色填充

    2. drawRGB(int r, int g, int b) 和 drawARGB(int a, int r, int g, int b) 作用同上

    3. drawCircle(float centerX, float centerY, float radius, Paint paint) 画圆

    4. drawRect(float left, float top, float right, float bottom, Paint paint) 画矩形

    5. drawPoint(float x, float y, Paint paint) 画点

    6. drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) 画多个点

    7. drawOval(float left, float top, float right, float bottom, Paint paint) 画椭圆

      left, top, right, bottom 是这个椭圆的左、上、右、下四个边界点的座标。

    8. drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 画线

    9. drawLines(float[] pts, int offset, int count, Paint paint) / drawLines(float[] pts, Paint paint) 画多条线

    10. drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 画圆角矩形

    11. drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制弧形或扇形

      startAngle 是弧形的起始角度(x 轴的正,是 0 度的位置;顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形

    12. drawPath(Path path, Paint paint) 画自定义图形

      path参数是用来描述图形路径的对象,path的类型是PathPath可以描述直线、二次曲线、三次曲线、圆、椭圆、弧形、矩形、圆角矩形。

      Path有两类方法,一类是直接描述路径的,另一类是辅助的设置或计算

      1. 直接描述路径

      这一类的方法可以分钟两组:添加子图形和画线

      1. addXxx() ——添加子图形状

        path.AddXxx()) + canvas.drawPath(path, paint) 这种写法,和直接使用 canvas.drawXxx() 的效果是一样的,所以如果只画一个图形,没必要用Path,直接用drawXXX就可以了,drawPath一般是在绘制组合图形时才会使用。

      2. xxxTo ——画线(包括直线和曲线)

        这一组和上面的区别在于,上一组是添加的完整封闭图像,而这一组添加的只有一条线

        • lineTo(float x, float y) / rLineTo(float x, float y) 从当前位置向目标位置画一条直线

        • quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 画二次贝塞尔曲线,x1, y1 和 x2, y2 则分别是控制点和终点的座标

          贝塞尔曲线:贝塞尔曲线是几何上的一种曲线,它通过起点、控制点和终点描述一条曲线,主要用于计算机图形学

        • cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) / rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 画三次贝塞尔曲线

        • moveTo(float x, float y) / rMoveTo(float x, float y) 移动到目标位置(不画线了)

          moveTo(x,y)不添加图形,它会设置图形的起点

        • arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)画弧形

          forceMoveTo 参数用于表述从当前点到弧的起点之间是否要画线,为true是不画线

        • addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)

          这也是一个添加弧形的方法,它是直接使用forceMoveTo=true的简化版本

        • close 封闭当前字图形

          把当前的子图形封闭,即由当前位置向当前子图形的起点绘制一条直线

      2.辅助的设置或计算
      1. Path.setFillType(fillType)

        前面在说 dir 参数的时候提到, Path.setFillType(fillType) 是用来设置图形自相交时的填充算法的:

        在这里插入图片描述
        FillType的取值有四个:

        • EVEN_ODD
        • WINDING(默认值)
        • INVERSE_EVEN_ODD
        • INVENRSE_SINDING

        后面的两个带有 INVERSE_ 前缀的,只是前两个的反色版本,只要弄懂前两个即可

        EVEN_ODD 和 WINDING 的原理
        1. EVEN_ODD

          即even_odd rule(奇偶原则):对于平面中的任意一点,想任何方向射出一条射线,这条射线和图形相交(不包含相切)的次数如果是奇数,则这个点被认为在图像内部,是要被涂色的区域;如果是偶数,则这个点被认为在图形外部,是不被涂色的区域。

          如下为左右两圆相交的示例

          6eab1882cbbc67c5c9bef7ffff7cb11e

        2. WINDING

          即 non-zero winding rule(非零环绕数原则):首先他需要你图形的所有线条都是有绘制方向的,然后同样是从平面中的点向任意方向射出一条射线,以0为初始值,对于射线和图形的所有焦点,遇到每个顺时针的交点把结果加1,遇到每个逆时针的交点把结果减1,最终把所有的交点都算上,如果得到的结果不是0,则认为这个点在图形内部,是要被涂色的区域;如果是0则认为这个点在图形的外部,是不被涂色的区域
          af36e2f187093a0cf0377a52797306f7

    13. drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 画 Bitmap

    14. drawText(String text, float x, float y, Paint paint) 绘制文字

  2. Paint基础

    • Paint.setColor(int color) 设置颜色

      设置画笔的颜色,后面使用该画笔绘制的图形将用这里设置的颜色填充

    • Paint.setStyle(Paint.Style style) 设置绘制模式
      Style有三种模式:FILLSTROKEFILL_AND_STROKEFILL是填充模式,STROKE是画线模式(描边),FILL_AND_STROKE是两种模式一并使用,即画线有填充。它的默认只是FILL,填充模式

    • Paint.setStrokeWidth(float width) 设置线条宽度

      设置描边的线条宽度

    • Paint.setAntiAlias(boolean aa) 设置抗锯齿开关

      或者在new Paint() 的时候加上一个 ANTI_ALIAS_FLAG 参数也可以

      没有开启抗锯齿的时候,图形或有毛边现象。对比效果如下
      1aeb52f20ba02eee091205ecef13280c

      为什么抗锯齿开启之后的图形边缘会更加平滑呢?因为抗锯齿的原理是:修改图形边缘处的像素颜色,从而让图形在肉眼看来具有更加平滑的感觉

      cdca72028f8aed2888826e5fef014260
      未开启抗锯齿的圆,所有像素都是同样的黑色,而开启了抗锯齿的圆,边缘的颜色被略微改变了。这种改变可以让人眼有边缘平滑的感觉,但从某种角度讲,它也造成了图形的颜色失真。

    • Paint.setTextSize(float textSize) 设置文字的大小

    Android 的座标系:

    在 Android 里,每个 View 都有一个自己的座标系,彼此之间是不影响的。这个座标系的原点是 View 左上角的那个点;水平方向是 x 轴,右正左负;竖直方向是 y 轴,下正上负,

    如下图
    [

二、Paint详解

henCoder链接https://juejin.im/post/596baf5f6fb9a06bb15a3df9

Paint的API大致可以分为4类

  • 颜色
  • 效果
  • drawText相关
  • 初始化
1)颜色

Canvas绘制的内容,有三层对颜色的处理:基本颜色,ColorFilter,Xfermode

  1. 基本颜色

    像素的基本颜色,根据绘制内容而有不同的控制方式:Canvas的颜色填充类方法drawColor/RGB/ARGB() 的颜色,直接写在方法参数里;drawBitmap() 的颜色,是有Bitmap对象来提供的;除此之外的图形和文字的绘制,他们的颜色使用paint参数来额外设置;

    paint设置颜色有两种方法:一种是直接用 Paint.setColor/ARGB() 来设置颜色,另一种是使用 Shader 来指定着色方案

    paint使用 setShader(Shader shader)方法 来设置Shader

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

    Android中绘制不直接使用Shader,而是使用它的几个子类。包括

    • LinearGradient 线性渐变

      设置两个点和两种颜色,以这两个点位断点,使用两种颜色的渐变来绘制颜色

    • RadiaGraient 辐射渐变

      从中心向周围辐射状的渐变。需要设置中心的颜色和边缘颜色

    • SweepGradient 扫描渐变

      以扫描中心为原点,从x轴正向开始,从开始颜色扫描360度到终止颜色

    • BitmapShaderBitmap 来着色

      用Bitmap的像素来作为图形和文字的填充

    • ComposeShader 把两个Shader混合使用

    构造方法:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

    最后的参数mode用于指定两个Shader的叠加模式

    PorterDuff.Mode

    PorterDuff.Mode 是用来指定两个图像共同绘制时的颜色策略的。【颜色策略】是指把***源图像绘制到目标图像*** 处时应该怎样确定二者结合后的颜色。

    PorterDuff.Mode 一共有 17 个,可以分为两类

    1. Alpha合成

      Alpha合成,就是【PorterDuff】这个词所代指的算法,来源于Tomas Porter和Tom Duf两个人共同发表的论文,论文描述了12种将两个图像共同绘制的算法。而这些算法都是关于Alpha通道计算的,后来就吧这类计算称为Alpha 合成

      3444dfcd8745677dc668ffd94fd26cb8

      Alpha合成

      在这里插入图片描述

    2. 混合

      这一类操作的是颜色的本身,并不是Alpha通道,不属于Alpha合成,和 Porter 与 Duff 这两个人也没什么关系

      混合

      4be41991c90e3eacf92e9378168b681d

  2. setColorFilter(ColorFilter colorFilter)

    ColorFilter,为绘制设置颜色过滤,也就是为绘制的内容设置一个统一的过滤策略,然后Canvas.drawXxx()方法会对每个像素都进行过滤后再绘制出来。

    在Android里ColorFilter并不被直接使用,而是使用他的三个子类

    • LightingColorFilter

    LightingColorFilter用来模拟简单的光照效果。

    LightingColorFilter的构造方法是 LightingColorFilter(int mul, int add) ,参数里的muladd都是和颜色值格式相同的int值,mul用来和目标像素相乘,add用来和目标相素相加

    R' = R * mul.R / 0xff + add.R
    G' = G * mul.G / 0xff + add.G
    B' = B * mul.B / 0xff + add.B
    
    • PorterDuffColorFilter

      PorterDuffColorFilter 的作用是使用一个指定的颜色和一种指定的PorterDuff.Mode来与绘制对象进行合成,它的构造方法是 PorterDuffColorFilter(int color, PorterDuff.Mode mode) 其中的color参数是指定的颜色,mode参数是指定的PorterDuff.Mode,跟 ComposeShader 的一样,不同的是PorterDuffColorFilter 只能指定一种颜色作为源,而不是Bitmap

    • ColorMatrixColorFilter

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

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

      通过计算,ColorMatrix可以把要绘制的像素进行转换,对于颜色[R,G,B,A],转换算法为

      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;
      
  3. setXfermode(Xfermode xfermode)

“Xfermode"其实就是"Transfer mode",用"X"来代替”Trans“是美国人喜欢用的简写方式.

Xfermode指的是你要绘制的内容和Canvas的目标位置的内容应该怎样结合计算出最终的颜色。就是以 要绘制的内容 作为 源图像,以View中 已有的内容 作为 目标图像,选取一个 PorterDuff.Mode 作为绘制内容的颜色处理方案。可以这么记忆:把源图像(要绘制的内容)绘制到目标图像(已有的内容)

实际上创建 Xfermode 的的时候要使用它的子类PorterDuffXfermode

**Xfermode **注意事项

  1. 使用离屏缓冲

    由于在绘制是,目标图像是真个View的所有内容,而且View自身的底色并不是默认的透明色,而且是一种不知道的颜色,导致在与源图像混合计算后,不仅仅是已有的图像进行了覆盖,还使得图像之外都变成里黑色,就像下面

    在这里插入图片描述

    导致要想使用serXfermode()正常绘制,必须使用离屏缓存把内容绘制在额外的层上,再把绘制的内容贴会到View中。

    在这里插入图片描述

    使用离屏缓冲有如下两种方式

    • Canvas.saveLaver()

      saveLayer() 可以做短时的离屏缓冲。只要绘制前调用该方法保存,绘制后恢复即可

      int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
      
      canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方
      paint.setXfermode(xfermode); // 设置 Xfermode
      canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆
      paint.setXfermode(null); // 用完及时清除 Xfermode
      
      canvas.restoreToCount(saved);
      
    • View.setLayerType

      View.setLayerType() 是直接把整个 View 都绘制在离屏缓冲中。

      setLayerType(LAYER_TYPE_HARDWARE) 是使用GPU来缓冲,

      setLayerType(LAYER_TYPE_SOFTWARE) 是直接用一个Bitmap来缓冲

  2. 控制好透明区域

    使用Xfermode来绘制内容,除了上面还应该注意它的透明区域不要太小,要让它足够覆盖到要和它结合的绘制的内容,否则结果无法达到我们的目标

2)效果
  1. setAntiAlias (boolean aa) 设置抗锯齿

  2. setStyle(Paint.Style style)

  3. 线条形状

    设置线条形状一共有四个方法

    • setStrokeWidth(float width)

      设置线条宽度,默认值为0

      线条宽度为0和1的区别

      在进行几何变换后,当线条放大到2倍时,如果线条宽度为1,则线条宽度变为2个像素,而若线条宽度为0则它被锁定为1个像素,不受几何变换的影响

      Google 在文档中把线条宽度为 0 时称作「hairline mode(发际线模式)」。

    • setStrokeCap(Paint.Cap cap)

      设置线头的形状。线头形状有三种BUTT 平头、ROUND 圆头、SQUARE 方头。默认为 BUTT

      当线条的宽度是1像素时,这三种线头的表现是一致的,全为一个像素的点,但当线条变粗时,他们将表现出不同的样子

      8fa8c7cfb47fa0f6d5d3c8c18635ae82

    • setStrokeJoin(Paint.Join join)

      设置拐角的形状。有三个值可以选择:MITER 尖角、 BEVEL 平角和 ROUND 圆角。默认为 MITER

      6a5f1de602276ce42cc5a7313a59fc08

    • setStrokeMiter(float miter)

      这个方法是对setStrokeJoin(Paint.Join join)的补充,它用于设置 MITER 型拐角的延长线的最大值。

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

      img

      但是如果拐角的角度太小,就有可能会出现连接点过长的情况

      img

      所以为了避免过长的尖角出现,MITER 型连接点在尖角太长时,自动改用BEVEL 的方式来渲染连接点。

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

      img
  4. 色彩优化

    1. setDither(bolean dither) 设置图像抖动

      抖动是指把图像从较高色彩深度(可用的颜色数)向较低色彩深度的区域绘制时(譬如 32 位的 ARGB_8888 ->16 位色的ARGB_4444 ),在图像中有意地插入噪点,通过有规律的扰乱图像来让图像对于肉眼更加真实的做法。抖动不可用在纯色的绘制中。在实际的场景中,抖动更多的作用是在图像降低色彩深度绘制时,避免出现大片的色带与色块。

      img
    2. setFilterBitmap(boolean filter) 设置是否使用双线性过滤来绘制 Bitmap

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

      img

      最近邻插值及双线性插值

  5. setPathEffect(PathEffect effect) 设置图像轮廓效果

    Andorid 有6种PathEffect

    • CornerPathEffect 把所有拐角都变为圆角

      img
    • DiscretePathEffect 把线条进行随机的偏离

      具体的显示方式是:把绘制改为使用定长的线段来拼接,并且在拼接的时候对路径进项随机偏离

      构造方法 DiscretePathEffect(float segmentLength, float deviation),其中segmentLength 是用来拼接的每个线段的长度,deviation是偏离量

      img
    • DashPathEffect 使用虚线来绘制线条

      构造方法 DashPathEffect(float[] intervals, float phase) ,第一个参数intervals是一个数组,指定了虚线的格式:数组中的元素必须为大于0的偶数,按照[划线长度、空白长度、划线长度、空白长度…]的顺序排列,第二个参数phase是虚线的便宜量

      img
    • PathDashPathEffect 使用路径来绘制虚线线条

      构造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) ,其中第一参数shape是用来绘制的Pathadvance是两个相邻的shape段起点之间的间隔;phase和上面一样是虚线的偏移;最后一个参数style是来用来指定拐弯改变时shape的转换方式。style的类型为PathDashPathEffect.Style,是一个枚举,有三个值:

      • TRANSLATE:位移
      • ROTATE:旋转
      • MORPH:变体
      img
    • SumPathEffect 组合两种PathEffect分别对目标同时进行绘制

      img
    • ComposePathEffect 组合两个PathEffect,先使用一种PathEffect,然后在对结果使用另一种PathEffect进行改变

    构造方法 ComposePathEffect(PathEffect outerpe, PathEffect innerpe) ,其中 innerpe 是先应用的, outerpe 是后应用的。

    注意: PathEffect 在有些情况下不支持硬件加速,需要关闭硬件加速才能正常使用:

    Canvas.drawLine()Canvas.drawLines() 方法画直线时,setPathEffect() 是不支持硬件加速的;

    PathDashPathEffect 对硬件加速的支持也有问题,所以当使用 PathDashPathEffect 的时候,最好也把硬件加速关了。

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

    在之后绘制的内容下面加一层阴影,方法的参数里,radius是阴影的模糊范围;dx dy是阴影的偏移量;shadowColor是阴影的颜色

    如果要清楚阴影层,使用cleanShadowLayer

    注意:

    • 在硬件加速时,setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。
    • 如果 shadowColor 是半透明的,阴影的透明度就是 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,阴影的透明度就是paint的透明度
  7. setMaskFilter(MaskFilter maskfilter)

    为之后的绘制的设置MashFilter。它与上面的阴影相反,他是绘制在绘制层上方的附加效果(遮罩),类似于背景和前景

    MaskFilter 有两种

    • BlurMaskFilter 模糊效果的MaskFilter

      构造方法 BlurMaskFilter(float radius, BlurMaskFilter.Blur style),其中,radius参数是模糊的范围,style四模糊的类型。一种有四种:

      • NORMAL:内外都模糊绘制

      • SOLID:内部正常绘制,外部模糊

      • INNER:内部模糊,外部不绘制

      • OUTER:内部不绘制,外部模糊

        img
    • EmbossMashFilter

      浮雕效果的 MaskFilter

      构造方法 EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius) ,其中direction 是一个 3 个元素的数组,指定了光源的方向; ambient 是环境光的强度,数值范围是 0 到 1; specular 是炫光的系数; blurRadius 是应用光线的范围

      img
  8. 获取绘制的Path

    根据 paint 的设置,计算出绘制 Path 或文字时的 实际 Path。所谓「实际 Path ,指的就是 drawPath() 的绘制内容的轮廓,要算上线条宽度和设置的 PathEffect

    • getFillPath(Path src, Path dst)

      src 是原 Path ,而 dst 就是实际 Path 的保存位置。 getFillPath(src, dst) 会计算出实际 Path,然后把结果保存在 dst 里。

      img
    • getTextPath(String text, int start, int end, float x, float y, Path path) / getTextPath(char[] text, int index, int count, float x, float y, Path path)

      getTextPath() 方法,获取的就是目标文字所对应的 Path 。这个就是所谓「文字的 Path

      img
    3)初始化类

    这一类方法专门用来初始化Paint对象,或者批量设置Paint的多个属性的方法

    1. reset()

      重置Paint的所有属性为默认值,相当于一个重新new一个,不过耗性能低

    2. set(Paint src)

      src的所有属性全部复制过来。

    3. setFlags(int flags)

      批量设置flags。相当于依次调用它们的set方法

      paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
      

      相当于

      paint.setAntiAlias(true);
      paint.setDither(true);
      

      setFlags(flags) 对应的 get 方法是 int getFlags()

三、文字的绘制

1)Canvas绘制文字的方式
  1. drawText(String text, float x, float y, Paint paint)

    text 是文字内容,xy 是文字的座标。这个座标并不是文字的左上角,而是一个与左下角比较接近的位置。

    drawText() 参数中的 y ,指的是文字的**基线( baseline )** 的位置。

    x 点并不是文字左边的位置,而是比它的左边再往左一点点。是因为绝大多数的字符,它们的宽度都是要略微大于实际显示的宽度的。字符的左右两边会留出一部分空隙,用于文字之间的间隔,以及文字和边框的间隔。而往左的那一点点就是文字的间隔

    img
  2. drawTextRun()

    这个方法对中国人没用,忽略

  3. drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)

    沿着一条 Path 来绘制文字。

    hOffsetvOffset。它们是文字相对于 Path 的水平偏移量和竖直偏移量,利用它们可以调整文字的位置

    img

    注意:drawTextOnPath() 使用的 Path ,拐弯处全用圆角,别用尖角。否则会像上面一样很难看

  4. StaticLayout

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

    构造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad),其中参数里

    width 是文字区域的宽度,文字到达这个宽度后就会自动换行;
    align 是文字的对齐方向;
    spacingmult 是行间距的倍数,通常情况下填 1 就好;
    spacingadd 是行间距的额外增加值,通常情况下填 0 就好;
    includeadd 是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。

    使用方法:

    String text1 = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";
    StaticLayout staticLayout1 = new StaticLayout(text1, paint, 600,
            Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    canvas.save();
    staticLayout1.draw(canvas);
    canvas.restore();
    
2)Paint对文字绘制的辅助
  1. 设置显示效果类

    • setTextSize(float textSize) 设置文字大小。

    • setTypeface(Typeface typeface) 设置字体。

      paint.setTypeface(Typeface.DEFAULT);
      canvas.drawText(text, 100, 150, paint);
      paint.setTypeface(Typeface.SERIF);
      canvas.drawText(text, 100, 300, paint);
      paint.setTypeface(Typeface.createFromAsset(getContext().getAssets(), "Satisfy-Regular.ttf"));
      canvas.drawText(text, 100, 450, paint);
      

      typeface 指的是某套字体(即 font family ),而 font 指的是一个 typeface 具体的某个 weight 和 size 的分支。

    • setFakeBoldText(boolean fakeBoldText) 否使用伪粗体。

      它并不是通过选用更高 weight 的字体让文字变粗,而是通过程序在运行时把文字给「描粗」

    • setStrikeThruText(boolean strikeThruText) 是否加删除线。

    • setUnderlineText(boolean underlineText) 是否条件下划线

    • setTextSkewX(float skewX) 设置文字横向错切角度。

    • setTextScaleX(float scaleX) 设置文字横向放缩

    • setLetterSpacing(float letterSpacing) 设置字符间距

    • setFontFeatureSettings(String settings) 用 CSS 的 font-feature-settings 的方式来设置文字。

    • setTextAlign(Paint.Align align) 设置文字的对齐方式

      一共有三个值:LEFT CETNERRIGHT。默认值为 LEFT

    • setTextLocale(Locale locale) 设置绘制所使用的 Locale

    • setHinting(int mode) 设置是否启用字体的 hinting (字体微调)

      Android 设备大多数都是是用的矢量字体。在字号较大的时候,矢量字体也能够保持字体的圆润。但是当文字的尺寸过小(比如高度小于 16 像素),有些文字会由于失去过多细节而变得不太好看。 hinting 技术就是为了解决这种问题的:通过向字体中加入 hinting 信息,让矢量字体在尺寸过小的时候得到针对性的修正,从而提高显示效果。

    • setSubpixelText(boolean subpixelText) 是否开启次像素级的抗锯齿

  2. 测量文字尺寸类

    1. float getFontSpacing()

      获取推荐的行距。

      获取推荐的两行文字的baseline 的距离。这个值是系统根据文字的字体和字号自动计算的。它的作用是当你要手动绘制多行文字的时候,可以在换行draw的时候确认下一行文字的y座标

    2. FontMetircs getFontMetrics()

      获取 PaintFontMetrics

      FontMetrics 提供了几个文字排版方面的数值:ascent,descent,top,bottom,leading.
      在这里插入图片描述
      ascentdescent是上图中绿色和橙色的线,它们限制普通字符的的顶部和底部范围。在Android中,ascent的值是图中绿色线和baseline的相对位移,它的值为负(y座标向下为正);descent的值是图中橙色和baseline的相对位移,它的值为正

      top/bottom的作用是限制所有字形的顶部和底部范围。除了普通字符,有些字形的显示范围会超过ascentdescent,但是包括这些特殊字符都不会超过topbottom。同上类似,top的值是蓝线相对baseline的位移,值为负;bottom是红线相对baseline的值,值为正

      leading指的是行的额外间距,即在相邻的两行中上一行的bottom和下一行的top的距离

      leading的本意是行距,即连个相邻行baseline中间的距离,但是在很多非专业领域,leading被用来指代航的额外间距,Android中就是这样。

      FontMetrics 提供的就是 Paint 根据当前字体和字号,得出的这些值的推荐值。它把这些值以变量的形式存储;

      ascentdescent 这两个值还可以通过 Paint.ascent()Paint.descent() 来快捷获取

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

      获取文字的显示范围

      text 是要测量的文字,startend 分别是文字的起始和结束位置,bounds 是存储文字显示范围的对象,方法在测算完成之后会把结果写进 `bounds

      该方法测量的是文字的显示范围

      a14be0c0279959d02d8b5c86926a3293

    4. float measureText(String text)

      测量文字的宽度并返回

      该方法测量的是文字绘制时所占用的宽度,一个文字在界面中,往往需要占用比他的实际显示宽度更多一点的宽度。

    5. getTextWidths(String text, float[] widths)

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

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

      在给出最大宽度的前提下测量文字宽度,如果文字的宽度超出限制,则在临近超限的位置截断文字。

      其返回值是截取的文字个数(没有超限时,是文字总个数)。

      方法中,text 是要测量的文字;measureForwards 表示文字的测量方向,true 表示由左往右测量;maxWidth 是给出的宽度上限;measuredWidth 是用于接受数据,而不是用于提供数据的:方法测量完成后会把截取的文字宽度(如果宽度没有超限,则为文字总宽度)赋值给 measuredWidth[0]

      这个方法可以用于多行文字的折行计算

    7. 光标相关

      • getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)

        对于文字text计算出offset个字符处光标的x座标。start end是文字的起始和结束座标;

        contextStart contextEnd是上下文的起始和结束座标;isRtl是文字的方向。

      • getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)

        给出一个位置的像素值advance,计算出文字text中最接近这个位置的字符偏移量。

        start end 是文字的起始和结束座标;contextStart contextEnd 是上下文的起始和结束座标;isRtl 是文字方向;返回结果是对应的字符偏移量。

      • hasGlyph(String string)

        检查指定的字符串中是否是一个单独的字形。
        img

四、Canvas 对绘制的辅助

  1. 范围裁切

    • clipRect()

      将画布裁切成一个矩形范围,后面的绘制都在这个范围内,超出范围的部分将会被裁掉

    • clipPath

      同上,只不过形状不是矩形了,而是一个Path

  2. 几何变换

    • 使用Canvas来做常见的二维变幻

      • Canvas.translate(float dx, float dy) 平移

        将画布分别沿着x轴平移dxy轴平移 dy

      • Canvas.rotate(float degrees, float px, float py) 旋转

        degrees 是旋转角度,同样是对画布的旋转,选装中心是px,py

      • Canvas.scale(float sx, float sy, float px, float py) 放缩

        sx sy 是横向和纵向的放缩倍数; px py 是放缩的轴心。

      • skew(float sx, float sy) 错切

        参数里的 sxsy 是 x 方向和 y 方向的错切系数

    • 使用Matrix来做变换

      • 使用Matrix来做常见变换

        一般步骤为:

        1. 创建 Matrix 对象;
        2. 调用 Matrixpre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
        3. 使用 Canvas.setMatrix(matrix)Canvas.concat(matrix) 来把几何变换应用到 Canvas

        Matrix 应用到 Canvas 有两个方法:

        1. Canvas.setMatrix(matrix):用Matrix直接替换Canvas当前的变换矩阵(hencoder 说不同的手机系统中 setMatrix(matrix) 的行为可能不一致,建议用下面的)。
        2. Canvas.concat(matrix):用Canavas当前的变换矩阵和Matrix相乘,及将Canvas当前的变换叠加上Matrix的变换
      • 使用Matrix来做自定义变换

        Matrix 的自定义变换使用的是setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)方法

        setPolyToPoly() 的作用是通过多点映射的方式来直接设置变换。

        多点影射的意思就是把指定的点移动到给出的位置,从而发生形变

        srcdst 是源点集合目标点集;srcIndexdstIndex 是第一个点的偏移;pointCount 是采集的点的个数(个数不能大于 4,因为大于 4 个点就无法计算变换了)

        Matrix matrix = new Matrix();
        float pointsSrc = {left, top, right, top, left, bottom, right, bottom};
        float pointsDst = {left - 10, top + 50, right + 120, top - 90, left + 20, bottom + 30, right + 20, bottom + 60};
        
        ...
        
        matrix.reset();
        matrix.setPolyToPoly(pointsSrc, 0, pointsDst, 0, 4);
        
        canvas.save();
        canvas.concat(matrix);
        canvas.drawBitmap(bitmap, x, y, paint);
        canvas.restore();
        
  3. 使用Camera来做三维变换

    • Camera.rotate*() 三维旋转

      rotateX(deg)

      rotateY(deg)

      rotateZ(deg)

      rotate(x, y, z)

    • Camera.translate(float x, float y, float z) 移动

    • Camera.setLocation(x, y, z) 设置虚拟相机的位置

      它的参数的单位不是像素,而是 inch,英寸

      Camera 的位置单位是英寸,英寸和像素的换算单位被写死为了 72 像素

五、绘制顺序

  1. 写在 super.onDraw()的下面

    由于绘制代码会在原有内容绘制结束之后才执行,所以绘制内容就会盖住控件原来的内容。

    常见的情况:为控件增加点缀性内容;比如,在 Debug 模式下绘制出 ImageView 的图像尺寸信息

  2. 写在 super.onDraw() 的上面

    由于绘制代码会执行在原有内容的绘制之前,所以绘制的内容会被控件的原内容盖住

    一般可以通过在文字的下层绘制纯色矩形来作为「强调色」

  3. dispatchDraw():绘制子 View 的方法

    当我们继承一个ViewGroup类型的View时,在onDraw里绘制了内容会正常显示(前提是做了setWillNotDraw(false)等会让ViewGroupOndraw方法的设置)

    但是当我们添加自view后,我们的绘制信息会消失,即使我们的绘制内容写在super.onDraw()的下面。

    这是因为在绘制过程中,每一个 ViewGroup会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。所以在onDraw这个方法里我们永远无法让我们的绘制内容显示在子View上方

    上面说的绘制子View的方法叫做 dispatchDraw() ,我们只要重写这个方法,并把内容写在super.dispatchDraw() 的下面即可

  4. 绘制过程简述

    一个完整的绘制过程会依次绘制以下几个内容:

    1. 背景
    2. 主体(onDraw()
    3. 子 View(dispatchDraw()
    4. 滑动边缘渐变和滑动条
    5. 前景
    img
  5. onDrawForeground()

    这个方法是 API 23 才引入的,所以在重写这个方法的时候要确认你的 minSdk 达到了 23,不然低版本的手机装上你的软件会没有效果

    如果你把绘制代码写在了 super.onDrawForeground() 的下面,绘制代码会在滑动边缘渐变、滑动条和前景之后被执行,那么绘制内容将会盖住滑动边缘渐变、滑动条和前景。

    如果你把绘制代码写在了 super.onDrawForeground() 的上面,绘制内容就会在 dispatchDraw()super.onDrawForeground() 之间执行,那么绘制内容会盖住子 View,但被滑动边缘渐变、滑动条以及前景盖住:

    无法在滑动边缘渐变、滑动条前景之间插入绘制代码

  6. draw() 总调度方法

    draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方法里。背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。

    // View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦):
    
    public void draw(Canvas canvas) {
       ...
    
       drawBackground(Canvas); // 绘制背景(不能重写)
       onDraw(Canvas); // 绘制主体
       dispatchDraw(Canvas); // 绘制子 View
       onDrawForeground(Canvas); // 绘制滑动相关和前景
    
       ...
    }
    
    img

    如果把绘制代码写在 super.draw() 的下面,那么这段代码会在其他所有绘制完成之后再执行,也就是说,它的绘制内容会盖住其他的所有绘制内容。

    如果把绘制代码写在 super.draw() 的上面,那么这段代码会在其他所有绘制之前被执行,所以这部分绘制内容会被其他所有的内容盖住,包括背景。是的,背景也会盖住它。

  1. ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false)
  2. 在重写的方法有多个选择时,优先选择 onDraw()。因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章