文章更新次數︰3
一、前提
話說人是貪婪的,這句話用在沉浸於3C的使用者來說,
真是滿恰當的。
一開始就以這段文字做開頭,
跟今天的主題很有關係,
一般初學者剛開始學的,
大部份都是如何建立一個TextView、EditText、Button等等的元件。
想要在我們的畫面上叫出一個TextView文字框,我們有以下2種方式︰
1.在程式碼裡
TextView text = new TextView(this); text.setText("test"); 我們的Layout.addView(text); //將TextView新增至我們的Layout2.在xml裡宣告
<TextView android:layout_width="fill_parent" android:layout_height="warp_content" android:text="test"/>於是,我們可以很順利的得到這個畫面
但是這些基礎元件用久了,我們或使用者,
開始想要搞東搞西,想要客製化。
以下是這篇分享文最後會呈現的樣子︰
ㄜ...這不就是再加一行字,並且把字的大小和顏色改一改而已嗎?
有什麼好說的呢?
二、文章開始
如果你這麼想,可就大錯特錯囉!快來看看我在layout資料夾裡面的main.xml放了些什麼︰
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> <com.test.testcustomize.LabelView android:layout_width="fill_parent" android:layout_height="wrap_content" app:text="Reddd" /> </LinearLayout>有注意到嗎?多了
xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize" app:text="Reddd"這時候你會問︰
app:text="Reddd"這個標籤怎麼來的?
從這裡︰
只要在res/values/裡新增attrs.xml這份文件,宣告我們自訂的屬性,
然後在main.xml裡導入這份文件,
再加上之後會提到LabelView.class的一些設定,
就可以達到藍色字的效果了。
也許你會說︰
「天啊!
為什麼我們要把一個原本可以很簡單設定大小和顏色就能做到的事,
搞得那麼複雜?」
這就是我今天要說的「客製化」。
不知道你有沒有注意到,畫面中藍色的那行字,
只在main.xml裡很簡單的宣告文字內容,
app:text="Reddd"卻呈現了藍色、而且更大的字體。
Android允許我們自訂View,
呈現我們自己想呈現的基本樣式。
技術文件說︰
如果我們想要有自己的View,
那我們就自己寫一個類並繼承View。
所以在這份文件裡,我多寫了一個LabelView來繼承View。
該類別Code如下(可快速跳過這段)︰
package com.test.testcustomize; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; public class LabelView extends View { private Paint mTextPaint; private String mText; private int mAscent; public LabelView(Context context) { super(context); initLabelView(); } public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if(s!=null){ setText(s.toString()); } setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); } private void initLabelView(){ mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(24); mTextPaint.setColor(0xFF000000); setPadding(3,3,3,3); } private void setText(String str){ mText = str; requestLayout(); invalidate(); //每呼叫一次就會重新繪圖onDraw() } /** * Sets the text size for this label * @param size Font size */ public void setTextSize(int size) { mTextPaint.setTextSize(size); requestLayout(); invalidate(); //每呼叫一次就會重新繪圖onDraw() } /** * Sets the text color for this label. * @param color ARGB value for the text */ public void setTextColor(int color) { mTextPaint.setColor(color); invalidate(); //每呼叫一次就會重新繪圖onDraw() } /** * @see android.view.View#measure(int, int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } /** * Determines the width of this view * @param measureSpec A measureSpec packed into an int * @return The width of the view, honoring constraints from measureSpec */ private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } /** * Determines the height of this view * @param measureSpec A measureSpec packed into an int * @return The height of the view, honoring constraints from measureSpec */ private int measureHeight(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); mAscent = (int) mTextPaint.ascent(); Log.i("tag", "Height Ascent: "+mAscent); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text (beware: ascent is a negative number) result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom(); Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent()); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint); } }Code很長,請聽我一一道來。
這段Code的基本骨架是這樣的︰
1.一開始,宣告2個建構式︰
//建構式1 public LabelView(Context context) { super(context); initLabelView(); //設定文字的最初樣式 } //建構式2 public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); //設定文字的最初樣式 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if(s!=null){ setText(s.toString()); } setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); }技術文件裡提到,
如果我們想對我們想呈現的View初始化,
那麼我們就應該在建構式裡做基本的設定。
先不看第2段建構式的龐大內容,
這2個建構式都有一個共通點︰
都呼叫了
initLabelView();該函式的內容如下︰
private void initLabelView(){ mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); //設定反鉅齒 mTextPaint.setTextSize(24); //設定預設大小為24 mTextPaint.setColor(0xFF000000); //設定預設的顏色為黑字體 setPadding(3,3,3,3); //設定在版面上下左右各距離3 }我們在輸入文字的一開始,
馬上呼叫了Paint,
並對這個我們自訂的View做了初始化.
回到第2個建構式︰
//建構式2 public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if(s!=null){ setText(s.toString()); } setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); }第6行︰將我們自己做的attrs.xml呼叫進來
第7行︰取出我們在main.xml裡設定的字
第8行︰如果有取到字,則呼叫setText()函式
第11行︰設定文字最後會呈現的顏色,如果在main.xml裡面沒有設定的話,
就用這裡的預設值︰0xFF0000FF,設為藍色
第13行︰設定文字大小
第18行︰讓我們自訂的attrs可以重覆被使用(因為以後也可能被用到啊!)
Android在畫出一個View時,
原理是這樣子的︰
1.不斷的呼叫我們程式裡的onMeasure()函式去量測現在要畫的View的尺寸會有多大︰
/** * @see android.view.View#measure(int, int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); }2.最後呼叫onDraw()繪圖
當我在試驗時,程式為了畫出藍色字體,
至少呼叫了3次的onMeasure()才呼叫onDraw()。
所以我們得知︰
系統在開始onDraw()前,
會不斷的呼叫onMeasure()去跟Layout問現在要畫的藍色字體,
範圍是多大。
所以,我們就要將尺寸的算法在這裡交待一下了︰
/** * 量測這個View的寬 * @param measureSpec A measureSpec packed into an int * @return The width of the view, honoring constraints from measureSpec */ private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; }
/** * 量測這個View的高 * @param measureSpec A measureSpec packed into an int * @return The height of the view, honoring constraints from measureSpec */ private int measureHeight(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); mAscent = (int) mTextPaint.ascent(); Log.i("tag", "Height Ascent: "+mAscent); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text (beware: ascent is a negative number) result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom(); Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent()); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; }第6行︰一開始系統傳進來的引數measureSpec是從main.xml裡抓過來的
第9行︰在MeasureSpec.getMode()裡會抓到的尺寸模式,有以下3種
AT_MOST
| |||
EXACTLY
| |||
UNSPECIFIED
|
幾次的onMeasure()呼叫完後,就跑去呼叫onDraw()開始繪圖了。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint); }挪,結束了。
三、結論
1.在res/values/新增一個自訂的attrs.xml2.在main.xml裡使用他們
3.繼承View,並在
(1)建構式 裡宣告初始值
(2)在onMeasure()裡交待清楚這個自創的View應該有多大
(3)在onDraw()裡開始繪圖
這樣子,你就能為所欲為,畫出任何你預設想呈現出來的View了。
有點複雜,但搞懂了就簡單了。
備註︰
來源︰百度知道
1.基準點是baseline
2.ascent:是baseline之上至字符最高處的距離
3.descent:是baseline之下至字符最低處的距離
4.leading:是上一行字符的descent到下一行的ascent之間的距離,也就是相鄰行間的空白距離
5.top:指的是最高字符到baseline的值,即ascent的最大值
6.bottom:指最低字符到baseline的值,即descent的最大值
感謝分享
ReplyDelete感謝分享
ReplyDelete不錯啊!
ReplyDelete點個廣告贊助你。
試著把你所寫的程式碼
ReplyDelete複製、貼上到 Android 去
會出現錯誤耶!
Exception raised during rendering: text cannot be null
Exception details are logged in Window > Show View > Error Log
java.lang.IllegalArgumentException: text cannot be null
at android.graphics.Paint.measureText(Paint.java:1350)
at com.custom.label.LabelView.measureWidth(LabelView.java:100)
at com.custom.label.LabelView.onMeasure(LabelView.java:79)
at android.view.View.measure(View.java:15848)
at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:728)
at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:477)
at android.view.View.measure(View.java:15848)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5012)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:15848)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5012)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1404)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:695)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:588)
at android.view.View.measure(View.java:15848)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5012)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1404)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:695)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:588)
at android.view.View.measure(View.java:15848)
找到你程式出錯的地方了
ReplyDelete在 26 行的地方
請加上
else{
setText("TextView");
}