Android 自定義 View 基本規範

本人只是Android小菜一個,寫技術文檔只是爲了總結本身在最近學習到的知識,歷來不敢爲人師,若是裏面有些不正確的地方請你們盡情指出,謝謝!java

1. 概述

在進行Android應用開發時,能夠選擇系統提供的各式各樣的控件,但有時原生控件在功能和效果上並不能知足需求,這時就要求必須根據實際需求來定義新的控件,能夠經過繼承View,也能夠繼承某些已經存在的原生控件,來實現自定義控件。本文將選擇直接繼承View來實現一個最簡單的控件。android

自定義控件包含了Android中和View相關的不少知識,學習自定義控件也能幫組學習和理解相關知識。canvas

要想自定義出功能強大效果酷炫的控件,要求必須對View體系有深刻的理解,在這點我還差的不少,因此本文並不能教你們怎樣去實現這樣的控件。本文只是從自定義View的基本規範方面,跟你們探討下在自定義一個控件的過程當中,有哪些方面須要注意的,或者說有哪些功能是須要實現的,主要包括:控件屬性控件測量控件繪製控件交互app

2. 控件屬性

當咱們在xml中定義控件的時候,確定須要對控件具備的某些屬性進行設置,例如寬高背景顏色文本等等,下面是在使用 TextView的一個示例:ide

<TextView android:id="@+id/main_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#FF0000" android:text="Hello World!" />
複製代碼

在自定義控件的時候,爲了可以讓用戶靈活定義控件的某些特性,也須要經過屬性的方法把用戶指定的值傳入控件,而不是在控件內部使用預約義的值,這也就要求在自定義控件的時候使用到自定義屬性。函數

2.1 定義屬性

自定義屬性須要在res/values/attrs.xml裏面定義,若是這個文件不存就本身建立一個。結合一個例子進行介紹:學習

<declare-styleable name="custom_view">
        <attr name="default_color" format="color"></attr>
    </declare-styleable>
複製代碼

declare-styleable name="custom_view"指定了自定義屬性集合的name信息,這個值能夠是任意值,但通常爲了方面使用都是直接使用自定義控件的名字。測試

<attr name="default_color" format="color"></attr>指定了自定義屬性集合裏的具體屬性和該屬性對應的類型,本例中使用的是color類型,代表這個屬性須要的是一個顏色值,可以支持的format類型以下表:this

類型 含義 取值
boolean 布爾類型 只能是truefalse
string 字符串類型 任意字符串值
integer 整數類型 只能是整數
float 浮點數類型 只能是浮點和整型
fraction 百分比類型 只能以%結尾
color 顏色類型 能夠是顏色值或者指向color的資源
dimension 尺寸類型 能夠是具體尺寸值或指向尺寸的資源
reference 引用類型 只能是指向某一資源的ID
enum 枚舉類型 只能是定義的枚舉值
flag 位標誌類型 只能是定義的位值

在這裏只定義了一個簡單的color類型的屬性,其餘類型的屬性你們可自行定義,方法是相似的。spa

2.2 使用屬性

在定義了屬性後,能夠直接在xml使用這些屬性,使用方法和原生控件屬性同樣,只需根據不一樣類型設置值便可。在上面定義一個屬性default_color,如今就能夠在xml裏使用了:

<com.test.androidtest.CustomView android:id="@+id/custom_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:default_color="#ffff00"/>
複製代碼

須要注意的是,在這裏使用了新的命名空間app,其聲明是xmlns:app="http://schemas.android.com/apk/res-auto",若是你們使用的Android Studio,這個命名空間是自動添加的,無須自行處理。

xml使用了自定義屬性後,在建立這個控件的時候,就會把這些屬性傳入控件,在控件內部就能夠獲取並使用到該屬性值了。

// 在代碼裏經過 new 方式建立控件實例時使用
    public CustomView(Context context) {
        super(context);
    }

    // 在 xml 定義控件時使用,會獲取到定義的屬性
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 獲取定義的屬性集合
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.custom_view);
        // 獲取特定的屬性值
        if (array != null) {
            default_color = array.getColor(R.styleable.custom_view_default_color, -1);
        }
    }
複製代碼

上述代碼演示瞭如何在控件內部獲取自定義屬性,在成功獲取到屬性值後就能夠利用該值進行後續的控件繪製工做了。

須要注意的是在自定義控件是須要實現兩個不一樣的構造函數,分別對應於在javaxml的使用場景。

2.3 修改屬性

在前面已經講了如何定義和在控件內部獲取屬性,可是咱們知道有時控件屬性是須要根據不一樣的場景進行修改的,而在xml只能指定屬性的初始值,沒法進行不斷的修改。這就要求必須針對有些屬性提供取值器設值器,也就是常說的gettersetter,這裏之因此說是「有些屬性」,是由於並非全部屬性都須要支持動態修改的。

仍是針對前面定義的default_color屬性,如今對其設置取值器設值器:

public int getColor() {
        return default_color;
    }

    public void setColor(int color) {
        default_color = color;
        // 調用 onDraw,從新刷新控件.
        invalidate();
    }
複製代碼

取值器比較簡單,只要返回當前屬性值就能夠了。設值器除了要更新當前屬性值外,更重要的是,在更新完當前屬性值外,要對當前的控件進行第二次的繪製,以更新控件狀態,這裏直接調用invalidate(),它會把當前view標誌爲DIRTY,在下一幀繪製時調用控件的onDraw()方法完成對控件的更新。設置了屬性的gettersetter後,就能夠在使用控件的時候,動態獲取和修改屬性值了。

3. 控件測量

