Android 入門(三)簡單自定義 View

知識點摘要:只須要會簡單的自定義 View、ViewGroup,沒必要了解 onMeasure、onLayout 和 onDraw 的過程。最後還提供了一個比較複雜的小米時鐘 View。android

自定義 View

自定義 View 其實很簡單,只須要繼承 View,而後重寫構造函數、 onMeasure 和 onDraw 方法便可,下面咱們就來學習學習他們的用法。git

重寫構造函數

在繼承 View 以後,編譯器提醒咱們必須實現構造函數,咱們通常實現以下兩種便可github

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

public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
}
複製代碼

重寫 onMeasure()

onMeasure 顧名思義就是測量當前 View 的大小,你可能會有疑惑,咱們不是在佈局 xml 中已經指定 View 的 layout_width 和 layout_height,這兩個屬性不就是 View 的高寬嗎?沒錯,這兩個屬性就是設置 View 的大小,你應該使用過 wrap_content 和 match_parent 這樣的值。咱們知道它們分別表明「包裹內容」和「填充父容器」,咱們還知道全部代碼最後經過編譯器都會編譯成機器碼,可是 cpu 確定不可能明白「包裹內容」和「填充父類」是什麼意思,因此咱們應該將它們轉化成具體的數值,如 100 px(100 個像素點,最後在屏幕根據像素點顯示)。canvas

囉嗦了半天,咱們仍是來看代碼更爲直觀,咱們若是想畫一個正方形,而且這個正方形的寬度須要填滿整個父容器,這個時候就須要重寫 onMeasure 來設置 View 的具體值。bash

這是重寫 onMeasure 的基礎代碼,有兩個參數 widthMeasureSpec 和 heightMeasureSpec,它們保存了 view 的長寬和「測量模式」信息。app

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼

長寬咱們懂,這個「測量模式」又是什麼東西?簡單來講「測量模式」包含三種 UNSPECIFIED、EXACTLY 和 AT_MOST。ide

UNSPECIFIED : 父容器對當前 view 沒有任何限制,能夠設置任意的尺寸。
EXACTLY : 當前讀到的尺寸就是 view 的尺寸。
AT_MOST : 當前讀到的尺寸是 view 可以設置的最大尺寸。函數

咱們在寫佈局界面的時候設置控件的大小經常使用的時三種狀況 match_parent 、wrap_content 和固定尺寸,三種測量模式與 match_parent 、wrap_content 和固定尺寸之間的關係以下,能夠看到 UNSPECIFIED 模式咱們基本上不會觸發。工具

match_parent --> EXACTLY。match_parent 就是要利用父 View 給咱們提供的全部剩餘空間,而父 View 剩餘空間是肯定的,也就是這個測量模式的整數裏面存放的尺寸。佈局

wrap_content --> AT_MOST。wrap_content 就是咱們想要將大小設置爲包裹咱們的 view 內容,那麼尺寸大小就是父 View 給咱們做爲參考的尺寸,只要不超過這個尺寸就能夠啦,具體尺寸就根據咱們的需求去設定。

固定尺寸(如 100dp)--> EXACTLY。用戶本身指定了尺寸大小,咱們就不用再去幹涉了,固然是以指定的大小爲主啦。

咱們弄懂了 onMeasure 方法的做用以及參數,接下來就設置正方形 view 的尺寸。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = getMySize(100, widthMeasureSpec);   //從 widthMeasureSpec 獲得寬度
    int height = getMySize(100, heightMeasureSpec);  //從 heightMeasureSpec 獲得高度
    if (width < height) {   // 取最小的那個值
        height = width;
    } else {
        width = height;
    }
    setMeasuredDimension(width, height);    //設置 view 具體的尺寸
}

private int getMySize(int defaultSize, int measureSpec) {
    int mySize = defaultSize;
    
    int mode = MeasureSpec.getMode(measureSpec);    //獲得測量模式
    int size = MeasureSpec.getSize(measureSpec);    //獲得建議尺寸

    switch (mode) {
        case MeasureSpec.UNSPECIFIED: {  //若是沒有指定大小,就設置爲默認值
            mySize = defaultSize;
            break;
        }
        case MeasureSpec.AT_MOST: {  //若是測量模式是最大值,就設置爲 size
            //咱們將大小取最大值,你也能夠取其餘值
            mySize = size;
            break;
        }
        case MeasureSpec.EXACTLY: {  //若是是固定的大小,那就不要去改變它
            mySize = size;
            break;
        }
        default:
            break;
    }
    return mySize;
}
複製代碼

重寫 onDraw()

