1. 程式人生 > >Android 自定義View-怎麼繪製居中文字?

Android 自定義View-怎麼繪製居中文字?

接觸過自定義控制元件的開發者一看,笑了,立馬關了網頁。但是…你真的知道怎麼繪製居中文字嗎?

我不會?開玩笑,不就是:X=控制元件寬度/2 - 文字寬度/2;Y=控制元件高度/2 + 文字寬度/2

好吧,那我試一下。

1.自定義控制元件基本步驟

  1. 自定義View的屬性
  2. 在View的構造方法中獲得我們自定義的屬性
  3. #重寫onMesure #
  4. 重寫onDraw

OK,簡單,直接幹起來。

1. 自定義View的屬性

按照最簡單的來,屬性有:文字,文字顏色,文字大小。
我們在 /value/attrs.xml 中這麼寫:

<?xml version="1.0" encoding="utf-8"?>
<resources> <attr name="text" format="string" /> <attr name="textColor" format="color" /> <attr name="textSize" format="dimension" /> <!-- RTextView --> <declare-styleable name="RTextView"> <attr name="text" /> <attr name
="textColor" />
<attr name="textSize" /> </declare-styleable> </resources>

2. 在View的構造方法中獲得我們自定義的屬性

    /**
     * 基本屬性
     */
    private String mText = "Loading";
    private int mTextColor;
    private int mTextSize;

    /**
     * 畫筆,文字繪製範圍
     */
    private Rect mBound;
    private
Paint mPaint; public RTextView(Context context) { this(context, null); } public RTextView(Context context, AttributeSet attrs) { super(context, attrs); /* * 獲取基本屬性 */ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RTextView); mText = a.getString(R.styleable.RTextView_text); mTextSize = a.getDimensionPixelSize(R.styleable.RTextView_textSize, 20); mTextColor = a.getColor(R.styleable.RTextView_textColor, Color.BLACK); a.recycle(); /* * 初始化畫筆 */ mBound = new Rect(); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Style.FILL); mPaint.setTextSize(mTextSize); mPaint.getTextBounds(mText, 0, mText.length(), mBound); }

程式碼超級簡單,就是在構造方法中獲取自定義的屬性。

3. #重寫onMesure

誒,這個有點不一樣哦。簡單說一下吧。我們在使用控制元件的時候一般會設定寬高。
設定型別有:wrap_contentmatch_parent100dp(明確值)

自定義控制元件時,
如果設定了 明確的寬高(100dp),系統幫我們測量的結果就是我們設定的實際值;
如果是 wrap_content 或者 match_parent 系統幫我們測量的結果就是 match_parent。
所以當設定為 wrap_content 的時候我們需要 重寫onMesure 方法重新測量。

重寫之前瞭解 MeasureSpec 的 specMode,一共分為三種類型:
EXACTLY:一般表示設定了 明確值,或者 match_parent
AT_MOST:表示子控制元件限制在一個最大值內,一般為 wrap_content
UNSPECIFIED:表示子控制元件像多大就多大,很少使用

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = onMeasureR(0, widthMeasureSpec);
        int height = onMeasureR(1, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }
    /**
     * 計算控制元件寬高
     * 
     * @param attr屬性
     *            [0寬,1高]
     * @param oldMeasure
     * @author Ruffian
     */
    public int onMeasureR(int attr, int oldMeasure) {

        int newSize = 0;
        int mode = MeasureSpec.getMode(oldMeasure);
        int oldSize = MeasureSpec.getSize(oldMeasure);

        switch (mode) {
        case MeasureSpec.EXACTLY:
            newSize = oldSize;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.UNSPECIFIED:

            float value;

            if (attr == 0) {

                value = mBound.width();
                // value = mPaint.measureText(mText);

                // 控制元件的寬度  + getPaddingLeft() +  getPaddingRight()
                newSize = (int) (getPaddingLeft() + value + getPaddingRight());

            } else if (attr == 1) {

                value = mBound.height();
                // FontMetrics fontMetrics = mPaint.getFontMetrics();
                // value = Math.abs((fontMetrics.descent - fontMetrics.ascent));

                // 控制元件的高度  + getPaddingTop() +  getPaddingBottom()
                newSize = (int) (getPaddingTop() + value + getPaddingBottom());

            }

            break;
        }

        return newSize;
    }

方法很簡單,獲取寬高的模式,如果是明確值,或者match_parent,直接獲取原始值返回。
如果是 wrap_content,計算寬高:控制元件的寬高 + 左右(上下)內邊距

4. 重寫onDraw

好了關鍵的時候來了,繪製文字。
根據文章開頭那些老鳥的方法:X=控制元件寬度/2 - 文字寬度/2;Y=控制元件高度/2 + 文字寬度/2

    @Override
    protected void onDraw(Canvas canvas) {

        mPaint.setColor(mTextColor);

        /*
         * 控制元件寬度/2 - 文字寬度/2
         */
        float startX = getWidth() / 2 - mBound.width() / 2;

        /*
         * 控制元件高度/2 + 文字高度/2,繪製文字從文字左下角開始,因此"+"
         */
        float startY = getHeight() / 2 + mBound.height() / 2;

        // 繪製文字
        canvas.drawText(mText, startX, startY, mPaint);

        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);
        // 中線,做對比
        canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
    }

