1. 程式人生 > >Android自定義控制元件開發系列(一)——第一次動手做自定義控制元件

Android自定義控制元件開發系列(一)——第一次動手做自定義控制元件

        Android系統提供的控制元件多種多樣,以至於很多初學者經常忘了還有這樣那樣的控制元件沒用過甚至沒聽過。儘管如此,但是系統控制元件大多比較死板,而且不夠美觀,很多多樣化的顯示或是互動方式都沒法完成。每每遇到這種情況,就需要我們來開發我們自己的控制元件了——所謂的“自定義控制元件”。接下來我們就一步一步紮紮實實的從頭開始Android自定義控制元件的開發。

        廢話少說,開始吧:

        一、實現自定義控制元件的3種主要方式

        (1)修改已有控制元件——繼承已有控制元件,重寫其顯示、響應等;

        (2)組合已有控制元件——將已有的系統控制元件組合成一個獨特的控制元件(接下來的示例中就是這種演示);

        (3)開發全新的控制元件——一般繼承View或SurfaceView。他們都提供一個Canvas(畫布)和一系列的畫的方法,還有Paint(畫筆)。使用它們去建立一個自定義的UI。你可以重寫事件,包括螢幕接觸或者按鍵按下等等,用來提供與使用者互動。這種方式比較高階,需要熟悉View的工作原理並熟悉其各個回撥方法。

        二、為自定義控制元件增加屬性的兩種方法:

        (1)在自定義類中定義屬性欄位

         本文通過開發一個可以顯示文字和圖片組合的控制元件來加以說明。

       通過建構函式中引入的AttributeSet 去查詢XML佈局的屬性名稱,然後找到它對應引用的資源ID去找值:

public class MyView1 extends View {
    private String mtext;
    private int msrc;

    public MyView1(Context context) {
        super(context);
    }

    public MyView1(Context context, AttributeSet attrs) {
        super(context, attrs);

        //方法attrs.getAttributeResourceValue(String nameSpace, String attriName, int default)的nameSpace引數傳null即可————
        int textId = attrs.getAttributeResourceValue(null, "Text", 0);
        int imgId = attrs.getAttributeResourceValue(null, "Img", 0);

        //根據傳來的id找出字串,比如示例程式碼中傳入的是@stirng/hello_world
        //這裡也可以直接在xml檔案中設定字串的引數但是獲取屬性值的方法就要相應變成
        //mtext = attrs.getAttributeValue(null, "Text")直接獲取傳入的字串
        mtext = getResources().getText(textId).toString();
        msrc = imgId;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setTextSize(30);

        InputStream is = getResources().openRawResource(msrc);
        Bitmap bitmap = BitmapFactory.decodeStream(is);
        int imgWid = bitmap.getWidth();
        int imgHei = bitmap.getHeight();
        canvas.drawBitmap(bitmap, 0, 0, paint);
        canvas.drawText(mtext,imgWid/3, imgHei/2, paint);
    }
}

        然後在佈局檔案中使用我們自定義的控制元件(控制元件名要用包名+類名):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.wangj.cusview.MyView1
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        Img="@drawable/xx"
        Text="@string/hello_world" />
