Android開發——自定義view之文字繪製
首先新建檔案MyTextView,繼承AppCompatTextView,並重寫onDraw方法:
public class MyTextView extends AppCompatTextView { /** * 需要繪製的文字 */ private String mText; /** * 文字的顏色 */ private int mTextColor; /** * 文字的大小 */ private int mTextSize; private Paint mPaint; publicMyTextView(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); } @Overrideprotected 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); }