首先新建文件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); } }
