本文為原創,如需轉載,請注明作者和出處,謝謝!
本文為新書《Android/OPhone
開發完全講義》的內容連載。《Android/OPhone開發完全講義》一書現已出版,敬請關注。
購 買:
互動網
《Android/OPhone
開發完全講義》目錄
源代碼下載
在本例中要實現一個可以在文本前方添加一個圖像(可以是任何Android系統支持的圖像格式)的TextView組件。在編寫代碼之前,先看一下Android組件的配置代碼。
<TextView android:id="@+id/textview1" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="textview1" />
上面的代碼配置了一個標準的TextView組件。在這段代碼中主要有兩部分組成:組件標簽(<TextView>)和標簽屬性(android:id、android:layout_width等)。需要注意的是,在所有的標簽屬性前面都需要加了一個命名空間(android)。實際上,android命名空間的值是在Android系統中預定義的,所有Android系統原有的組件在配置時都需要在標簽屬性前加android。
對于定制組件,可以有如下3種選擇。
1. 仍然沿用android命名空間。
2. 改用其他的命名空間。
3. 不使用命名空間。
雖然上面3種選擇從技術上說都沒有問題,但作者建議使用第2種方式(尤其是對外發布的組件),這是因為在使用定制組件時,可能需要指定相同名稱的屬性,在這種情況下,可以通過命名空間來區分這些屬性,例如,有兩個命名空間:android和mobile,這時可以在各自的命名空間下有相同名稱的屬性,如android:src和mobile:src。在本例中定義了一個mobile命名空間,因此,在配置本例實現的組件時需要在屬性前加mobile。
實現定制組件的一個重要環節就是讀取配置文件中相應標簽的屬性值,由于本例要實現的組件類需要從TextView類繼承,因此,只需要覆蓋TextView類中帶AttributeSet類型參數的構造方法即可,該構造方法的定義如下:
public TextView(Context context, AttributeSet attrs)
在構造方法中可以通過AttributeSet接口的相應getter方法來讀取指定的屬性值,如果在配置屬性時指定了命名空間,需要在使用getter方法獲得屬性值時指定這個命名空間,如果未指定命名空間,則將命名空間設為null即可。
IconTextView是本例要編寫的組件類,該類從TextView繼承,在onDraw方法中將TextView中的文本后移,并在文本的前方添加了一個圖像,該圖像的資源ID通過mobile:iconSrc屬性來指定。IconTextView類的代碼如下:
package net.blogjava.mobile.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.widget.TextView;
public class IconTextView extends TextView
{
// 命名空間的值
private final String namespace = "http://net.blogjava.mobile";
// 保存圖像資源ID的變量
private int resourceId = 0;
private Bitmap bitmap;
public IconTextView(Context context, AttributeSet attrs)
{
super(context, attrs);
// getAttributeResourceValue方法用來獲得組件屬性的值,在本例中需要通過該方法的第1個參數指
// 定命名空間的值。該方法的第2個參數表示組件屬性名(不包括命名空間名稱),第3個參數表示默
// 認值,也就是如果該屬性不存在,則返回第3個參數指定的值
resourceId = attrs.getAttributeResourceValue(namespace, "iconSrc", 0);
if (resourceId > 0)
// 如果成功獲得圖像資源的ID,裝載這個圖像資源,并創建Bitmap對象
bitmap = BitmapFactory.decodeResource(getResources(), resourceId);
}
@Override
protected void onDraw(Canvas canvas)
{
if (bitmap != null)
{
// 從原圖上截取圖像的區域,在本例中為整個圖像
Rect src = new Rect();
// 將截取的圖像復制到bitmap上的目標區域,在本例中與復制區域相同
Rect target = new Rect();
src.left = 0;
src.top = 0;
src.right = bitmap.getWidth();
src.bottom = bitmap.getHeight();
int textHeight = (int) getTextSize();
target.left = 0;
// 計算圖像復制到目標區域的縱坐標。由于TextView組件的文本內容并不是
// 從最頂端開始繪制的,因此,需要重新計算繪制圖像的縱坐標
target.top = (int) ((getMeasuredHeight() - getTextSize()) / 2) + 1;
target.bottom = target.top + textHeight;
// 為了保證圖像不變形,需要根據圖像高度重新計算圖像的寬度
target.right = (int) (textHeight * (bitmap.getWidth() / (float) bitmap.getHeight()));
// 開始繪制圖像
canvas.drawBitmap(bitmap, src, target, getPaint());
// 將TextView中的文本向右移動一定的距離(在本例中移動了圖像寬度加2個象素點的位置)
canvas.translate(target.right + 2, 0);
}
super.onDraw(canvas);
}
}
在編寫上面代碼時需要注意如下3點:
1. 需要指定命名空間的值。該值將在<LinearLayout>標簽的xmlns:mobile屬性中定義。
2. 如果在配置組件的屬性時指定了命名空間,需要在AttributeSet 接口的相應getter方法中的第1個參數指定命名空間的值,而第2個參數只需指定不帶命名空間的屬性名即可。
3. TextView類中的onDraw方法一定要在translate方法后面執行,否則系統不會移動TextView中的文本。
下面在main.xml文件中配置了7個IconTextView組件,分別設置了不同的字體大小,同時,文本前面的圖像也會隨著字體大小的變化而放大或縮小,配置代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<!-- 在下面的標簽中通過xmlns:mobile屬性定義了一個命名空間 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:mobile="http://net.blogjava.mobile" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<!-- mobile:iconSrc是可選屬性,如果未設置該屬性,則IconTextView與TextView的效果相同 -->
<!-- 由于IconTextView和Main類不在同一個包中,因此,需要顯式指定package -->
<net.blogjava.mobile.view.IconTextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="第一個笑臉" mobile:iconSrc="@drawable/small" />
<net.blogjava.mobile.widget.IconTextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="第二個笑臉" android:textSize="24dp" mobile:iconSrc="@drawable/small" />
<net.blogjava.mobile.widget.IconTextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="第三個笑臉" android:textSize="36dp" mobile:iconSrc="@drawable/small" />
<net.blogjava.mobile.widget.IconTextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="第四個笑臉" android:textSize="48dp" mobile:iconSrc="@drawable/small" />
<net.blogjava.mobile.widget.IconTextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="第五個笑臉" android:textSize="36dp" mobile:iconSrc="@drawable/small" />
<net.blogjava.mobile.widget.IconTextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="第六個笑臉" android:textSize="24dp" mobile:iconSrc="@drawable/small" />
<net.blogjava.mobile.widget.IconTextView
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="第七個笑臉" mobile:iconSrc="@drawable/small" />
</LinearLayout>
運行本實例后,將顯示如圖1所示的效果。

注意:雖然很多人認為組件的屬性必須以android命名空間開頭,該命名空間的值必須是http://schemas.android.com/apk/res/android。實際上,只是命名空間的值必須是http://schemas.android.com/apk/res/android而已,命名空間的名稱可以是任何值,如下面的代碼所示:
<?xml version="1.0" encoding="utf-8"?>
<!-- 將android換成了abcd -->
<LinearLayout xmlns:abcd="http://schemas.android.com/apk/res/android"
abcd:orientation="vertical" abcd:layout_width="fill_parent"
abcd:layout_height="fill_parent">

</LinearLayout>
新浪微博:http://t.sina.com.cn/androidguy 昵稱:李寧_Lining