以下內(nèi)容整理自互聯(lián)網(wǎng),僅用于個(gè)人學(xué)習(xí)
如何自定義控件
- 自定義屬性的聲明和獲取
- 分析需要的自定義屬性
- 在res/values/attrs.xml定義聲明
- 在layout文件中進(jìn)行使用
- 在View的構(gòu)造方法中進(jìn)行獲取
- 測量onMeasure
- 布局onLayout(ViewGroup)
- 繪制onDraw
- onTouchEvent
- onInterceptTouchEvent(ViewGroup)
- 狀態(tài)的恢復(fù)與保存
自定義View大部分時(shí)候只需重寫兩個(gè)函數(shù):onMeasure()、onDraw()。
1. 自定義view
onMeasure負(fù)責(zé)對當(dāng)前View的尺寸進(jìn)行測量,onDraw負(fù)責(zé)把當(dāng)前這個(gè)View繪制出來。最后,至少寫2個(gè)構(gòu)造函數(shù)。
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
onMeasure()
我們自定義的View,首先得要測量寬高尺寸。
onMeasure函數(shù)原型:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
參數(shù)中的widthMeasureSpec和heightMeasureSpec包含寬和高的信息,這些信息包括測量模式和尺寸大小。
一個(gè)int整數(shù)如何存放兩種信息?
首先要了解測量模式,測量模式有三種:UNSPECIFIED,EXACTLY,AT_MOST。二進(jìn)制只需要 2bit 就能表示,而int型整數(shù)有 32bit ,Google的做法是,將int數(shù)據(jù)的前面2個(gè)bit用于區(qū)分不同的布局模式,后面30個(gè)bit存放的是尺寸的數(shù)據(jù)。
Android通過內(nèi)置類MeasureSpec可以獲取int中的測量模式和尺寸大小。
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
測量模式是用來做什么的?
測量模式 | 表示意思 |
---|---|
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)系
- warp_content 對應(yīng) AT_MOST:warp_content意味著將大小設(shè)置為足夠包裹內(nèi)部view內(nèi)容即可,就是我們想要將大小設(shè)置為包裹內(nèi)容,那么尺寸大小就是父View給我們作為參考的尺寸,只要不超過這個(gè)尺寸就可以,具體尺寸就根據(jù)我們的需求去設(shè)定。
- match_parent 對應(yīng) EXACTLY:match_parent就是要利用父View給我們提供的所有剩余空間,而父View剩余空間是確定的,也就是這個(gè)測量模式的整數(shù)里面存放的尺寸。
- 固定尺寸 對應(yīng) EXACTLY:用戶自己指定了尺寸大小,我們就不用再去干涉了,當(dāng)然是以指定的大小為主。
重寫onMeasure
自定義一個(gè)默認(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);
}
接著就可以在xml中使用并設(shè)置布局
<com.ljr.example.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#ff0000" />
如果使用自定義的view,則會(huì)在左上角顯示一個(gè)正方形,如果不使用自定義view,則會(huì)在最上方顯示一個(gè)高為100dp,長度充滿父容器的長方形。
重寫onDraw
學(xué)會(huì)了設(shè)置尺寸,接下來就是把效果畫出來。在上面的onMeasure基礎(chǔ)上,重寫onDraw,實(shí)現(xiàn)一個(gè)顯示圓形的例子。
@Override
protected void onDraw(Canvas canvas) {
//調(diào)用父View的onDraw函數(shù),因?yàn)閂iew這個(gè)類幫我們實(shí)現(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);
}
顯示效果為在左上角顯示一個(gè)圓形。
自定義布局屬性
如果有些屬性我們希望由用戶指定,只有當(dāng)用戶不指定的時(shí)候才用我們硬編碼的值,比如上面的默認(rèn)尺寸。我們可以通過自定義自己的屬性,讓用戶使用我們定義的屬性。
首先我們需要在res/values/attrs.xml文件(如果沒有請自己新建)里面聲明一個(gè)我們自定義的屬性:
<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:ljr="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.ljr.example.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
ljr:default_size="100dp" />
</LinearLayout>
我們需要在根標(biāo)簽(LinearLayout)里面設(shè)定命名空間,命名空間名稱可以隨便取,比如ljr,命名空間后面取得值是固定的:
"http://schemas.android.com/apk/res-auto"
最后就是在我們的自定義的View里面把我們自定義的屬性的值取出來,在構(gòu)造函數(shù)中,有個(gè)AttributeSet屬性,就是靠它幫我們把布局里面的屬性取出來:
private int defalutSize;
//構(gòu)造函數(shù)
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//第二個(gè)參數(shù)就是我們在attrs.xml文件中的<declare-styleable>標(biāo)簽
//即屬性集合的標(biāo)簽,在R文件中名稱為R.styleable+name
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
//第一個(gè)參數(shù)為屬性集合里面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱
//第二個(gè)參數(shù)為,如果沒有設(shè)置這個(gè)屬性,則設(shè)置的默認(rèn)的值
defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
//最后記得將TypedArray對象回收
a.recycle();
}
2. 自定義ViewGroup
- 先獲得子view的大小,這樣我們才能知道需要多大的ViewGroup去容納它們。
- 根據(jù)子view的大小以及需要實(shí)現(xiàn)的功能,決定ViewGroup的大小。
- 決定大小之后,就該將子view擺放在ViewGroup中。
接下來實(shí)現(xiàn)將子View按從上到下垂直順序一個(gè)挨著一個(gè)擺放的例子。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//將所有的子View進(jìn)行測量,這會(huì)觸發(fā)每個(gè)子View的onMeasure函數(shù)
//注意要與measureChild區(qū)分,measureChild是對單個(gè)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;
}
上面的onMeasure將子View測量好了,以及把自己的尺寸也設(shè)置好了,接下來擺放子View。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
//記錄當(dāng)前的高度位置
int curHeight = t;
//將子View逐個(gè)擺放
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;
}
}