Canvas的那些事,图解Android中的绘制技巧

Canvas的那些事,图解Android中的绘制技巧



今日快讯



北京时间2月7日凌晨4时45分,太空探索公司SpaceX试射的猎鹰重型火箭已成功发射升空,将把一辆特斯拉跑车送入绕太阳飞行的轨道。该火箭拥有3个推进器和27个引擎,能够提供超过200万公斤的推力,是世界现役最强大的运载火箭。



作者简介



今天这篇文章是过年前的倒数二篇文章,很快就要和大家短暂地说再见了,也希望每一位朋友都能坚守好最后几天的岗位。

本篇来自 

容华谢后

的投稿,分享了Android中Canvas调用drawText方法的原理,希望大家会喜欢!

容华谢后

 

的博客地址:

http://www.jianshu.com/u/34ece31cd6eb



正文



写在前面

在实现自定义控件的过程中,常常会有绘制居中文字的需求,于是在网上搜了一些相关的博客,总是看的一脸懵逼,就想着自己分析一下,在此记录下来,希望对大家能够有所帮助。

绘制一段文本

首先把坐标原点移动到控件中心(默认坐标原点在屏幕左上角),这样看起来比较直观一些,然后绘制x、y轴,此时原点向上y为负,向下y为正,向左x为负,向右x为正,以(0,0)坐标开始绘制一段文本:

@Override



public

void

draw

(Canvas canvas)

{

   

super

.draw(canvas);

   

// 将坐标原点移到控件中心



   canvas.translate(getWidth() /

2

, getHeight() /

2

);

   

// x轴



   canvas.drawLine(-getWidth() /

2

,

0

, getWidth() /

2

,

0

, paint);

   

// y轴



   canvas.drawLine(

0

, -getHeight() /

2

,

0

, getHeight() /

2

, paint);



   

// 绘制文字



   paint.setTextSize(sp2px(

50

));

   canvas.drawText(

"YangLe"

,

0

,

0

, paint);

}

看下绘制的文本:

Canvas的那些事,图解Android中的绘制技巧

咦,为什么绘制的文本在第一象限,y坐标不是指定的0吗,为什么文本没有在x轴的上面或下面,而是穿过了x轴,带着这些疑问继续往下看:

首先看一个重要的类:

public

static

class

FontMetrics

{

   

/**

    * The maximum distance above the baseline for the tallest glyph in

    * the font at a given text size.

    */



   

public

float

  top;

   

/**

    * The recommended distance above the baseline for singled spaced text.

    */



   

public

float

  ascent;

   

/**

    * The recommended distance below the baseline for singled spaced text.

    */



   

public

float

  descent;

   

/**

    * The maximum distance below the baseline for the lowest glyph in

    * the font at a given text size.

    */



   

public

float

  bottom;

   

/**

    * The recommended additional space to add between lines of text.

    */



   

public

float

  leading;

}

FontMetrics类是Paint的一个内部类,主要定义了绘制文本时的一些关键坐标位置,看下这些值都代表什么:

Canvas的那些事,图解Android中的绘制技巧

