1. 程式人生 > >Android開發進階——自定義View的使用及其原理探索

Android開發進階——自定義View的使用及其原理探索

  在Android開發中,系統提供給我們的UI控制元件是有限的,當我們需要使用一些特殊的控制元件的時候,只靠系統提供的控制元件,可能無法達到我們想要的效果,這時,就需要我們自定義一些控制元件,來完成我們想要的效果了。下面,我就來講講自定義控制元件的那些事。

  首先,我來講講Android的控制元件架構。Android的控制元件可以被分為兩類,分別是ViewGroup和View。在ViewGroup中可以包含多個View,並且管理他們。控制元件樹就是有這兩個部分組成的,控制元件樹的上層負責的是下層控制元件的繪製和測量以及互動。我們在Activity中使用的findViewById()方法,就是在控制元件樹中用深度遍歷的方法搜尋到對應的ID的。每一顆控制元件樹的頂部,都有個ViewParent物件,他是整棵樹的核心,負責排程所有的互動事件。在Activity中,我們是使用setContentView()來載入佈局的。每個Activity都是包含著一個Window物件的,在Android中通常是PhoneWindow,他將一個DecorView作為整個視窗的根View,將要顯示的內容呈現在window上。DecorView又分為兩個部分,一個是TitleView,一個是ContentView。ContentView是一個ID為content的Framelayout,佈局檔案就是設定在這裡面的。而TitleView就是我們看到topbar標題欄。這就是activity載入佈局檔案的過程了。

  接下來,我們開始講自定義控制元件的使用,下面講解使用的時候,會夾帶著一些原理的分析。自定義控制元件可以分為三種類型,一種是拓展谷歌提供的系統控制元件,來達到自己想要的效果。一種是將系統提供的控制元件組合在一起,作為一個組合控制元件來使用。還有一種是重新繪製測量一個全新的控制元件。

 

一、拓展谷歌提供的系統控制元件

  假如我們要對Textview控制元件進行拓展,首先我們要定義一個類繼承TextView,選擇性的重寫它的onDraw()、onMeasure()、onTouchEvent()等方法。其中,onDraw()負責對影象的繪製,onMeasure()負責測量位置,onTouchEvent()負責設定觸控的事件。當我們想直接繪製出有背景顏色的TextView時,可以在類中定義畫筆,在onDraw()進行繪製。程式碼如下:

 

Paint paint1=new Paint();  //定義畫筆
paint1.setColor(Color.YELLOW);
paint1.setStyle(Paint.Style.FILL);

   然後,通過以下的程式碼,就可以繪製出一個帶矩形框的Textview,但是需要在繪製完成後在呼叫父類的onDraw(),因為是在系統控制元件上拓展,所以,還要有其原來的功能。

@Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),paint1);//繪製矩形
        canvas.save();
        super.onDraw(canvas);
        canvas.restore();
    }

   

  使用canvas物件就可以進行繪圖了,對canvas的講解,我將會在下一篇部落格講解。

  然後,我們只需要在佈局檔案中加入自定義的控制元件即可,在佈局檔案中,自定義view的名字就是自定義控制元件類的包名加上類名,假設定義CustomTextview類繼承TextView,例子如下:

<com.example.myapplication.View.CustomTextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"></com.example.myapplication.View.Buttonbtn>

 

二、將系統提供的控制元件組合在一起

  除了拓展原有的控制元件以外,我們還可以將控制元件組合成一個新的控制元件使用。首先,我們先定義一個新的佈局檔案,並把Imageview和Textview加入,程式碼如下。

    <ImageView
        android:id="@+id/iv"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="2dp"
        android:text="訊息"
        android:textSize="13sp" />

  然後我們定義一個類繼承LinearLayout,在類的構造方法中對控制元件和佈局進行初始化。

public void init(Context context) {
        //指定線性佈局的顯示方式,垂直
        setOrientation(VERTICAL);
        //設定使用者期望的佈局方式
        LayoutParams mLayoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        setLayoutParams(mLayoutParams);
        setGravity(Gravity.CENTER);
        setPadding(4, 4, 4, 4);
       //設定其佈局檔案
        View mButtonbtnView = LayoutInflater.from(context).inflate(layout.botton_btn_view, this, true);
        mImageView = mButtonbtnView.findViewById(id.iv);
        mTextView = mButtonbtnView.findViewById(id.tv);
    }

  接下來,它的使用方法就和拓展控制元件的方法一樣了,直接在佈局檔案中,加入控制元件即可。

<com.example.myapplication.View.Buttonbtn
        android:layout_width="wrap_content"
        android:layout_height="match_parent"></com.example.myapplication.View.Buttonbtn>

 

