1. 程式人生 > 實用技巧 >Android開發——自定義view之文字繪製

Android開發——自定義view之文字繪製

首先新建檔案MyTextView,繼承AppCompatTextView,並重寫onDraw方法:

public class MyTextView extends AppCompatTextView {
    /**
     * 需要繪製的文字
     */
    private String mText;
    /**
     * 文字的顏色
     */
    private int mTextColor;
    /**
     * 文字的大小
     */
    private int mTextSize;

    private Paint mPaint;

    public
MyTextView(Context context) { super(context); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); } public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override
protected void onDraw(Canvas canvas) { super.onDraw(canvas); } }

先處理自定義的屬性,使我們在佈局時可以隨意更改該view的文字內容、顏色、大小

在res/values/下建立一個名為attrs.xml的檔案,然後定義如下屬性:
format的意思是該屬性的取值是什麼型別(支援的型別有string,color,demension,integer,enum,reference,float,boolean,fraction,flag)

<?xml version="1.0" encoding="utf-8"
?> <resources> <declare-styleable name="MyTextView"> <attr name="mText" format="string" /> <attr name="mTextColor" format="color" /> <attr name="mTextSize" format="dimension" /> </declare-styleable> </resources>

在佈局檔案中引入我們的名稱空間xmlns:lfm="http://schemas.android.com/apk/res-auto"

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:lfm="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.lfm.view.MyTextView
        android:id="@+id/my_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        lfm:mText="金大人的夢"
        lfm:mTextColor="#000000"
        lfm:mTextSize="30sp" />

</LinearLayout>

在構造方法中獲取自定義屬性的值:

public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //獲取自定義屬性的值
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
        mText = a.getString(R.styleable.MyTextView_mText);
        mTextColor = a.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
        mTextSize = (int) a.getDimension(R.styleable.MyTextView_mTextSize, 100);
        a.recycle();  //回收
    }

接下來處理onDraw方法裡面的內容即可

自定義view,畫筆畫布不可少,

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 繪製文字
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
     canvas.drawText(mText,0,0,mPaint);
}

看下效果:

與想象的似乎不太一樣,文字的X座標和Y座標並不是從螢幕的0、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);
    }

從原始碼的註釋發現,y座標是根據一個叫baseline的值來開始繪製的,那麼baseline是什麼東西呢:

以上圖來理解,baseline就是紅線,指的是文字的基準線,就好像我們小學時候的標準拼音四格線一樣:

因此,當y設定為0的時候,實際上就是基準線為0,那麼此時我們在螢幕上就只能看到基準線以下的區域,所以才會出現執行效果圖的那種情況,所以當我們設定baseline=100時看看效果:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 繪製文字
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        float baseline = 100;
        canvas.drawText(mText, 0, baseline, mPaint);
    }

那麼此時文字就可以顯示出來了。

以上是最最基本的自定義textview,那麼加強一點難度,我們如何將文字橫豎都居中呢?

1、我們先把中心座標給畫出來,便於我們處理是否真的是居中了。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 繪製文字
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        float baseline = 100;
        canvas.drawText(mText, 0, baseline, mPaint);

        drawCenterLineX(canvas);
        drawCenterLineY(canvas);

    }

    private void drawCenterLineX(final Canvas canvas){
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.FILL);//實線
        paint.setColor(Color.RED);// 顏色
        paint.setStrokeWidth(3);// 線的寬度
        canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
    }

    private void drawCenterLineY(final Canvas canvas){
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(3);
        canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
    }

2、橫向居中,有兩種方法

(1)在繪製文字之前也就是drawText之前新增如下程式碼

mPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(mText, getWidth()/2, baseline, mPaint);

(2)當setTextAlign不設定為CENTER時,預設則是為LEFT,效果如圖所示

//mPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(mText, getWidth()/2, baseline, mPaint);

那麼此時想要居中的話,應該在X軸向左偏移文字寬度的一半即可,效果圖+程式碼如下