xml檔案呼叫方式

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="2dp"
        android:layout_marginTop="2dp"
        android:background="#ADD597"
        android:text="@string/text"
        android:textSize="25sp" />

    <cn.r.android.view.RTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="2dp"
        android:layout_marginTop="2dp"
        android:background="#ADD597"
        custom:text="@string/text"
        custom:textColor="#000000"
        custom:textSize="25sp" />

</LinearLayout>

注意:這裡寬高設定為wrap_content,並且沒有padding

好了,根據那些老鳥的方法寫出來了,那麼執行一下看看結果。
為了更好的檢視效果,加上原生TextView做對比

這裡寫圖片描述

很明顯可以看出自定義的寬度小了,高度也不夠,寬高文字都不能完整的繪製。

獲取很多人看到這個會覺得奇怪,以前沒有發現這種效果,因為這裡寬高設定為wrap_content,並且沒有padding,如果設定了padding或許很難看出這些細微的效果,因此很多開發者以為這就是滿意的效果了。

2.繪製水平,垂直居中文字

之前我也以為繪製文字嘛,再簡單不過的啦,深入研究一下才發現,哎喲,有文章哦。

OK,說一下解決思路吧。上圖所示,寬高都出現了問題,都偏小了。這裡寬度問題比較容易解決,高度才比較麻煩。

2.1寬度偏小

寬度偏小是因為文字測量出現了誤差,
原始方式,這是一種粗略的文字寬度計算

value = mBound.width();

改進,這是比較精確的測量文字寬度的方式

value = mPaint.measureText(mText);

開發者可以自行列印對比一下 mBound.width(); 和 mPaint.measureText(mText); 的值。

這裡寫圖片描述

上圖中,第1個是原生TextView,第2個是修改的過的,第三個是沒有修改的,明顯看到寬度已經和原生一樣,而且最後一個文字也完整繪製出來了。第三個可以對比

2.2高度偏小

高度偏小就比較麻煩了。不是一行程式碼可以解決的了
先了解一下Android是怎麼樣繪製文字的,這裡涉及到幾個概念,分別是文字的top,bottom,ascent,descent,baseline。
看下面的圖(摘自網路):

這裡寫圖片描述

解釋一下這張圖片。(摘自網路)
Baseline是基線,在Android中,文字的繪製都是從Baseline處開始的,Baseline往上至字元“最高處”的距離我們稱之為ascent(上坡度),Baseline往下至字元“最低處”的距離我們稱之為descent(下坡度);

 leading(行間距)則表示上一行字元的descent到該行字元的ascent之間的距離;
 
 top和bottom文件描述地很模糊,其實這裡我們可以借鑑一下TextView對文字的繪製,TextView在繪製文字的時候總會在文字的最外層留出一些內邊距,為什麼要這樣做?因為TextView在繪製文字的時候考慮到了類似讀音符號,下圖中的A上面的符號就是一個拉丁文的類似讀音符號的東西:

這裡寫圖片描述

Baseline是基線,Baseline以上是負值,以下是正值,因此 ascent,top是負值, descent和bottom是正值。
OK,知道了這幾個概念之後就開始想想要怎麼修改了。

我們先修改高度偏小的問題
原始程式碼,

value = mBound.height();

修改後程式碼

FontMetrics fontMetrics = mPaint.getFontMetrics();
value = Math.abs((fontMetrics.bottom - fontMetrics.top));

結合圖一,bottom和top相減的絕對值就是view的高度height。注意:Baseline以上是負值,以下是正值

這裡寫圖片描述

OK,高度和寬度大小和原生的大小一樣了,那麼現在怎麼使得文字垂直居中呢?

查閱了網上資料和測試了多次的結果得出一個計算 Y 值的計算公式:

FontMetricsInt fm = mPaint.getFontMetricsInt();

int startY = getHeight() / 2 - fm.descent + (fm.bottom - fm.top) / 2;

int startY = getHeight() / 2 - fm.descent + (fm.descent - fm.ascent)/ 2;

getHeight():控制元件的高度

getHeight()/2-fm.descent:意思是將整個文字區域擡高至控制元件的1/2

+ (fm.bottom - fm.top) / 2:(fm.bottom - fm.top)其實就是文字的高度,意思就是將文字下沉文字高度的一半

  • 執行:getHeight()/2-fm.descent , 將整個文字區域擡高至控制元件的1/2

這裡寫圖片描述

  • 執行: + (fm.bottom - fm.top) / 2 , 將文字下沉文字高度的一半

這裡寫圖片描述

為什麼是:(fm.bottom - fm.top) ;而不是:(fm.descent - fm.ascent)

這裡寫圖片描述

第一張是原生TextView,第二張是(fm.bottom - fm.top),第三張是(fm.descent - fm.ascent)。

從效果圖看,第三種才是真正意義上的居中,不是嗎?但是第二種是和原生TextView最接近的,為什麼呢?經過測試你會知道,如果單純是漢字或者數字第三種的效果或者會比較好,但是如果其他的語言,就比如上圖的英文來看,第二種是比較好的。不能排除其他國家的語言,或者一些帶音標的拼音之類的呢?

所以根據實際需求來確定使用哪一個,推薦第二種:(fm.bottom - fm.top)

原始碼下載