三、重寫View來實現全新的控制元件

  當系統原生的控制元件無法滿足我們需求時,我們就可以定義一個新的控制元件來完成需要的功能。建立一個新的控制元件,需要繼承View類,其難點主要在於繪製控制元件和實現互動。在繼承View類時,我們還需要重寫它的onDraw(),onMeasure()、onTouchEvent()來實現繪製、測量和觸控事件。

 

  onDraw()繪製就是在canvas物件上呼叫其一系列方法進行繪圖,繪製控制元件的形狀。

 

  onMeasure()

  下面,我來講講onMeasure()。在繪製View之前,我們需要告訴系統我們需要畫一個多大的View以及他的位置,這就是onMeasure()進行的了。首先,我們來了解一下測量的三種模式:

  EXACTLY:精確值模式,在指定view具體數值的時候會用到。

  AT_MOST:最大值模式,將控制元件設定為"wrap_content"用到,它會根據子控制元件或者內容變化而變化。

  UNSPECIFIED:繪製控制元件想要多大就可以多大。

  根據以上三種模式,我們就可以在測量的時候判斷和使用了。首先,我們重寫一個view的onMeasure()方法。再通過使用MeasureSpec類獲得控制元件的測量模式。MeasureSpec使用的是位運算,其高2位為測量的模式,剩下的30位為測量的大小。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY) {

        } else if (widthMode == MeasureSpec.AT_MOST) {

        } else if (widthMode == MeasureSpec.UNSPECIFIED) {

        }

    }

   以上程式碼就是通過判斷測量模式來給定義控制元件的大小,這裡只是測量了控制元件的寬度,控制元件高度的測量也是類似的,就不在做詳解。

  前面說過,ViewGroup是用來管理控制元件的,當ViewGroup的大小為"wrap_content"時,它就會遍歷其所有子View,來獲得子View的大小,再來設定自身的大小。我們使用過的佈局,像RelativeLayout,LinearLayout都是繼承ViewGroup的,所以他們也是使用這種方法來獲得自己的大小的。

 

  onTouchEvent()

  onTouchEvent()就是我們所說的觸控事件,由於Android手機是觸屏的,所以我們自定義View在觸控式螢幕幕的時候,也需要有一定的處理來完成互動。當重寫onTouchEvent方法的時候,我們可以看到,需要傳入MotionEvent的物件。我們可以通過這個類來設定觸控的事件,也可以獲得觸控點的位置。我們可以通過getAction()來獲取觸控事件的行動,來判斷是否按下螢幕或者移動。在Android的座標系中,我們都知道Android的螢幕在豎屏的時候,以左上角的位置為原點,向右為x軸的正方向,向下為y軸的正方向,知道了這個後,我們就可以通過呼叫getX()和getY()方法可以獲取觸控點的座標,來完成一些互動操作。

 

public boolean onTouchEvent(MotionEvent event) {
        float x;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            {
                x=event.getX();
            }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

 

   以上就是自定義控制元件常用重寫的方法,通過了重寫這幾個方法,我們基本就可以實現一個簡易的自定義控制元件了。下面,我們來了解下控制元件的事件攔截機制的原理。

事件攔截機制分析

  我們前面講過,控制元件結構是樹形結構,一個ViewGroup中可能有多個ViewGroup或者View,那麼,觸控事件是怎麼準確的分配給每個View和ViewGroup的呢。我們假設有一個ViewGroupA,在他的裡面巢狀著ViewGroupB,而在ViewGroupB的裡面,又巢狀著一個View。當我們重寫ViewGroupA類的時候,就需要重寫裡面的這三個方法:

    dispatchTouchEvent()

    onInterceptTouchEvent()

    onTouchEvent()

  而在重寫View的時候,需要重寫兩個方法:

    dispatchTouchEvent()

    onTouchEvent()

 

  可以根據名字看出,ViewGroup中比View多了onInterceptTouchEvent()方法,這個方法就是事件攔截的核心。在每一個方法中Log一下,再點選View的時候,就會發現方法呼叫的順序:

 

    首先,呼叫了ViewGroupA類的dispatchTouchEvent()和onInterceptTouchEvent()。

    再呼叫了ViewGroupB類的dispatchTouchEvent()和onInterceptTouchEvent()。

    再到View的dispatchTouchEvent()方法。

 

  這個呼叫的順序就是事件傳遞的順序,而事件處理的順序則是:

 

    View的onTouchEvent()。

    ViewGroupB的onTouchEvent()。

    ViewGroupA的onTouchEvent()。

  

  由此,可以看出,事件的分發是由上層的ViewGroup釋出的,再逐層下發。而事件的處理,則是由下層的View處理後,再逐層上傳。前面也說過,onInterceptTouchEvent()是事件攔截的核心,那麼,只要設定它的返回值為true,就可以攔截事件,使其不再下發,而onTouchEvent()返回false,事件處理後就不會再上傳。事件的分發和攔截的流程就大致講解完成了。

  最後,這篇部落格是我看了《Android群英傳》後總結和歸納出的,希望能幫到正在學習自定義View的朋友,同時,有理解錯誤的地方,也歡迎大家指出。

 

 

&n