//mPaint.setTextAlign(Paint.Align.CENTER);
float width = mPaint.measureText(mText);//獲取文字寬度
canvas.drawText(mText, (getWidth()-width)/2, baseline, mPaint);

3、縱向居中

縱向座標的起始位置取決於baseline,先把baseline設定為螢幕高度的一半看看效果,為了有更明顯的感覺,我把文字大小進行了調整,變得更大了

float baseline = getHeight()/2;
canvas.drawText(mText, (getWidth()-width)/2, baseline, mPaint);

發現文字在縱向並沒有居中,與前面的分析一樣,也就是如果我們將baseline設定為控制元件高度的一半,那麼文字的繪製是以該線為基準線,那麼想要將文字縱向居中要如何處理呢?paint有一個屬性

Paint.FontMetrics fontMetrics = paint.getFontMetrics();

FontMetrics有幾個屬性,對應的就是下圖所標明的位置

// public float ascent;//基準線距離上邊界的距離,文字通常在這個範圍內(由於各個國家地區文字不同,可能會有超出這個範圍的,例如藏文,但是最高不會超出top),ascent為負數

// public float bottom; //基準線距離下邊界的最高距離,文字最低不能超出bottom的範圍

// public float descent;//基準線距離下邊界的距離,文字通常在這個範圍內(由於各個國家地區文字不同,可能會有超出這個範圍的,例如藏文,但是最高不會超出bottom)

// public float leading;//看成行距即可

// public float top; //基準線距離上邊界的最高距離,文字最高不能超出top的範圍,top為負數

注:Android中,X或Y偏移時,往左和上是減,往右和下是加,而top和ascent的值為負數

那麼實際上想要文字垂直方向居中,那麼基於上圖再結合我們的執行效果圖,我們可以得出以下結論:

此時圖中紅線也就是基準線,也是螢幕的中心線,如果我們想讓文字垂直居中,在中心線不變的情況下,那麼文字需要再往下移動一段距離,也就是基準線(baseline)需要往下移動才能使文字居中

結合圖中字母Jj,可以想象成,文字下移一段距離之後,紅線才會處於文字的中間,那麼這個文字下移的距離就是baseline下移的距離,那麼我們可以得出結論

首先紅線的位置在我們程式碼裡面是 getHeight()/2的位置

文字先向下走ascent的距離,再向上走(descent+ascent)/2的距離,就居中了。

因為ascent為負數,所以要取絕對值,所以向下走是 getHeight()/2-ascent,再向上走getHeight()/2-ascent-(descent-ascent)/2

float baseline = getHeight() / 2 - fontMetrics.ascent - (fontMetrics.descent - fontMetrics.ascent) / 2;

一步一步簡化公式:

float baseline = getHeight() / 2 - fontMetrics.ascent - fontMetrics.descent/2 + fontMetrics.ascent/ 2;
float baseline = getHeight() / 2 - fontMetrics.descent/2 - fontMetrics.ascent + fontMetrics.ascent/ 2;
float baseline = getHeight() / 2 - fontMetrics.descent/2 - fontMetrics.ascent/2;
float baseline = getHeight() / 2 - (fontMetrics.descent + fontMetrics.ascent)/2;

看到上面這段程式碼就非常的熟悉了,這個公式應該在很多部落格裡面都見到過,就是這樣簡算得到的,如果不進行這樣的分析,估計很多人都無法理解這個公式到底是什麼意思。看下效果圖:

最終onDraw方法裡面的程式碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 繪製文字
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        //mPaint.setTextAlign(Paint.Align.CENTER);
        float width = mPaint.measureText(mText);//獲取文字寬度

        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float baseline = getHeight() / 2 - (fontMetrics.descent + fontMetrics.ascent)/2;
        canvas.drawText(mText, (getWidth() - width) / 2, baseline, mPaint);

        drawCenterLineX(canvas);
        drawCenterLineY(canvas);

    }

完。