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);

    }

如何仿歌詞動畫?

已知文字是沒有只設置一部分顏色值這個方法的,聽歌的時候,歌詞是逐漸的變色,那么思路是如何呢?

首先要了解canvas(畫布),canvas在繪制的時候是可以一層一層繪制的,以save方法開始、restore方法結束為一層。

canvas.save();
...
canvas.restore();
canvas.save();
...
canvas.restore();

得知這個之后,我們就有思路了,我們可以將文字分為兩層,例如底層為黑色,上層為紅色,紅色逐漸顯示出來,覆蓋黑色的字,那么就能實現我們需要的效果了。

先繪制兩層:

    private Paint mPaint = new Paint();
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //底層 黑色
        drawCenterText1(canvas);
        //上面一層 紅色
        drawCenterText2(canvas);

        //中心線
        drawCenterLineX(canvas);
        drawCenterLineY(canvas);

    }

    private void drawCenterText1(Canvas canvas){
        // 繪制黑色文字
        canvas.save();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);//抗鋸齒
        //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);
        canvas.restore();
    }

    private void drawCenterText2(Canvas canvas){
        // 繪制紅色文字
        canvas.save();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        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);
        canvas.restore();
    }

在我們自定義view里面,除了Paint、canvas之外還有一個類Rect類,它是用來設置畫布大小的,配合canvas.clipRect(rect);一起使用,我們可以把它看成裁剪。

該方法用於裁剪畫布,也就是設置畫布的顯示區域
調用clipRect()方法后,只會顯示被裁剪的區域,之外的區域將不會顯示

由於是canvas繪制了兩層,那么如果將第二層進行逐漸的裁剪,讓它由0%到100%逐漸的進行顯示,就能達到預期的效果了。所以在第二層紅色的字體上加上Rect來進行處理

    private void drawCenterText2(Canvas canvas) {
        // 繪制紅色文字
        canvas.save();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);

        // 文字X軸起始位置
        float width = mPaint.measureText(mText);//獲取文字寬度
        float X = (getWidth() - width) / 2;

        // 文字baseline位置
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float baseline = getHeight() / 2 - (fontMetrics.descent + fontMetrics.ascent) / 2;

        //裁剪的范圍(矩形的四條邊)
        int left = (int)X;
        int top = 0;
        int right = (int)(left + width);
        int bottom = getHeight();
        Rect rect = new Rect(left, top, right, bottom);
        canvas.clipRect(rect);//掏空
        canvas.drawText(mText, X, baseline, mPaint);
        canvas.restore();
    }

運行效果圖(紅色字體全部顯示出來):

將代碼中right的值改成

int right = (int)(left + width/2);

看下效果

此時,證明我們的思路是對的,那么我們只要處理好百分比逐漸增加,就能達到預期效果了。

自定義view的類中添加變量

    private float percent = 0.0f;

    public float getPercent() {
        return percent;
    }

    public void setPercent(float percent) {
        this.percent = percent;
        invalidate();//重繪
    }

每當percent的值改變,也就是每次調用setPercent方法之后,重繪一次。那么right的代碼就要改成

int right = (int)(left + width*percent);

修改percent的值我使用的是,在activity里面調用屬性動畫用來測試

    my_tv = (MyTextView) findViewById(R.id.my_tv);
        //屬性動畫
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                onStartLeft(null);
            }
        }, 2000);
    public void onStartLeft(View view) {
        ObjectAnimator.ofFloat(my_tv, "percent", 0, 1).setDuration(5000).start();
    }

在onStartLeft方法里面有一個“percent”參數,這個實際上只要是在自定義view里面有一個這個參數的set方法,那么他就會通過放射去調用setPercent方法。

效果實現完畢。接下來進行查漏補缺。

1、通過我們的方式進行漸變,那么會重復的調用onDraw方法,因此Paint需要放在最外層new出來,否則會產生很多的GC動作。

