Android 圖解Canvas drawText文字居中的那些事

封面

本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布

GitHub傳送門

1.寫在前面

在實(shí)現(xiàn)自定義控件的過程中颠区,常常會有繪制居中文字的需求,于是在網(wǎng)上搜了一些相關(guān)的博客媒熊,總是看的一臉懵逼,就想著自己分析一下痊剖,在此記錄下來晃洒,希望對大家能夠有所幫助。

2.繪制一段文本

首先把坐標(biāo)原點(diǎn)移動(dòng)到控件中心(默認(rèn)坐標(biāo)原點(diǎn)在屏幕左上角)唱歧,這樣看起來比較直觀一些宪摧,然后繪制x、y軸迈喉,此時(shí)原點(diǎn)向上y為負(fù),向下y為正温圆,向左x為負(fù)挨摸,向右x為正,以(0,0)坐標(biāo)開始繪制一段文本:

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    // 將坐標(biāo)原點(diǎn)移到控件中心
    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);
}

看下繪制的文本:

繪制文本

咦岁歉,為什么繪制的文本在第一象限得运,y坐標(biāo)不是指定的0嗎,為什么文本沒有在x軸的上面或下面锅移,而是穿過了x軸熔掺,帶著這些疑問繼續(xù)往下看:

首先看一個(gè)重要的類:

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的一個(gè)內(nèi)部類,主要定義了繪制文本時(shí)的一些關(guān)鍵坐標(biāo)位置非剃,看下這些值都代表什么:

關(guān)鍵坐標(biāo)

看圖說話:

  • top:從基線(x軸)向上繪制區(qū)域的最高點(diǎn)置逻,此值為負(fù)值

  • ascent:單行文本,從基線(x軸)向上繪制的推薦最高點(diǎn)备绽,此值為負(fù)值

  • baseline:基線券坞,此值為0

  • descent:單行文本,從基線(x軸)向下繪制的推薦最低點(diǎn)肺素,此值為正值

  • bottom:從基線(x軸)向下繪制區(qū)域的最低點(diǎn)恨锚,此值為正值

  • leading:推薦的額外行距,一般為0

下面再來看看drawText這個(gè)方法:

/**
 * 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);
}

重點(diǎn)看下x倍靡、y參數(shù)的含義:

  • x:繪制文本的起始x坐標(biāo)

  • y:繪制文本的baseline在y軸方向的位置

有點(diǎn)難理解猴伶,舉個(gè)栗子,上文中的x、y參數(shù)傳的是(0他挎,0)筝尾,此時(shí)的baseline正好是坐標(biāo)系中x軸,就相當(dāng)于從y軸開始向右繪制雇盖,以x軸作為文本的baseline進(jìn)行繪制忿等。

如果參數(shù)傳(0,10)崔挖,此時(shí)繪制文本的baseline從x軸開始向下移動(dòng)10px贸街,也就是以y10作為文本的baseline進(jìn)行繪制,y10就是繪制文本的baseline在y軸方向的位置狸相。

注意:baseline是繪制文本的基線薛匪,相對于繪制文本區(qū)域來說,相當(dāng)于x軸脓鹃,向上為負(fù)(top逸尖、ascent),向下為正(descent瘸右、bottom)娇跟,但是這個(gè)x軸并不是控件的x軸,切記切記L0!

還記得我們在上文中提出的疑問嗎龄章,這下可以解釋了:

  • 為什么繪制的文本在第一象限吃谣?

    因?yàn)槲覀儼炎鴺?biāo)原點(diǎn)移到了控件中心,文本的baseline正好為x軸做裙,top岗憋、ascent值為負(fù),所以繪制的文本在第一象限锚贱。

  • y坐標(biāo)不是指定的0嗎仔戈,為什么文本沒有在x軸的上面或下面,而是穿過了x軸拧廊?

    drawText方法默認(rèn)x軸方向是從左到右繪制的杂穷,y軸方向是從baseline為基準(zhǔn)繪制的,文中的baseline正好為x軸卦绣,以baseline為基準(zhǔn)繪制文本向下還有一段距離耐量,所以文本穿過了x軸。

3.繪制居中的文本

在上文中滤港,我們學(xué)習(xí)了如何繪制一段文本廊蜒,以及其中參數(shù)和坐標(biāo)的含義趴拧,接下來進(jìn)入正題,看下如何才能繪制居中的文本山叮。

首先看一張圖著榴,此時(shí)文本的baseline正好為x軸,如果想要文本居中顯示的話屁倔,就需要先計(jì)算文本的寬度和高度:

  • 寬度:調(diào)用Paint的measureText方法就可以獲得文本的寬度

  • 高度:文本的高度就是實(shí)際繪制區(qū)域的高度脑又,可以用(fontMetrics.descent - fontMetrics.ascent)獲取,因?yàn)閍scent為負(fù)數(shù)锐借,所以最終算出來的是兩者的和

現(xiàn)在有了寬度问麸,把繪制文本的x坐標(biāo)向左移動(dòng)(寬度 / 2)就可以水平居中,但是垂直方向就不能這么干了钞翔,我們要將文本向下移動(dòng)baseline到文本中心的距離严卖,也就是(高度 / 2 - fontMetrics.descent),如下圖所示:

計(jì)算baseLineY

現(xiàn)在的公式為:

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;

注意:此公式是相對于坐標(biāo)原點(diǎn)在控件中心來計(jì)算的哮笆,如果坐標(biāo)原點(diǎn)在左上角,baseLineY需要加上控件高度的一半汰扭。

float baseLineY = height / 2 + Math.abs(paint.ascent() + paint.descent()) / 2;

看下代碼:

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    // 將坐標(biāo)原點(diǎn)移到控件中心
    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);
}

看下居中了嗎:

繪制居中文本

大功告成稠肘!

4.繪制多行居中的文本

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

4.1 方式一

使用支持自動(dòng)換行的StaticLayout:

/**
 * 繪制多行居中文本(方式1)
 *
 * @param canvas 畫布
 */
private void drawCenterMultiText1(Canvas canvas) {
    String text = "ABC";

    // 畫筆
    TextPaint textPaint = new TextPaint();
    textPaint.setAntiAlias(true);
    textPaint.setColor(Color.GRAY);

    // 設(shè)置寬度超過50dp時(shí)換行
    StaticLayout staticLayout = new StaticLayout(text, textPaint, dp2px(50),
            Layout.Alignment.ALIGN_CENTER, 1f, 0f, false);
    canvas.save();
    // StaticLayout默認(rèn)從(0,0)點(diǎn)開始繪制
    // 如果需要調(diào)整位置萝毛,只能在繪制之前移動(dòng)Canvas的起始坐標(biāo)
    canvas.translate(-staticLayout.getWidth() / 2, -staticLayout.getHeight() / 2);
    staticLayout.draw(canvas);
    canvas.restore();
}

看下StaticLayout的構(gòu)造方法參數(shù)含義:

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的寬度项阴,文本超出寬度時(shí)自動(dòng)換行

  • align:layout的對其方式

  • spacingmult:相對行間距,相對字體大小珊泳,1f表示行間距為1倍的字體高度

  • spacingadd:基礎(chǔ)行距偏移值鲁冯,實(shí)際行間距等于(spacingmult + spacingadd)

  • includepad:參數(shù)未知

看下效果:

StaticLayout

使用StaticLayout拷沸,每行設(shè)置的寬度是相同的色查,當(dāng)需求為每行顯示不同長度的文本時(shí),這種方式就不能使用了撞芍,別擔(dān)心秧了,接著來看下第二種方式。

4.2 方式二

使用循環(huán)drawText的方式進(jìn)行繪制序无,看圖說話:

計(jì)算baseLineY