咱們已經設置好了 view 的尺寸,也就是將畫板準備好。接下來須要在畫板上繪製圖形,咱們只須要重寫 onDraw 方法。參數 Canvas 是官方爲咱們提供的畫圖工具箱,咱們能夠利用它繪製各類各樣的圖形。

@Override
protected void onDraw(Canvas canvas) {
    //調用父 View 的 onDraw 函數,由於 View 這個類幫咱們實現了一些
    // 基本的而繪製功能,好比繪製背景顏色、背景圖片等
    super.onDraw(canvas);
    int r = getMeasuredHeight() / 2;
    //圓心的從橫座標
    int centerX = r;
    //圓心的從縱座標
    int centerY = r;
    
    Paint p = new Paint();  //畫筆
    p.setColor(Color.GREEN);    //設置畫筆的顏色
    //開始繪製
    canvas.drawCircle(centerX, centerY, r, p);
}
複製代碼

這樣一個簡單的正方形控件就完成了,咱們只須要在佈局 xml 中加入 CustomView 控件,就能看到效果

<?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-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    
    <com.wendraw.customviewexample.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#f00" />
</LinearLayout>
複製代碼

自定義 view
Custom View

自定義佈局屬性

不知道你在寫佈局文件的時候,有沒有想過這樣的問題,在佈局文件中設置控件的 layout_width 屬性的值以後,相應的 View 對象就會改變,這是怎麼實現的呢?咱們的 CustomView 可不能夠本身定義一個這樣的佈局文件上能夠用的屬性呢?

咱們在使用 view 時會發現,defaultSize 值被咱們寫死了,若是有別的開發者想使用咱們的 CustomView,可是默認大小想設置爲 200,就須要去修改源碼,這就破壞了代碼的封裝特性,有的人會說咱們能夠增長 getDefaultSize、setDefaultSize 方法,這個方法沒有問題,可是還不夠優雅,其實 Google 已經幫咱們優雅的實現了,就本節要講到的 AttributeSet。

咱們在重寫構造函數時,其實埋下了一個伏筆,爲何咱們要實現 public CustomView(Context context, AttributeSet attrs) 方法呢?AttributeSet 參數又有什麼做用呢?

首先咱們須要新建一個 res/values/attr.xml 文件,用來存放各類自定義的佈局屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- name 爲聲明的"屬性集合"名,能夠隨便取,可是最好是設置爲跟咱們的 View 同樣的名稱-->
    <declare-styleable name="CustomView">
        <!-- 聲明咱們的屬性,名稱爲 default_size,取值類型爲尺寸類型(dp,px等)-->
        <attr name="default_size" format="dimension" />
    </declare-styleable>
</resources>
複製代碼

接下來就能在佈局文件中使用這個屬性了

<?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-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    
    <com.wendraw.customviewexample.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#f00"
        app:default_size="100dp" />
</LinearLayout>
複製代碼

注意:須要在根標籤(LinearLayout)裏面設定命名空間,命名空間名稱能夠隨便取,好比 app,命名空間後面取得值是固定的:"schemas.android.com/apk/res-aut…"

咱們在佈局文件中使用剛剛定義的屬性還不會產生效果,由於咱們沒有將它解析到 CustomView 類中,解析的過程也很簡單,使用咱們前面介紹過帶 AttributeSet 參數的構造函數便可:

public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
    //第二個參數就是咱們在styles.xml文件中的<declare-styleable>標籤
    //即屬性集合的標籤,在R文件中名稱爲R.styleable+name
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    
    //第一個參數爲屬性集合裏面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱
    //第二個參數爲,若是沒有設置這個屬性,則設置的默認的值
    mDefaultSize = typedArray.getDimensionPixelSize(R.styleable.CustomView_default_size, 100);
    
    //最後將 TypedArray 回收
    typedArray.recycle();
}
複製代碼

全局變量 mDefaultSize 就是從佈局文件中的 default_size 屬性中解析來的值。

至此一個簡單的自定義 view 就建立成功了,跟咱們平時使用的 Buttom 控件是同樣的。而且咱們還能夠在 activity_main.xml 的 Design 界面的左上角看到咱們剛剛建立的控件:

Project Custom View
自定義控件

自定義 ViewGroup

咱們寫一個佈局文件用到的就是兩個元素,控件、佈局。控件在上一節已經講了,這一節咱們一塊兒來學習佈局 ViewGroup。佈局就是一個 View 容器,其做用就是決定控件的擺放位置。

其實官方給咱們提供的六個佈局已經夠用了,咱們學習自定義 view 主要是爲了在使用佈局的時候更好的理解其原理。既然是佈局就要知足幾個條件:

  1. 首先要知道子 view 的大小,才能根據子 View 才能設置 ViewGroup 的大小。
  2. 而後要知道佈局功能,也就是子 View 須要怎麼擺放,知道子 View 的尺寸和擺放方式才能肯定 ViewGroup 的大小。
  3. 最後就是將子 View 填到相應的位置。

接下來就經過一個簡單的案例學習一下,要求自定義一個將子 View 按垂直方向依此擺放的佈局。

咱們先建立一個 CustomViewLayout 類並繼承 ViewGroup。

實現 onMeasure,測量子 View 的大小,設置 ViewGroup 的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //將全部的子View進行測量,這會觸發每一個子View的onMeasure函數
    //注意要與measureChild區分,measureChild是對單個view進行測量
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int childCount = getChildCount();

    if (childCount == 0) {
        //若是沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
        setMeasuredDimension(0, 0);
    } else {
        //若是高寬都是包裹內容
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            //咱們就將高度設爲全部子 View 的高度相加,寬度設爲子 View 最大的。
            int width = getMaxChildWidth();
            int height = getTotalHeight();
            setMeasuredDimension(width, height);

        } else if (widthMode == MeasureSpec.AT_MOST) {    //只有寬度是包裹內容
            //高度設置爲 ViewGroup 的測量值,寬度爲子 View 的最大寬度
            setMeasuredDimension(getMaxChildWidth(), heightSize);

        } else if (heightMode == MeasureSpec.AT_MOST) {    //只有高度是包裹內容
            //高度設置爲 ViewGroup 的測量值,寬度爲子 View 的最大寬度
            setMeasuredDimension(widthSize, getTotalHeight());
        }
    }
}

/**
 * 獲取子 View 中寬度最大的值
 *
 * @return 子 View 中寬度最大的值
 */
private int getMaxChildWidth() {
    int childCount = getChildCount();
    int maxWidth = 0;
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);
        if (childView.getMeasuredWidth() > maxWidth) {
            maxWidth = childView.getMeasuredWidth();
        }
    }
    return maxWidth;
}

/**
 * 將全部子 View 的高度相加
 *
 * @return 全部子 View 的高度的總和
 */
private int getTotalHeight() {
    int childCount = getChildCount();
    int height = 0;
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);
        height += childView.getMeasuredHeight();
    }
    return height;
}
複製代碼

代碼已經註釋的比較詳細了,我就不贅述了。如今咱們解決了 ViewGroup 的大小問題,接下來就是解決子 View 的擺放問題。

實現 onLayout 擺放子 View

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int count = getChildCount();
    //記錄當前的高度位置
    int curHeight = t;
    //將子 View 逐個拜訪
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        int width = child.getMeasuredWidth();
        int height = child.getMeasuredHeight();
        //擺放子 View,參數分別是子 View 矩形區域的左、上、右、下邊
        child.layout(l, curHeight, l + width, curHeight + height);
        curHeight += height;
    }
}
複製代碼

代碼很簡單,用一個循環將子 View 按照順序一次執行 layout,設置子 View 的擺放位置。

至此一個簡單的自定義佈局咱們也完成了,咱們來測試一下:

<?xml version="1.0" encoding="utf-8"?>
<com.wendraw.customviewexample.CustomViewLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.wendraw.customviewexample.CustomViewLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#0f0">

        <com.wendraw.customviewexample.CustomView
            android:layout_width="300dp"
            android:layout_height="100dp"
            android:background="#f00"
            app:default_size="200dp" />

        <Button
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:text="xxxxxxxxxx" />

        <com.wendraw.customviewexample.CustomView
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="#f00"
            app:default_size="200dp" />
    </com.wendraw.customviewexample.CustomViewLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="100dp" />

    <com.wendraw.customviewexample.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#f00"
        app:default_size="100dp" />
        
</com.wendraw.customviewexample.CustomViewLayout>
複製代碼

能夠看到咱們建立的自定義的 View 和 ViewGroup 跟日常使用控件、佈局的方式同樣,咱們組合起來其效果以下:

demo
自定義 View 和 ViewGroup

深刻學習自定義 View

經過上面的學習你應該對自定義 View 和 ViewGroup 有必定的認識,甚至以爲還有一點點簡單,接下來你就能夠學習一下更復雜的 View。好比小米時鐘,你能夠先嚐試本身實現,不會的再參考個人代碼。

MiClock Demo
MiClock Demo

結束

在入門階段咱們不須要去詳細 onMeasure、onLayout 和 onDraw 的過程,只須要會簡單的自定義 View、ViewGroup 便可,切記只見樹木不見森林。最後還提供了一個比較複雜小米時針 View, 很是值得本身動手學習。

全部的代碼都上傳到了 GayHub CustomViewExample,歡迎拍磚。

參考

自定義View,有這一篇就夠了

Github-MiClockView

相關文章
相關標籤/搜索