文章更新次數︰3
一、前提
話說人是貪婪的,這句話用在沉浸於3C的使用者來說,
真是滿恰當的。
一開始就以這段文字做開頭,
跟今天的主題很有關係,
一般初學者剛開始學的,
大部份都是如何建立一個TextView、EditText、Button等等的元件。
想要在我們的畫面上叫出一個TextView文字框,我們有以下2種方式︰
1.在程式碼裡
TextView text = new TextView(this);
text.setText("test");
我們的Layout.addView(text); //將TextView新增至我們的Layout
2.在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的最大值

5 comments:
感謝分享
感謝分享
不錯啊!
點個廣告贊助你。
試著把你所寫的程式碼
複製、貼上到 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)
找到你程式出錯的地方了
在 26 行的地方
請加上
else{
setText("TextView");
}
Post a Comment