</RelativeLayout>

         執行一下如下圖:

       

        到這裡,可能會有人問,佈局檔案裡自定義控制元件的屬性設定直接用Img="@drawable/xx"和Text="@string/hello_world",怎麼不像系統控制元件的屬性那樣的寫法android:layout_height="wrap_content"呢?這裡就需要回顧一下上邊我們在上邊MyView1類中用attrs.getAttributeResourceValue(String nameSpace, String attriName, int default)方法時的nameSpace引數傳了個null,沒有使用名稱空間

        ----------------------------------------------好了,上邊這種方法完成了,下邊的和上邊的一樣,只是更規範而已----------------------------------------------

        什麼是名稱空間呢?呵呵,你就先理解成一個歸屬吧,就像你們學校有好多個叫“王小二”的同學,校長要叫王二小同學來辦公室聊聊,這是就要指出是“哪個班的王二小”了,名稱空間就相當於這個班級的作用。我們看佈局檔案,根節點屬性中都會有xmlns:android="http://schemas.android.com/apk/res/android"這一句,這就是宣告一個名稱空間,“名稱空間的名字”就叫android,接下來就可以用android:layout_height="wrap_content"這種寫法了。

        接下來我們也將上邊的佈局檔案改成這種方式(我們也自己給一個名稱空間)(這裡需要說明一下,有人認為系統元件的名稱空間必須要用“android”,其實不是的。“android”只是名稱空間的名稱,“http://schemas.android.com/apk/res/android”才是名稱空間,你把xmlns:android="http://schemas.android.com/apk/res/android"改成xmlns:abcd="http://schemas.android.com/apk/res/android",在系統元件屬性用abcd:layout_width也是沒有問題的):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:myview="http://blog.csdn.net/a_running_wolf"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--這裡第二行xmlns:my="http://blog.csdn.net/a_running_wolf"就是自己定義的名稱空間-->
    <!--***注意這裡的http://blog.csdn.net/a_running_wolf要和自定義類中的nameSpace一致,否則屬性找不出來的-->

    <com.wangj.cusview.MyView1
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        myview:Img="@drawable/xx"
        myview:Text="@string/hello_world" />  <!--上邊定義了名稱空間,這裡就可以像系統控制元件的屬性一樣的寫法了-->

</RelativeLayout>

        相應的,在MyView1中就應該使用我們宣告的這個名稱空間了,直接看程式碼(這個類應該先於佈局檔案建立,不然在佈局檔案中怎麼會有自己的控制元件可以用呢?這裡放在後邊主要是讓大家重點看“名稱空間”)

public class MyView1 extends View {
    //要和佈局檔案中宣告的名稱空間一致
    private final String nameSpace = "http://blog.csdn.net/a_running_wolf";
    private String mtext;
    private int msrc;

    public MyView1(Context context) {
        super(context);
    }

    public MyView1(Context context, AttributeSet attrs) {
        super(context, attrs);
        int textId = attrs.getAttributeResourceValue(nameSpace, "Text", 0);   //引數nameSpace用我們的名稱空間
        int imgId = attrs.getAttributeResourceValue(nameSpace, "Img", 0);
        mtext = getResources().getText(textId).toString();
        msrc = imgId;
    }

    //onDraw方法和上邊的一樣
    .......
 }
        改造完畢,執行一下和上邊一致:

       

        顯然這種方式能實現相應的效果,但是效果很不好,文字顯示在圖片的什麼地方、如果文字很多圖片很小呢、如果我要改文字位置還要手動改drawText的位置引數,那麼我們來看更規範的做法——往下看。

        (2)通過XML資原始檔為自定義控制元件註冊屬性(和Android系統提供屬性的方式一致)

        通過XML資原始檔為控制元件註冊屬性,並使用名稱空間的方式就是Android系統控制元件的屬性提供方法,所以這種方式是推薦的自定義控制元件開發方法。首先在value目錄下建立attrs.xml檔案(有的話就不用建立了,直接往進寫),寫入如下的屬性欄位和相應的值(當然這裡是根據自己的需要寫,如下只是示例):
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyView2">
        <attr name="Text" format="reference|string" />
        <attr name="Oriental">
            <enum name="Herizontal" value="1" />
            <enum name="Vertical" value="0" />
        </attr>
        <attr name="Img" format="reference|integer" />
    </declare-styleable>
</resources>

        建立好attrs.xml後就可以用這些屬性欄位了(前提是你先建立好MyView2,否則控制元件啊!可以先建立一個空類,構造方法具體實現先不寫,等後邊在具體實現):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:TV="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/activity_vertical_margin">
    <!-- 有的部落格上說這裡自己的名稱空間要用“http://schemas.android.com/apk/包名”,但是我在AndroidStudio1.3.2、Android5.0時
    用“http://schemas.android.com/apk/res-auto”更智慧一點 -->

    <com.wangj.cusview.MyView2
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        TV:Text="@string/app_name"
        TV:Img="@drawable/xx"
        TV:Oriental="Herizontal"/>
</RelativeLayout>

       附上MyView2的實現類: 

public class MyView2 extends LinearLayout {   //為了簡單,這裡繼承LinearLayout
    public MyView2(Context context) {
        super(context);
    }

    public MyView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        int resourceId = -1;
        
        //獲取佈局檔案中對應控制元件的屬性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView2);
        ImageView iv = new ImageView(context);
        TextView tv = new TextView(context);
        int N = typedArray.getIndexCount();
        for (int i = 0; i < N; i++){
            int attr = typedArray.getIndex(i);
            switch (attr){
                case R.styleable.MyView2_Text:
                    resourceId = typedArray.getResourceId(R.styleable.MyView2_Text, 0);
                    tv.setText(resourceId > 0 ? getResources().getText(resourceId) : typedArray.getString(R.styleable.MyView2_Text));
                    break;
                case R.styleable.MyView2_Img:
                    resourceId = typedArray.getResourceId(R.styleable.MyView2_Img, 0);
                    iv.setImageResource(resourceId > 0 ? resourceId : R.mipmap.ic_launcher);
                    break;
                case R.styleable.MyView2_Oriental:
                    resourceId = typedArray.getInt(R.styleable.MyView2_Oriental, 0);
                    this.setOrientation(resourceId == 1 ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL );
                    break;
            }
        }

        //記住要將TextView和ImageView都addView,否則什麼都顯示不了
        addView(iv);
        addView(tv);
        typedArray.recycle();   //釋放資源
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

      MainActivity中沒有做任何邏輯。直接執行,看效果:


        這裡看到這個紅色框中就是我們的MyView2控制元件(不包括紅色框)。肯定有人問,這裡看起來這個控制元件就和一個ImageView和一個TextView水平線性排列就一個樣子啊?其實是的,但是區別在於——這裡是一個控制元件,而不是兩個控制元件——這裡是為了簡單演示,就簡單的讓MyView2繼承了LineraLayout。根據實際的需求,我們可已讓它繼承RelativeLayout......各種佈局,再建立自己的屬性根據屬性來做出漂亮的控制元件;可以繼承已有的系統控制元件,重寫其構造方法或是覆蓋其onDraw方法來擴展出自己的個性化控制元件

        這些都是自定義控制元件開發最基礎的知識,接下來我們將逐步深入,開發更高階和更美觀的自定義控制元件,我也是在邊學邊寫,如有不足希望大家指出,一起討論,一起進步,也希望大家繼續關注我的文章。