我的CSDN博客同步發(fā)布:自定義View,有這一篇就夠了
為了掃除學(xué)習(xí)中的盲點,盡可能多的覆蓋Android知識的邊邊角角,決定對自定義View做一個稍微全面一點的使用方法總結(jié),在內(nèi)容上面并沒有什么獨特的地方,其他大神們的博客上面基本上都有講這方面的內(nèi)容,如果你對自定義View很熟了,那么就不用往下看啦~。如果對自定義View不是很熟,或者說很多內(nèi)容忘記了想復(fù)習(xí)一下,更或者說是從來沒用過,歡迎跟我一起重溫這方面的知識,或許我的博文更符合你的胃口呢(*__*) 嘻嘻……
1.自定義View
首先我們要明白,為什么要自定義View?主要是Android系統(tǒng)內(nèi)置的View無法實現(xiàn)我們的需求,我們需要針對我們的業(yè)務(wù)需求定制我們想要的View。自定義View我們大部分時候只需重寫兩個函數(shù):onMeasure()、onDraw()。onMeasure負(fù)責(zé)對當(dāng)前View的尺寸進(jìn)行測量,onDraw負(fù)責(zé)把當(dāng)前這個View繪制出來。當(dāng)然了,你還得寫至少寫2個構(gòu)造函數(shù):
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
1.1.onMeasure
我們自定義的View,首先得要測量寬高尺寸。為什么要測量寬高尺寸?我在剛學(xué)自定義View的時候非常無法理解!因為我當(dāng)時覺得,我在xml文件中已經(jīng)指定好了寬高尺寸了,我自定義View中有必要再次獲取寬高并設(shè)置寬高嗎?既然我自定義的View是繼承自View類,google團(tuán)隊直接在View類中直接把xml設(shè)置的寬高獲取,并且設(shè)置進(jìn)去不就好了嗎?那google為啥讓我們做這樣的“重復(fù)工作”呢?客官別急,馬上給您上茶~
在學(xué)習(xí)Android的時候,我們就知道,在xml布局文件中,我們的layout_width
和layout_height
參數(shù)可以不用寫具體的尺寸,而是wrap_content
或者是match_parent
。其意思我們都知道,就是將尺寸設(shè)置為“包住內(nèi)容”和“填充父布局給我們的所有空間”。這兩個設(shè)置并沒有指定真正的大小,可是我們繪制到屏幕上的View必須是要有具體的寬高的,正是因為這個原因,我們必須自己去處理和設(shè)置尺寸。當(dāng)然了,View類給了默認(rèn)的處理,但是如果View類的默認(rèn)處理不滿足我們的要求,我們就得重寫onMeasure函數(shù)啦。這里舉個例子,比如我們希望我們的View是個正方形,如果在xml中指定寬高為`wrap_content`,如果使用View類提供的measure處理方式,顯然無法滿足我們的需求。
先看看onMeasure函數(shù)原型:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
參數(shù)中的widthMeasureSpec
和heightMeasureSpec
是個什么鬼?看起來很像width和height,沒錯,這兩個參數(shù)就是包含寬和高的信息。什么?包含?難道還要其他信息?是的!它還包含測量模式,也就是說,一個int整數(shù),里面放了測量模式和尺寸大小。那么一個數(shù)怎么放兩個信息呢?我們知道,我們在設(shè)置寬高時有3個選擇:wrap_content
、match_parent
以及指定固定尺寸
,而測量模式也有3種:UNSPECIFIED
,EXACTLY
,AT_MOST
,當(dāng)然,他們并不是一一對應(yīng)關(guān)系哈,這三種模式后面我會詳細(xì)介紹,但測量模式無非就是這3種情況,而如果使用二進(jìn)制,我們只需要使用2個bit就可以做到,因為2個bit取值范圍是[0,3]里面可以存放4個數(shù)足夠我們用了。那么Google是怎么把一個int同時放測量模式和尺寸信息呢?我們知道int型數(shù)據(jù)占用32個bit,而google實現(xiàn)的是,將int數(shù)據(jù)的前面2個bit用于區(qū)分不同的布局模式,后面30個bit存放的是尺寸的數(shù)據(jù)。
那我們怎么從int數(shù)據(jù)中提取測量模式和尺寸呢?放心,不用你每次都要寫一次移位<<
和取且&
操作,Android內(nèi)置類MeasureSpec幫我們寫好啦~,我們只需按照下面方法就可以拿到啦:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
愛思考的你肯定會問,既然我們能通過widthMeasureSpec拿到寬度尺寸大小,那我們還要測量模式干嘛?測量模式會不會是多余的?請注意:這里的的尺寸大小并不是最終我們的View的尺寸大小,而是父View提供的參考大小。我們看看測量模式,測量模式是干啥用的呢?
測量模式 | 表示意思 |
---|---|
UNSPECIFIED | 父容器沒有對當(dāng)前View有任何限制,當(dāng)前View可以任意取尺寸 |
EXACTLY | 當(dāng)前的尺寸就是當(dāng)前View應(yīng)該取的尺寸 |
AT_MOST | 當(dāng)前尺寸是當(dāng)前View能取的最大尺寸 |
而上面的測量模式跟我們的布局時的wrap_content
、match_parent
以及寫成固定的尺寸有什么對應(yīng)關(guān)系呢?
match_parent
--->EXACTLY。怎么理解呢?match_parent
就是要利用父View給我們提供的所有剩余空間,而父View剩余空間是確定的,也就是這個測量模式的整數(shù)里面存放的尺寸。
wrap_content
--->AT_MOST。怎么理解:就是我們想要將大小設(shè)置為包裹我們的view內(nèi)容,那么尺寸大小就是父View給我們作為參考的尺寸,只要不超過這個尺寸就可以啦,具體尺寸就根據(jù)我們的需求去設(shè)定。
固定尺寸(如100dp)
--->EXACTLY。用戶自己指定了尺寸大小,我們就不用再去干涉了,當(dāng)然是以指定的大小為主啦。
1.2.動手重寫onMeasure函數(shù)
上面講了太多理論,我們實際操作一下吧,感受一下onMeasure的使用,假設(shè)我們要實現(xiàn)這樣一個效果:將當(dāng)前的View以正方形的形式顯示,即要寬高相等,并且默認(rèn)的寬高值為100像素。就可以這些編寫:
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: {//如果沒有指定大小,就設(shè)置為默認(rèn)大小
mySize = defaultSize;
break;
}
case MeasureSpec.AT_MOST: {//如果測量模式是最大取值為size
//我們將大小取最大值,你也可以取其他值
mySize = size;
break;
}
case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改變它
mySize = size;
break;
}
}
return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(100, widthMeasureSpec);
int height = getMySize(100, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
我們設(shè)置一下布局
<com.hc.studyview.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#ff0000" />
看看使用了我們自己定義的onMeasure函數(shù)后的效果:
而如果我們不重寫onMeasure,效果則是如下:
1.3.重寫onDraw
上面我們學(xué)會了自定義尺寸大小,那么尺寸我們會設(shè)定了,接下來就是把我們想要的效果畫出來吧~繪制我們想要的效果很簡單,直接在畫板Canvas對象上繪制就好啦,過于簡單,我們以一個簡單的例子去學(xué)習(xí):假設(shè)我們需要實現(xiàn)的是,我們的View顯示一個圓形,我們在上面已經(jīng)實現(xiàn)了寬高尺寸相等的基礎(chǔ)上,繼續(xù)往下做:
@Override
protected void onDraw(Canvas canvas) {
//調(diào)用父View的onDraw函數(shù),因為View這個類幫我們實現(xiàn)了一些
// 基本的而繪制功能,比如繪制背景顏色、背景圖片等
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我們已經(jīng)將寬高設(shè)置相等了
//圓心的橫坐標(biāo)為當(dāng)前的View的左邊起始位置+半徑
int centerX = getLeft() + r;
//圓心的縱坐標(biāo)為當(dāng)前的View的頂部起始位置+半徑
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.GREEN);
//開始繪制
canvas.drawCircle(centerX, centerY, r, paint);
}
1.4.自定義布局屬性
如果有些屬性我們希望由用戶指定,只有當(dāng)用戶不指定的時候才用我們硬編碼的值,比如上面的默認(rèn)尺寸,我們想要由用戶自己在布局文件里面指定該怎么做呢?那當(dāng)然是通我們自定屬性,讓用戶用我們定義的屬性啦~
首先我們需要在res/values/styles.xml
文件(如果沒有請自己新建)里面聲明一個我們自定義的屬性:
<resources>
<!--name為聲明的"屬性集合"名,可以隨便取,但是最好是設(shè)置為跟我們的View一樣的名稱-->
<declare-styleable name="MyView">
<!--聲明我們的屬性,名稱為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:hc="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hc.studyview.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
hc:default_size="100dp" />
</LinearLayout>
注意:需要在根標(biāo)簽(LinearLayout)里面設(shè)定命名空間,命名空間名稱可以隨便取,比如hc
,命名空間后面取得值是固定的:"http://schemas.android.com/apk/res-auto"
最后就是在我們的自定義的View里面把我們自定義的屬性的值取出來,在構(gòu)造函數(shù)中,還記得有個AttributeSet屬性嗎?就是靠它幫我們把布局里面的屬性取出來:
private int defalutSize;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//第二個參數(shù)就是我們在styles.xml文件中的<declare-styleable>標(biāo)簽
//即屬性集合的標(biāo)簽,在R文件中名稱為R.styleable+name
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
//第一個參數(shù)為屬性集合里面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱
//第二個參數(shù)為,如果沒有設(shè)置這個屬性,則設(shè)置的默認(rèn)的值
defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
//最后記得將TypedArray對象回收
a.recycle();
}
最后,把MyView的完整代碼附上:
package com.hc.studyview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* Package com.hc.studyview
* Created by HuaChao on 2016/6/3.
*/
public class MyView extends View {
private int defalutSize;
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//第二個參數(shù)就是我們在styles.xml文件中的<declare-styleable>標(biāo)簽
//即屬性集合的標(biāo)簽,在R文件中名稱為R.styleable+name
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
//第一個參數(shù)為屬性集合里面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱
//第二個參數(shù)為,如果沒有設(shè)置這個屬性,則設(shè)置的默認(rèn)的值
defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
//最后記得將TypedArray對象回收
a.recycle();
}
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: {//如果沒有指定大小,就設(shè)置為默認(rèn)大小
mySize = defaultSize;
break;
}
case MeasureSpec.AT_MOST: {//如果測量模式是最大取值為size
//我們將大小取最大值,你也可以取其他值
mySize = size;
break;
}
case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改變它
mySize = size;
break;
}
}
return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(defalutSize, widthMeasureSpec);
int height = getMySize(defalutSize, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
//調(diào)用父View的onDraw函數(shù),因為View這個類幫我們實現(xiàn)了一些
// 基本的而繪制功能,比如繪制背景顏色、背景圖片等
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我們已經(jīng)將寬高設(shè)置相等了
//圓心的橫坐標(biāo)為當(dāng)前的View的左邊起始位置+半徑
int centerX = getLeft() + r;
//圓心的縱坐標(biāo)為當(dāng)前的View的頂部起始位置+半徑
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.GREEN);
//開始繪制
canvas.drawCircle(centerX, centerY, r, paint);
}
}
2 自定義ViewGroup
自定義View的過程很簡單,就那幾步,可自定義ViewGroup可就沒那么簡單啦~,因為它不僅要管好自己的,還要兼顧它的子View。我們都知道ViewGroup是個View容器,它裝納child View并且負(fù)責(zé)把child View放入指定的位置。我們假象一下,如果是讓你負(fù)責(zé)設(shè)計ViewGroup,你會怎么去設(shè)計呢?
1.首先,我們得知道各個子View的大小吧,只有先知道子View的大小,我們才知道當(dāng)前的ViewGroup該設(shè)置為多大去容納它們。
2.根據(jù)子View的大小,以及我們的ViewGroup要實現(xiàn)的功能,決定出ViewGroup的大小
3.ViewGroup和子View的大小算出來了之后,接下來就是去擺放了吧,具體怎么去擺放呢?這得根據(jù)你定制的需求去擺放了,比如,你想讓子View按照垂直順序一個挨著一個放,或者是按照先后順序一個疊一個去放,這是你自己決定的。
4.已經(jīng)知道怎么去擺放還不行啊,決定了怎么擺放就是相當(dāng)于把已有的空間"分割"成大大小小的空間,每個空間對應(yīng)一個子View,我們接下來就是把子View對號入座了,把它們放進(jìn)它們該放的地方去。
現(xiàn)在就完成了ViewGroup的設(shè)計了,我們來個具體的案例:將子View按從上到下垂直順序一個挨著一個擺放,即模仿實現(xiàn)LinearLayout的垂直布局。
首先重寫onMeasure,實現(xiàn)測量子View大小以及設(shè)定ViewGroup的大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//將所有的子View進(jìn)行測量,這會觸發(fā)每個子View的onMeasure函數(shù)
//注意要與measureChild區(qū)分,measureChild是對單個view進(jìn)行測量
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,當(dāng)前ViewGroup沒有存在的意義,不用占用空間
setMeasuredDimension(0, 0);
} else {
//如果寬高都是包裹內(nèi)容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我們將高度設(shè)置為所有子View的高度相加,寬度設(shè)為子View中最大的寬度
int height = getTotleHeight();
int width = getMaxChildWidth();
setMeasuredDimension(width, height);
} else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹內(nèi)容
//寬度設(shè)置為ViewGroup自己的測量寬度,高度設(shè)置為所有子View的高度總和
setMeasuredDimension(widthSize, getTotleHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {//如果只有寬度是包裹內(nèi)容
//寬度設(shè)置為子View中寬度最大的值,高度設(shè)置為ViewGroup自己的測量值
setMeasuredDimension(getMaxChildWidth(), heightSize);
}
}
}
/***
* 獲取子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的高度相加
**/
private int getTotleHeight() {
int childCount = getChildCount();
int height = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
height += childView.getMeasuredHeight();
}
return height;
}
代碼中的注釋我已經(jīng)寫得很詳細(xì),不再對每一行代碼進(jìn)行講解。上面的onMeasure將子View測量好了,以及把自己的尺寸也設(shè)置好了,接下來我們?nèi)[放子View吧~
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
//記錄當(dāng)前的高度位置
int curHeight = t;
//將子View逐個擺放
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int height = child.getMeasuredHeight();
int width = child.getMeasuredWidth();
//擺放子View,參數(shù)分別是子View矩形區(qū)域的左、上、右、下邊
child.layout(l, curHeight, l + width, curHeight + height);
curHeight += height;
}
}
我們測試一下,將我們自定義的ViewGroup里面放3個Button ,將這3個Button的寬度設(shè)置不一樣,把我們的ViewGroup的寬高都設(shè)置為包裹內(nèi)容wrap_content
,為了看的效果明顯,我們給ViewGroup加個背景:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hc.studyview.MyViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff9900">
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="50dp"
android:layout_height="wrap_content"
android:text="btn" />
</com.hc.studyview.MyViewGroup>
</LinearLayout>
看看最后的效果吧~
是不是很激動我們自己也可以實現(xiàn)LinearLayout的效果啦~~~
最后附上MyViewGroup的完整源碼:
package com.hc.studyview;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* Package com.hc.studyview
* Created by HuaChao on 2016/6/3.
*/
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
/***
* 獲取子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的高度相加
**/
private int getTotleHeight() {
int childCount = getChildCount();
int height = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
height += childView.getMeasuredHeight();
}
return height;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//將所有的子View進(jìn)行測量,這會觸發(fā)每個子View的onMeasure函數(shù)
//注意要與measureChild區(qū)分,measureChild是對單個view進(jìn)行測量
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,當(dāng)前ViewGroup沒有存在的意義,不用占用空間
setMeasuredDimension(0, 0);
} else {
//如果寬高都是包裹內(nèi)容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我們將高度設(shè)置為所有子View的高度相加,寬度設(shè)為子View中最大的寬度
int height = getTotleHeight();
int width = getMaxChildWidth();
setMeasuredDimension(width, height);
} else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹內(nèi)容
//寬度設(shè)置為ViewGroup自己的測量寬度,高度設(shè)置為所有子View的高度總和
setMeasuredDimension(widthSize, getTotleHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {//如果只有寬度是包裹內(nèi)容
//寬度設(shè)置為子View中寬度最大的值,高度設(shè)置為ViewGroup自己的測量值
setMeasuredDimension(getMaxChildWidth(), heightSize);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
//記錄當(dāng)前的高度位置
int curHeight = t;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int height = child.getMeasuredHeight();
int width = child.getMeasuredWidth();
child.layout(l, curHeight, l + width, curHeight + height);
curHeight += height;
}
}
}
好啦~自定義View的學(xué)習(xí)到此結(jié)束,是不是發(fā)現(xiàn)自定義View如此簡單呢?