測量的目的是要肯定控件在顯示的時候具體的顯示尺寸,你們可能會奇怪:不是在xml已經指定了控件大小了嗎?爲何還要再測量一次呢?這是由於在xml指定控件大小的時候有不一樣的方式,每種方式最終致使分配給控件的尺寸也不同。

指定尺寸方式 含義
wrap_content 根據控件具體內容分配尺寸
match_parent 根據父控件剩餘大小給控件分配尺寸
具體數值 根據給定的數值進行分配控件尺寸

爲了可以測了控件,須要實現onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,先看下View中該方法聲明:

/** * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製代碼

這裏提到:onMeasure是用來決定控件的寬高信息的,爲了可以提供更準確和高效的控件測量,子類最好要重寫這個方法,因此自定義控件最好也要實現這個方法。

這裏的參數widthMeasureSpecheightMeasureSpec表明是什麼意思?是否是就是控件的寬高呢?固然不是,若是它們表示的就是控件寬高就不須要咱們繼續測量了。widthMeasureSpecheightMeasureSpec裏面都包含了兩個信息:sizemode,其中size表示的是父控件告訴子控件的建議寬高,mode表示當前的測量模式,具體有AT_MOST,EXACTLYUNSPECIFIED,其含義以下:

測量模式 尺寸模式 含義
AT_MOST wrap_content 父控件提供一個最大值,子控件不要超過父控件提供的尺寸大小。
EXACTLY match_parent或者具體值 父控件提供一個確切值,子控件能夠直接使用這個尺寸來設置大小。
UNSPECIFIED 暫無 父控件不提供,子控件能夠任意設置大小。

從上面的表格能夠看到:UNSPECIFIED通常是遇不到的,而AT_MOSTEXACTLY都會提供一個建議值,能夠根據這個值和測試模式來肯定子控件大小。

本文中的自定義控件的onMeasure以下:

@Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 使用寬高中的最小值把寬高設置爲等值,由於控件的最終目的是畫一個圓。
        int dimension = Math.min(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
        // 設置最終的寬高信息,若是少了這步,獲得的寬高將沒法應用到控件中。
        setMeasuredDimension(dimension, dimension);
    }

    private int getSize(int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        // EXACTLY 和 AT_MOST 直接使用父控件提供的寬高信息。
        switch (mode) {
            case MeasureSpec.EXACTLY:
            case MeasureSpec.AT_MOST:
                return size;
            default:
                // UNSPECIFIED 返回預約義的寬高信息,通常不會遇到。
                return mMeasureWidthHeight;
        }
    }
複製代碼

在本文的自定義控件中,最終的目的是要顯示一個圓形,在onMeasure裏設置了等值寬高,而在獲取寬高時針對AT_MOSTEXACTLY兩種狀況都直接使用了父控制傳遞過來的尺寸。固然這只是一種最簡單的狀況,當要自定義高能複雜的控件時,寬高的肯定須要結合的因素會更多,計算也會更復雜。

4. 控件繪製

測量控件後就能夠知道控件的最終寬高信息,這時須要作的就是進行實際的繪製,只有經過繪製,控件才能真正地顯示出來。繪製控件須要實現onDraw(Canvas canvas)方法,和onMeasure同樣,先看下在View中的聲明:

/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */
    protected void onDraw(Canvas canvas) {
    }
複製代碼

能夠發現:View並無實現onDraw,這是由於View 是全部控件的父類,但其自己並非一個能夠直接顯示的控件,這就要求全部須要顯示的控件都必須實現這個方法,它的參數是Canvas類,就是常說的畫布。爲了顯示控件,咱們須要作的就是用PaintCanvas上把須要顯示的圖像畫出來,正如咱們在電腦上常常在畫圖軟件上畫圖同樣。

如今看下本例中自定義控件的onDraw(Canvas canvas)的實現:

@Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 初始化畫筆,這個對象須要在控件初始化時初始化,這裏正常不會走到。
        if (mPaint == null) {
            mPaint = new Paint();
        }
        // 設置畫筆的顏色。
        mPaint.setColor(default_color);

        // 在畫布上畫出一個圓形。
        int radius = getMeasuredWidth() / 2;
        canvas.drawCircle(getLeft() + radius, getTop() +radius, radius, mPaint);
    }
複製代碼

上面的示例代碼只是實現一個根據用戶傳入的顏色來進行畫圓功能,其效果以下:

示例效果

Canvas除了畫圓,還能夠畫出更多更復雜的圖形,Paint也能夠有更多的控制,其你們自行查閱相關API

5. 控件交互

經過上面的幾個過程,已經能在界面上顯示自定義控件了,但顯示不是最終的目的,真正的目的仍是但願能與控件進行交互,最重要的是可以響應touch事件,接下來就經過實現一個簡單的隨手指移動功能:

private int mLastX;
    private int mLastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                //從新放置新的位置
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                break;
            default:
                break;
        }
        return true;
    }
複製代碼

此次對onTouchEvent的重寫能夠實現讓控件隨着手指會移動,固然這裏只是一個簡單演示,還存在一些問題,好比控件會被移出屏幕以外,這是由於在移動時並無判斷當前控件的位置,把這個條件加上就能夠保證控件只在界面以內移動。

6. 總結

本文經過一個簡單的自定義圓形的例子,大體講解了自定義View的基本規範,其中包括屬性、測量、繪製、交互,你們能夠把它當作自定義控件的入門知識,但相信在瞭解了這些基本規範後,再加上勤奮的練習,之後也能定義出功能複雜效果炫酷的控件,一塊兒加油!

相關文章
相關標籤/搜索