看图说话:

  • top:从基线(x轴)向上绘制区域的最高点,此值为负值

  • ascent:单行文本,从基线(x轴)向上绘制的推荐最高点,此值为负值

  • baseline:基线,此值为0

  • descent:单行文本,从基线(x轴)向下绘制的推荐最低点,此值为正值

  • bottom:从基线(x轴)向下绘制区域的最低点,此值为正值

  • leading:推荐的额外行距,一般为0

  • 下面再来看看drawText这个方法:

    /**

    * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted

    * based on the Align setting in the paint.

    *

    *

    @param

    text The text to be drawn

    *

    @param

    x The x-coordinate of the origin of the text being drawn

    *

    @param

    y The y-coordinate of the baseline of the text being drawn

    *

    @param

    paint The paint used for the text (e.g. color, size, style)

    */



    public

    void

    drawText

    (@NonNull String text,

    float

    x,

    float

    y, @NonNull Paint paint)

    {

       

    super

    .drawText(text, x, y, paint);

    }

    重点看下x、y参数的含义:

  • x:绘制文本的起始x坐标

  • y:绘制文本的baseline在y轴方向的位置

  • 有点难理解,举个栗子,上文中的x、y参数传的是(0,0),此时的baseline正好是坐标系中x轴,就相当于从y轴开始向右绘制,以x轴作为文本的baseline进行绘制。

    如果参数传(0,10),此时绘制文本的baseline从x轴开始向下移动10px,也就是以y10作为文本的baseline进行绘制,y10就是绘制文本的baseline在y轴方向的位置。

    注意:baseline是绘制文本的基线,相对于绘制文本区域来说,相当于x轴,向上为负(top、ascent),向下为正(descent、bottom),但是这个x轴并不是控件的x轴,切记切记!!!

    还记得我们在上文中提出的疑问吗,这下可以解释了:

  • 为什么绘制的文本在第一象限?

  • 因为我们把坐标原点移到了控件中心,文本的baseline正好为x轴,top、ascent值为负,所以绘制的文本在第一象限。

  • y坐标不是指定的0吗,为什么文本没有在x轴的上面或下面,而是穿过了x轴?

  • drawText方法默认x轴方向是从左到右绘制的,y轴方向是从baseline为基准绘制的,文中的baseline正好为x轴,以baseline为基准绘制文本向下还有一段距离,所以文本穿过了x轴。

    绘制居中的文本

    在上文中,我们学习了如何绘制一段文本,以及其中参数和坐标的含义,接下来进入正题,看下如何才能绘制居中的文本。



    首先看一张图,此时文本的baseline正好为x轴,如果想要文本居中显示的话,就需要先计算文本的宽度和高度:

  • 宽度:调用Paint的measureText方法就可以获得文本的宽度

  • 高度:文本的高度就是实际绘制区域的高度,可以用(fontMetrics.descent - fontMetrics.ascent)获取,因为ascent为负数,所以最终算出来的是两者的和

  • 现在有了宽度,把绘制文本的x坐标向左移动(宽度 / 2)就可以水平居中,但是垂直方向就不能这么干了,我们要将文本向下移动baseline到文本中心的距离,也就是(高度 / 2 - fontMetrics.descent),如下图所示:

    Canvas的那些事,图解Android中的绘制技巧

    现在的公式为:

    float

    baseLineY

    = (fontMetrics.descent - fontMetrics.ascent) /

    2

    - fontMetrics.descent;

    = -fontMetrics.ascent /

    2

    - fontMetrics.descent /

    2

    ;

    = -(fontMetrics.ascent + fontMetrics.descent) /

    2

    ;

    = Math.abs(fontMetrics.ascent + fontMetrics.descent) /

    2

    ;

    Paint中也有获取ascent和descent值的方法,所以公式最终为:

    float

    baseLineY = Math.abs(paint.ascent() + paint.descent()) /

    2

    ;

    注意:此公式是相对于坐标原点在控件中心来计算的,如果坐标原点在左上角,baseLineY需要加上控件高度的一半。

    float

    baseLineY = height /

    2

    + Math.abs(paint.ascent() + paint.descent()) /

    2

    ;

    看下代码:

    @Override



    public

    void

    draw

    (Canvas canvas)

    {

       

    super

    .draw(canvas);

       

    // 将坐标原点移到控件中心



       canvas.translate(getWidth() /

    2

    , getHeight() /

    2

    );

       

    // x轴



       canvas.drawLine(-getWidth() /

    2

    ,

    0

    , getWidth() /

    2

    ,

    0

    , paint);

       

    // y轴



       canvas.drawLine(

    0

    , -getHeight() /

    2

    ,

    0

    , getHeight() /

    2

    , paint);



       

    // 绘制居中文字



       paint.setTextSize(sp2px(

    50

    ));

       paint.setColor(Color.GRAY);

       

    // 文字宽



       

    float

    textWidth = paint.measureText(

    "YangLe"Blog"

    );

       

    // 文字baseline在y轴方向的位置



       

    float

    baseLineY = Math.abs(paint.ascent() + paint.descent()) /

    2

    ;

       canvas.drawText(

    "YangLe"Blog"

    , -textWidth /

    2

    , baseLineY, paint);

    }

    看下居中了吗:

    Canvas的那些事,图解Android中的绘制技巧

    大功告成!

    绘制多行居中的文本

    注意:drawText方法不支持绘制多行文本

  • 方式一

  • 使用支持自动换行的StaticLayout:

    /**

    * 绘制多行居中文本(方式1)

    *

    *

    @param

    canvas 画布

    */



    private

    void

    drawCenterMultiText1

    (Canvas canvas)

    {

       String text =

    "ABC"

    ;



       

    // 画笔



       TextPaint textPaint =

    new

    TextPaint();

       textPaint.setAntiAlias(

    true

    );

       textPaint.setColor(Color.GRAY);



       

    // 设置宽度超过50dp时换行



       StaticLayout staticLayout =

    new

    StaticLayout(text, textPaint, dp2px(

    50

    ),

               Layout.Alignment.ALIGN_CENTER,

    1f

    ,

    0f

    ,

    false

    );

       canvas.save();

       

    // StaticLayout默认从(0,0)点开始绘制



       

    // 如果需要调整位置,只能在绘制之前移动Canvas的起始坐标



       canvas.translate(-staticLayout.getWidth() /

    2

    , -staticLayout.getHeight() /

    2

    );

       staticLayout.draw(canvas);

       canvas.restore();

    }

    看下StaticLayout的构造方法参数含义:

    public

    StaticLayout

    (CharSequence source, TextPaint paint,

    int

    width, Alignment align,

                       

    float

    spacingmult,

    float

    spacingadd,

    boolean

    includepad)

    {

       

    this

    (source,

    0

    , source.length(), paint, width, align, spacingmult, spacingadd, includepad);

    }

  • source:需要分行的文本

  • paint:画笔对象

  • width:layout的宽度,文本超出宽度时自动换行

  • align:layout的对其方式

  • spacingmult:相对行间距,相对字体大小,1f表示行间距为1倍的字体高度

  • spacingadd:基础行距偏移值,实际行间距等于(spacingmult + spacingadd)

  • includepad:参数未知

  • 看下效果:

    Canvas的那些事,图解Android中的绘制技巧

    使用StaticLayout,每行设置的宽度是相同的,当需求为每行显示不同长度的文本时,这种方式就不能使用了,别担心,接着来看下第二种方式。

  • 方式二

  • 使用循环drawText的方式进行绘制,看图说话:

    Canvas的那些事,图解Android中的绘制技巧

    现在需要绘制A、B、C三行文本,红色A代表每行文本默认的绘制位置,绿色的线代表每行文本的baseline,x轴为红色A的baseline,现在分为三种情况:

  • 文本在x轴上方:红色A的baseline向上移动a距离,总高度的/2 - 文本的top值(绝对值)

  • 文本在x轴中间:红色A的baseline向下移动b距离,计算公式请参考单行文本居中公式

  • 文本在x轴下方:红色A的baseline向下移动c距离,总高度的/2 - 文本的bottom值(绝对值)

  • 看下代码:

    /**

    * 绘制多行居中文本(方式2)

    *

    *

    @param

    canvas 画布

    */



    private

    void

    drawCenterMultiText2

    (Canvas canvas)

    {

       String[] texts = {

    "A"

    ,

    "B"

    ,

    "C"

    };



       Paint.FontMetrics fontMetrics = paint.getFontMetrics();

       

    // top绝对值



       

    float

    top = Math.abs(fontMetrics.top);

       

    // ascent绝对值



       

    float

    ascent = Math.abs(fontMetrics.ascent);

       

    // descent,正值



       

    float

    descent = fontMetrics.descent;

       

    // bottom,正值



       

    float

    bottom = fontMetrics.bottom;

       

    // 行数



       

    int

    textLines = texts.length;

       

    // 文本高度



       

    float

    textHeight = top + bottom;

       

    // 文本总高度



       

    float

    textTotalHeight = textHeight * textLines;

       

    // 基数



       

    float

    basePosition = (textLines -

    1

    ) /

    2f

    ;



       

    for

    (

    int

    i =

    0

    ; i < textLines; i++) {

           

    // 文本宽度



           

    float

    textWidth = paint.measureText(texts[i]);

           

    // 文本baseline在y轴方向的位置



           

    float

    baselineY;



           

    if

    (i < basePosition) {

               

    // x轴上,值为负



               

    // 总高度的/2 - 已绘制的文本高度 - 文本的top值(绝对值)



               baselineY = -(textTotalHeight /

    2

    - textHeight * i - top);



           }

    else

    if

    (i > basePosition) {

               

    // x轴下,值为正



               

    // 总高度的/2 - 未绘制的文本高度 - 文本的bottom值(绝对值)



               baselineY = textTotalHeight /

    2

    - textHeight * (textLines - i -

    1

    ) - bottom;



           }

    else

    {

               

    // x轴中,值为正



               

    // 计算公式请参考单行文本居中公式



               baselineY = (ascent - descent) /

    2

    ;

           }



           canvas.drawText(texts[i], -textWidth /

    2

    , baselineY, paint);

       }

    }

    对照上图再看代码就很好理解了,觉得代码中的公式还有可以优化的地方,如果你有好的方法,可以留言告诉我哈。

    再看下中文版的多行文本:

    Canvas的那些事,图解Android中的绘制技巧

    TextAlign

    Paint的TextAlign属性决定了绘制文本相对于drawText方法中x参数的相对位置。

    举个栗子:

  • Paint.Align.LEFT:默认属性,x坐标为绘制文本的最左侧坐标

  • Paint.Align.CENTER:x坐标为绘制文本的水平中心坐标

  • Paint.Align.RIGHT:x坐标为绘制文本的最右侧坐标

  • 看图理解下:

    Canvas的那些事,图解Android中的绘制技巧

    文本居中的公式

    坐标原点在控件中心:

    float

    baseLineY = Math.abs(paint.ascent() + paint.descent()) /

    2

    ;

    坐标原点在控件左上角:

    float

    baseLineY = height /

    2

    + Math.abs(paint.ascent() + paint.descent()) /

    2

    ;



    结语



    源码已经上传到GitHub上了,欢迎Fork,觉得还不错就Start一下吧!

    项目地址:

    http://github.com/alidili/Demos/tree/master/DrawTextDemo

    如果有什么问题欢迎留言哈,谢谢!



    欢迎长按下图 -> 识别图中二维码

    或者 扫一扫 关注我的公众号

    Canvas的那些事,图解Android中的绘制技巧

    Canvas的那些事,图解Android中的绘制技巧