Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 自定義View-繪制居中文本

Android 自定義View-繪制居中文本

編輯:關於Android編程

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

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

好吧,那我試一下。

1.自定義控件基本步驟

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

OK,簡單,直接干起來。

1. 自定義View的屬性

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




    
    
    

    
    
        
        
        
    

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文件調用方式



    

    

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

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

這裡寫圖片描述

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

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

2.繪制水平,垂直居中文本

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

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

2.1寬度偏小

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

value = mBound.width();

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

value = mPaint.measureText(mText);

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

這裡寫圖片描述<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxjb2RlIGNsYXNzPQ=="hljs xml">上圖中,第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)

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved