知識點摘要:只須要會簡單的自定義 View、ViewGroup,沒必要了解 onMeasure、onLayout 和 onDraw 的過程。最後還提供了一個比較複雜的小米時鐘 View。android
自定義 View 其實很簡單,只須要繼承 View,而後重寫構造函數、 onMeasure 和 onDraw 方法便可,下面咱們就來學習學習他們的用法。git
在繼承 View 以後,編譯器提醒咱們必須實現構造函數,咱們通常實現以下兩種便可github
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
複製代碼
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;
}
複製代碼
咱們已經設置好了 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>
複製代碼
不知道你在寫佈局文件的時候,有沒有想過這樣的問題,在佈局文件中設置控件的 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 界面的左上角看到咱們剛剛建立的控件:
咱們寫一個佈局文件用到的就是兩個元素,控件、佈局。控件在上一節已經講了,這一節咱們一塊兒來學習佈局 ViewGroup。佈局就是一個 View 容器,其做用就是決定控件的擺放位置。
其實官方給咱們提供的六個佈局已經夠用了,咱們學習自定義 view 主要是爲了在使用佈局的時候更好的理解其原理。既然是佈局就要知足幾個條件:
接下來就經過一個簡單的案例學習一下,要求自定義一個將子 View 按垂直方向依此擺放的佈局。
咱們先建立一個 CustomViewLayout 類並繼承 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 的擺放問題。
@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 跟日常使用控件、佈局的方式同樣,咱們組合起來其效果以下:
經過上面的學習你應該對自定義 View 和 ViewGroup 有必定的認識,甚至以爲還有一點點簡單,接下來你就能夠學習一下更復雜的 View。好比小米時鐘,你能夠先嚐試本身實現,不會的再參考個人代碼。
在入門階段咱們不須要去詳細 onMeasure、onLayout 和 onDraw 的過程,只須要會簡單的自定義 View、ViewGroup 便可,切記只見樹木不見森林。最後還提供了一個比較複雜小米時針 View, 很是值得本身動手學習。
全部的代碼都上傳到了 GayHub CustomViewExample,歡迎拍磚。