現(xiàn)在需要繪制A验毡、B、C三行文本帝嗡,紅色A代表每行文本默認(rèn)的繪制位置晶通,綠色的線代表每行文本的baseline,x軸為紅色A的baseline哟玷,現(xiàn)在分為三種情況:

  • 文本在x軸上方:紅色A的baseline向上移動(dòng)a距離狮辽,總高度的/2 - 文本的top值(絕對值)

  • 文本在x軸中間:紅色A的baseline向下移動(dòng)b距離一也,計(jì)算公式請參考單行文本居中公式

  • 文本在x軸下方:紅色A的baseline向下移動(dòng)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;
    // 行數(shù)
    int textLines = texts.length;
    // 文本高度
    float textHeight = top + bottom;
    // 文本總高度
    float textTotalHeight = textHeight * textLines;
    // 基數(shù)
    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軸上,值為負(fù)
            // 總高度的/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軸中舆蝴,值為正
            // 計(jì)算公式請參考單行文本居中公式
            baselineY = (ascent - descent) / 2;
        }

        canvas.drawText(texts[i], -textWidth / 2, baselineY, paint);
    }
}

對照上圖再看代碼就很好理解了,覺得代碼中的公式還有可以優(yōu)化的地方题诵,如果你有好的方法洁仗,可以留言告訴我哈。

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

多行居中文本

5.TextAlign

Paint的TextAlign屬性決定了繪制文本相對于drawText方法中x參數(shù)的相對位置仇轻。

舉個(gè)栗子:

  • Paint.Align.LEFT:默認(rèn)屬性京痢,x坐標(biāo)為繪制文本的最左側(cè)坐標(biāo)

  • Paint.Align.CENTER:x坐標(biāo)為繪制文本的水平中心坐標(biāo)

  • Paint.Align.RIGHT:x坐標(biāo)為繪制文本的最右側(cè)坐標(biāo)

看圖理解下:

Paint.Align.LEFT
Paint.Align.CENTER
Paint.Align.RIGHT

6.文本居中的公式

坐標(biāo)原點(diǎn)在控件中心:

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

坐標(biāo)原點(diǎn)在控件左上角:

float baseLineY = height / 2 + Math.abs(paint.ascent() + paint.descent()) / 2;

7.寫在最后

源碼已經(jīng)上傳到GitHub上了,歡迎Fork篷店,覺得還不錯(cuò)就Start一下吧祭椰!

GitHub傳送門

點(diǎn)我下載本文Demo的Apk

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市疲陕,隨后出現(xiàn)的幾起案子方淤,更是在濱河造成了極大的恐慌,老刑警劉巖蹄殃,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件携茂,死亡現(xiàn)場離奇詭異,居然都是意外死亡诅岩,警方通過查閱死者的電腦和手機(jī)讳苦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吩谦,“玉大人鸳谜,你說我怎么就攤上這事∈酵ⅲ” “怎么了咐扭?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長滑废。 經(jīng)常有香客問我蝗肪,道長,這世上最難降的妖魔是什么蠕趁? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任薛闪,我火速辦了婚禮,結(jié)果婚禮上俺陋,老公的妹妹穿的比我還像新娘豁延。我一直安慰自己怀各,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布术浪。 她就那樣靜靜地躺著瓢对,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胰苏。 梳的紋絲不亂的頭發(fā)上硕蛹,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音硕并,去河邊找鬼法焰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛倔毙,可吹牛的內(nèi)容都是我干的埃仪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼陕赃,長吁一口氣:“原來是場噩夢啊……” “哼卵蛉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起么库,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤傻丝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后诉儒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葡缰,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年忱反,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泛释。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡温算,死狀恐怖怜校,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情米者,我是刑警寧澤韭畸,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布宇智,位于F島的核電站蔓搞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏随橘。R本人自食惡果不足惜喂分,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一梅肤、第九天 我趴在偏房一處隱蔽的房頂上張望夹抗。 院中可真熱鬧,春花似錦峭范、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至酸钦,卻和暖如春怪得,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卑硫。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工徒恋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人欢伏。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓入挣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親硝拧。 傳聞我的和親對象是個(gè)殘疾皇子径筏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容