2、繪制的層級越少越好,不要過度繪制

  • 真彩色:沒有過度繪制

  • 藍色:過度繪制 1 次
  • 綠色:過度繪制 2 次
  • 粉色:過度繪制 3 次
  • 紅色:過度繪制 4 次或更多次

如果顯示紅色,那么布局就要進行修改了,而我們上面的代碼結束時候,層級是2,顯示的是綠色。那么是否有辦法把他降低為一層呢?

思路:當我們第二層紅色字體在繪制的時候,第一層的黑色字體就不繪制,即“一進一出”,這樣在繪制的時候就始終都為一層了。

那么第二層紅色字體是從左到右依次繪制(即左坐標不變,右坐標百分比增加),所以第一層黑色字體就是從左到右依次消失(左坐標依次增加,右坐標不變)

修改后drawCenterText1方法代碼

    private void drawCenterText1(Canvas canvas) {
        // 繪制黑色文字
        canvas.save();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);//抗鋸齒

        // 文字X軸起始位置
        float width = mPaint.measureText(mText);//獲取文字寬度
        float X = (getWidth() - width) / 2;

        // 文字baseline位置
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float baseline = getHeight() / 2 - (fontMetrics.descent + fontMetrics.ascent) / 2;

        //裁剪的范圍(矩形的四條邊)
        int left = (int) (X + width * percent);
        int top = 0;
        int right = (int) (X + width);
        int bottom = getHeight();
        Rect rect = new Rect(left, top, right, bottom);
        canvas.clipRect(rect);//掏空

        canvas.drawText(mText, X, baseline, mPaint);
        canvas.restore();
    }

MyTextView整體代碼:

public class MyTextView extends AppCompatTextView {
    private float percent = 0.0f;

    public float getPercent() {
        return percent;
    }

    public void setPercent(float percent) {
        this.percent = percent;
        invalidate();//重繪
    }

    /**
     * 需要繪制的文字
     */
    private String mText;
    /**
     * 文本的顏色
     */
    private int mTextColor;
    /**
     * 文本的大小
     */
    private int mTextSize;

    public MyTextView(Context context) {
        super(context);
    }

    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();  //回收
    }

    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private Paint mPaint = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //底層 黑色
        drawCenterText1(canvas);
        //上面一層 紅色
        drawCenterText2(canvas);

        //中心線
        drawCenterLineX(canvas);
        drawCenterLineY(canvas);

    }

    private void drawCenterText1(Canvas canvas) {
        // 繪制黑色文字
        canvas.save();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);//抗鋸齒

        // 文字X軸起始位置
        float width = mPaint.measureText(mText);//獲取文字寬度
        float X = (getWidth() - width) / 2;

        // 文字baseline位置
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float baseline = getHeight() / 2 - (fontMetrics.descent + fontMetrics.ascent) / 2;

        //裁剪的范圍(矩形的四條邊)
        int left = (int) (X + width * percent);
        int top = 0;
        int right = (int) (X + width);
        int bottom = getHeight();
        Rect rect = new Rect(left, top, right, bottom);
        canvas.clipRect(rect);//掏空

        canvas.drawText(mText, X, baseline, mPaint);
        canvas.restore();
    }

    private void drawCenterText2(Canvas canvas) {
        // 繪制紅色文字
        canvas.save();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);

        // 文字X軸起始位置
        float width = mPaint.measureText(mText);//獲取文字寬度
        float X = (getWidth() - width) / 2;

        // 文字baseline位置
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float baseline = getHeight() / 2 - (fontMetrics.descent + fontMetrics.ascent) / 2;

        //裁剪的范圍(矩形的四條邊)
        int left = (int) X;
        int top = 0;
        int right = (int) (left + width * percent);
        int bottom = getHeight();
        Rect rect = new Rect(left, top, right, bottom);
        canvas.clipRect(rect);//掏空
        canvas.drawText(mText, X, baseline, mPaint);
        canvas.restore();
    }

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

 

完。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM