前言: 最近開發(fā)的時候, 頻繁的需要使用到自定義控件控乾。自定義控件是成為高級工程師必不可少的條件之一恕刘,所以今天決定認(rèn)真總結(jié)一下。其實自定義控件也沒有想象中的那么復(fù)雜,無非只要掌握其中的幾個關(guān)鍵方法就能滿足絕大部分需求刹衫。但是若要真的要深入進(jìn)去,都能寫一本書了搞挣,這里就不做那么深入了带迟。能滿足日常的需求即可, 想深入了解的可自行查閱其他資料進(jìn)行學(xué)習(xí)。
在學(xué)習(xí)本篇自定義View之前,讀者有必要先學(xué)習(xí)一下View的繪制流程,這樣才能更好的理解文字的內(nèi)容囱桨。必知必會 | 面試官裝逼失敗之View的繪制流程
首先我們要明白仓犬,為什么要自定義View?主要是Android
系統(tǒng)內(nèi)置的View無法實現(xiàn)我們的需求舍肠,我們需要針對我們的業(yè)務(wù)需求定制我們想要的View搀继。簡單來說自定義控件無非就兩種窘面,自定義View
和自定義ViewGroup
:
自定義View
可以理解為自定義View的父類,是一個單獨(dú)的控件叽躯,里面無法存放子View财边。例如TextView,ImageView等都是繼承View的点骑,View里面最關(guān)鍵的方法是onMeasure
和onDraw
酣难。自定義ViewGroup
ViewGroup是View的子類,相當(dāng)于一個容器黑滴,里面可以放子View憨募。例如LinearLayout,RelativeLayout等都是繼承ViewGroup的跷跪。ViewGroup里面最關(guān)鍵的方法是onMeasure和onDraw和onLayout馋嗜。其中onLayout是ViewGroup中特有的方法,用來實現(xiàn)子View的擺放吵瞻。
1. 自定義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ù):
// 一個參數(shù)的構(gòu)造方法,在代碼中創(chuàng)建該控件時卿泽,調(diào)用該構(gòu)造方法
public MyView(Context context) {
super(context);
}
// 在xml 中引用該控件時,調(diào)用該方法滋觉。attrs是定義在xml布局中的屬性集合
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
1.1 重寫onMeasure
我們自定義View签夭,首先得要測量寬高尺寸。為什么要測量寬高尺寸椎侠?有的人要問了,我不是在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必須是要有具體的寬高的稚新,這回知道了吧?并不是所有情況下我們都會給某個View特定的尺寸的等缀。正是因為這個原因枷莉,我們必須自己去處理和設(shè)置尺寸。當(dāng)然了尺迂,View類給了默認(rèn)的處理笤妙,但是如果View類的默認(rèn)處理不滿足我們的要求,我們就得重寫onMeasure函數(shù)啦噪裕。這里舉個例子蹲盘,比如我們希望我們的View是個正方形,如果在xml中指定寬高為wrap_content
膳音,如果使用View類提供的measure處理方式召衔,顯然無法滿足我們的需求。
關(guān)于onMeasure函數(shù)的源碼解析,我已經(jīng)在上一篇文章中做了詳細(xì)的解釋了,不了解的請移步必知必會 | 面試官裝逼失敗之View的繪制流程祭陷。了解了onMeaSure方法的實現(xiàn)原理,在自定義View時我們需要對其進(jìn)行重寫苍凛。
講了太多理論,我們來實際操作一下吧兵志,感受一下onMeasure的使用醇蝴,現(xiàn)在假設(shè)我們要實現(xiàn)這樣一個效果:將當(dāng)前的View以正方形的形式顯示,即要寬高相等想罕,并且默認(rèn)的寬高值為100像素悠栓。代碼如下:
// defaultSize 默認(rèn)尺寸,這里為100像素
// measureSpec 測量規(guī)格
private int getSize(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 = getSize(100, widthMeasureSpec);
int height = getSize(100, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
// 設(shè)置測量之后的參數(shù)
setMeasuredDimension(width, height);
}
布局中使用它:
<com.jieyao.test.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#ff0000" />
使用了我們自己定義的onMeasure函數(shù)后的效果:
而如果我們不重寫onMeasure惭适,效果則是如下:
顯然重寫之后按照了我們意愿去顯示的,實現(xiàn)了我們的需求。
1.2 重寫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);
//也可以是getMeasuredHeight()/2。
//本例中我們已經(jīng)將寬高設(shè)置相等了。
int r = getMeasuredWidth() / 2;
//圓心的橫坐標(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.3 自定義屬性
有時候有些屬性我們希望由用戶指定膝舅,只有當(dāng)用戶不指定的時候才用我們硬編碼的值嗡载,比如上面的默認(rèn)尺寸,我們想要由用戶自己在布局文件里面指定該怎么做呢仍稀?那當(dāng)然是通我們自定屬性洼滚,讓用戶用我們定義的屬性啦~
- 首先我們需要在res/values/attrs.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:jieyao="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.jieyao.test.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
jieyao:default_size="100dp" />
</LinearLayout>
注意:需要在根標(biāo)簽(LinearLayout)里面設(shè)定命名空間,命名空間名稱可以隨便取享幽,比如 jieyao铲掐,命名空間后面取的值是固定的:"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ù)就是我們在attrs.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.jieyao.test;
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;
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 getSize(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 = getSize(defalutSize, widthMeasureSpec);
int height = getSize(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放入指定的位置。我們結(jié)合一個具體案例來一步步實現(xiàn)自定義ViewGroup的過程:將子View按從上到下以垂直順序一個挨著一個擺放皆疹,即模仿實現(xiàn)LinearLayout的垂直布局疏橄。
2.1 重寫onMeasure
重寫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();// 子View個數(shù)
if (childCount == 0) {//如果沒有子View,當(dāng)前ViewGroup沒有存在的意義晃酒,不用占用空間
setMeasuredDimension(0, 0);
} else { // 有子View,對MeasureSpec為AT_MOST時進(jìn)行特殊處理
//如果寬高都是包裹內(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)行講解,相信很容易理解吧彰导。
2.2 重寫onLayout
上面的onMeasure將子View測量好了蛔翅,以及把自己的尺寸也設(shè)置好了,接下來我們?nèi)[放子View吧~只需要重寫onLayout方法即可,代碼如下:
@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已經(jīng)完成, 我們來測試一下效果盖腿,將我們自定義的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.jieyao.test.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.jieyao.test;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
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();//子View個數(shù)
if (childCount == 0) {//如果沒有子View,當(dāng)前ViewGroup沒有存在的意義茂卦,不用占用空間
setMeasuredDimension(0, 0);
} else { // 有子View,對MeasureSpec為AT_MOST時進(jìn)行特殊處理
//如果寬高都是包裹內(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;
}
}
}
3. 實戰(zhàn)項目
本人雖然是一個Android開發(fā)者,卻對蘋果手機(jī)有獨(dú)特的愛好。經(jīng)常使用蘋果手機(jī)的朋友可能知道, 蘋果的設(shè)置界面有很多滑動的開關(guān)按鈕, 可以左滑右滑實現(xiàn)某個功能的開啟和關(guān)閉, 看上去也是很酷炫有沒有~今天, 就來實現(xiàn)一下這個功能伶贰。
首先,來看一下我實現(xiàn)的滑動開關(guān)效果圖:
這個滑動開關(guān)是一個純粹的自定義控件蛛砰,上面的按鈕會隨著我們的左右滑動而滑動,并且在狀態(tài)改變時通知用戶黍衙,這也是應(yīng)用中設(shè)置某些狀態(tài)信息時最常見的控件泥畅。
在實際開發(fā)中,完整的實現(xiàn)一個自定義控件,并讓該控件具備某個功能,一般來說要有以下幾個步驟:
- 創(chuàng)建一個view繼承自View或者ViewGroup
- 定義自定義view的屬性
- 在代碼中獲取屬性,并給自定義屬性相應(yīng)的設(shè)置事件
- 根據(jù)實際重寫自定義view的onMeasure,onLayout,onDraw方法
- 與用戶進(jìn)行交互的邏輯實現(xiàn)
- 自定義view的代碼優(yōu)化
- 1.創(chuàng)建view
public class ToggleButton extends View { // 滑動開關(guān)類
}
- 自定義view屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ToggleButton">
<!-- 滑動開關(guān)背景圖片屬性-->
<attr name="SwitchBtnBackgroud" format="reference" />
<!-- 滑動塊背景圖片屬性-->
<attr name="SlidBtnBackgroud" format="reference" />
<!-- 滑動開關(guān)的狀態(tài)-->
<attr name="CurrentState" format="boolean" />
</declare-styleable>
</resources>
- 在代碼中獲取屬性并給自定義屬性相應(yīng)的設(shè)置事件
private Bitmap switchBitmap;//滑動開關(guān)的背景圖片
private Bitmap slidBitmap;//滑動塊的背景圖片
private boolean currentState;// 滑動開關(guān)的狀態(tài)
//在xml 中引用該控件時,調(diào)用該方法
public ToggleButton(Context context, AttributeSet attrs) {
super(context, attrs);
String namespace = "http://schemas.android.com/apk/res/com.itheima.togglebuttondemo";
currentState = attrs.getAttributeBooleanValue(namespace, "CurrentState",
int switchBtnBackgroudId = attrs.getAttributeResourceValue(namespace, "SwitchBtnBackgroud", -1);
int slidBtnBackgroudId =attrs.getAttributeResourceValue(namespace, "SlidBtnBackgroud", -1);
setSwitchBtnBackgroudResource(switchBtnBackgroudId);
setSlidBtnBackgroudResource(slidBtnBackgroudId);
}
//在代碼中創(chuàng)建該控件時琅翻,調(diào)用該構(gòu)造方法
public ToggleButton(Context context) {
super(context);
}
// 為了可以高度自定義和增強(qiáng)可擴(kuò)展性,我們給滑動按鈕背景和滑動塊背景都提供了設(shè)置方法
//設(shè)置滑動開關(guān)的背景圖片
public void setSwitchBtnBackgroudResource(int switchBackground) {
switchBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
}
// 設(shè)置滑動塊的背景圖片
public void setSlidBtnBackgroudResource(int slideButtonBackground) {
slidBitmap = BitmapFactory.decodeResource(getResources(), slideButtonBackground);
}
//設(shè)置滑動開關(guān)的默認(rèn)狀態(tài)
public void setCurrentState(boolean b) {
currentState = b;
}
- 重寫onMeasure方法和onDraw方法
// 1位仁、測量滑動開關(guān)的寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
}
// 2、繪制方椎,畫出我們的滑動開關(guān)
//canvas:畫布聂抢,將圖形繪制在canvas,才能顯示到屏幕上
@Override
protected void onDraw(Canvas canvas) {
//繪制滑動開關(guān)的背景圖片
canvas.drawBitmap(switchBitmap, 0, 0, null);
//繪制滑動塊的背景圖片,要根據(jù)手勢實時繪制
if(isTouching){//手指觸摸的時候辩尊,根據(jù)currentx 的值來繪制滑動塊
//根據(jù)手指的X 值涛浙,來繪制滑動塊圖片
int left = currentX - slidBitmap.getWidth()/2;
if(left < 0){//設(shè)置左邊界
left = 0;//左邊零點(diǎn)
}else if(left > (switchBitmap.getWidth() - slidBitmap.getWidth())){//設(shè)置右邊界
left = switchBitmap.getWidth() - slidBitmap.getWidth();//中心點(diǎn)
}
canvas.drawBitmap(slidBitmap, left, 0, null);//根據(jù)左邊界位置繪制滑動塊背景
}else{ // 手指已經(jīng)離開控件的時候,根據(jù)狀態(tài)來繪制滑動塊
// 根據(jù)狀態(tài)值,來繪制滑動塊
if(currentState){ //當(dāng)前為true轿亮,開關(guān)打開疮薇,滑動塊顯示在最右邊
canvas.drawBitmap(slidBitmap,switchBitmap.getWidth() - slidBitmap.getWidth(),0, null);
}else{//當(dāng)前為false,開關(guān)關(guān)閉我注,滑動塊顯示在最左邊
canvas.drawBitmap(slidBitmap, 0, 0, null);
}
}
}
5.與用戶進(jìn)行交互的邏輯實現(xiàn)
// 當(dāng)控件被觸摸后按咒,會調(diào)用該方法(通過改動isTouching 和currentState的值動態(tài)繪制滑動塊)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:// 手指按下
isTouching = true;
currentX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:// 手指滑動
isTouching = true;
currentX = (int) event.getX();
break;
case MotionEvent.ACTION_UP:// 手指抬起
isTouching = false;
currentX = (int) event.getX();
int center = switchBitmap.getWidth() / 2;
// 當(dāng)滑動塊中心點(diǎn)大于滑動開關(guān)背景圖片的中心線時,顯示到右邊但骨,狀態(tài)改為true
boolean state = currentState;
// 獲取滑動塊的狀態(tài)
currentState = currentX > center;
// 設(shè)置滑動塊的狀態(tài)
// state != currentState說明開關(guān)狀態(tài)發(fā)生了改變
if (mToggleBtnStateChangeListener != null && state != currentState) {
mToggleBtnStateChangeListener.onToggleBtnStateChange(currentState);
}
break;
default:
break;
}
// 強(qiáng)制讓控件重新繪制励七,
invalidate(); //此方法可以強(qiáng)制重新調(diào)用onDraw方法
// 自己處理觸摸事件
return true;
}
// 給滑動塊設(shè)置狀態(tài)改變監(jiān)聽(方便在activity代碼中做相應(yīng)邏輯處理)
// 參數(shù)為ToggleBtnStateChangeListener 接口,傳入之后會回調(diào)onToggleBtnStateChange方法。
// 根據(jù)回調(diào)方法中的currentState做對應(yīng)邏輯判斷和邏輯處理
public void setToggleBtnStateChangeListener(
ToggleBtnStateChangeListener listener) {
this.mToggleBtnStateChangeListener = listener;
}
// 滑動開關(guān)狀態(tài)改變的回調(diào)接口
public interface ToggleBtnStateChangeListener {
void onToggleBtnStateChange(boolean currentState);
}
- 自定義view的代碼優(yōu)化:
在上面的步驟結(jié)束之后奔缠,其實一個完善的自定義控件已經(jīng)出來了掠抬。接下來你要做的只是確保自定義控件運(yùn)行得流暢,官方的說法是:為了避免你的控件看得來遲緩校哎,確保動畫始終保持每秒60幀.
下面是官網(wǎng)給出的優(yōu)化建議:
1两波、避免不必要的代碼
2、在onDraw()方法中不應(yīng)該有會導(dǎo)致垃圾回收的代碼闷哆。
3腰奋、盡可能少讓onDraw()方法調(diào)用,大多數(shù)onDraw()方法調(diào)用都是手動調(diào)用了invalidate()的結(jié)果抱怔,所以如果不是必須劣坊,不要調(diào)用invalidate()方法。
下面貼出自定義滑動開關(guān)的完整源碼:
/**
* 自定義滑動開關(guān)
*/
public class ToggleButton extends View {
private Bitmap switchBitmap;// 滑動開關(guān)的背景圖片
private Bitmap slidBitmap;// 滑動塊的背景圖片
private boolean currentState; // 當(dāng)前滑動開關(guān)的狀態(tài)
private int currentX;// 手指觸摸點(diǎn)的X值
private boolean isTouching = false; // 是否觸摸到屏幕
private ToggleBtnStateChangeListener mToggleBtnStateChangeListener;// 狀態(tài)改變監(jiān)聽器
// 在xml中引用該控件時屈留,調(diào)用該方法
public ToggleButton(Context context, AttributeSet attrs) {
super(context, attrs);
// 聲明的命名空間
String namespace = "http://schemas.android.com/apk/res/com.itheima.togglebuttondemo";
// 獲取布局中滑動開關(guān)狀態(tài)的屬性
currentState = attrs.getAttributeBooleanValue(namespace,
"CurrentState", false);
// 獲取布局中滑動開關(guān)背景的屬性
int switchBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
"SwitchBtnBackgroud", -1);
// 獲取布局中滑動開關(guān)滑動塊的背景的屬性
int slidBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
"SlidBtnBackgroud", -1);
// 根據(jù)布局中的屬性設(shè)置滑動開關(guān)背景
setSwitchBtnBackgroudResource(switchBtnBackgroudId);
// 根據(jù)布局中的屬性設(shè)置滑動開關(guān)滑動塊的背景
setSlidBtnBackgroudResource(slidBtnBackgroudId);
}
// 在代碼中創(chuàng)建該控件時局冰,調(diào)用該構(gòu)造方法
public ToggleButton(Context context) {
super(context);
}
// 設(shè)置滑動開關(guān)的背景圖片
public void setSwitchBtnBackgroudResource(int switchBackground) {
switchBitmap = BitmapFactory.decodeResource(getResources(),
switchBackground);
}
// 設(shè)置滑動塊的背景圖片
public void setSlidBtnBackgroudResource(int slideButtonBackground) {
slidBitmap = BitmapFactory.decodeResource(getResources(),
slideButtonBackground);
}
// 設(shè)置滑動開關(guān)的默認(rèn)狀態(tài)
public void setCurrentState(boolean b) {
currentState = b;
}
// 1、測量滑動開關(guān)的寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
}
// 2灌危、繪制锐想,畫出我們的滑動開關(guān)
// canvas:畫布,將圖形繪制在canvas乍狐,才能顯示到屏幕上
@Override
protected void onDraw(Canvas canvas) {
// 繪制滑動開關(guān)的背景圖片
canvas.drawBitmap(switchBitmap, 0, 0, null);
// 繪制滑動塊的背景圖片
if (isTouching) {// 手指觸摸的時候,根據(jù)currentX的值來繪制滑動塊
// 根據(jù)手指的X值固逗,來繪制滑動塊圖片
int left = currentX - slidBitmap.getWidth() / 2;
if (left < 0) { // 設(shè)置左邊界
left = 0;
} else if (left > (switchBitmap.getWidth() - slidBitmap.getWidth())) {// 設(shè)置右邊界
left = switchBitmap.getWidth() - slidBitmap.getWidth();
}
canvas.drawBitmap(slidBitmap, left, 0, null);
} else {// 手指離開控件的時候浅蚪,根據(jù)狀態(tài)來繪制滑動塊
// 根據(jù)狀態(tài)值,來繪制滑動塊
if (currentState) {// 當(dāng)前為true烫罩,開關(guān)打開惜傲,滑動塊顯示在最右邊
canvas.drawBitmap(slidBitmap, switchBitmap.getWidth()
- slidBitmap.getWidth(), 0, null);
} else {// 當(dāng)前為false,開關(guān)關(guān)閉贝攒,滑動塊顯示在最左邊
canvas.drawBitmap(slidBitmap, 0, 0, null);
}
}
}
// 當(dāng)控件被觸摸后盗誊,會調(diào)用該方法
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:// 手指按下
isTouching = true;
currentX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:// 手指滑動
isTouching = true;
currentX = (int) event.getX();
break;
case MotionEvent.ACTION_UP:// 手指抬起
isTouching = false;
currentX = (int) event.getX();
int center = switchBitmap.getWidth() / 2;
// 當(dāng)滑動塊中心點(diǎn)大于滑動開關(guān)背景圖片的中心線時,顯示到右邊,當(dāng)前狀態(tài)為true
boolean state = currentState;
// 獲取滑動塊的狀態(tài)
currentState = currentX > center;
// 設(shè)置滑動塊的狀態(tài)
if (mToggleBtnStateChangeListener != null && state != currentState) {
mToggleBtnStateChangeListener
.onToggleBtnStateChange(currentState);
}
break;
default:
break;
}
// 強(qiáng)制讓控件重新繪制,重新調(diào)用onDraw方法
invalidate();
// 自己處理觸摸事件
return true;
}
// 給滑動塊設(shè)置狀態(tài)改變監(jiān)聽
public void setToggleBtnStateChangeListener(
ToggleBtnStateChangeListener listener) {
this.mToggleBtnStateChangeListener = listener;
}
// 滑動開關(guān)狀態(tài)改變的回調(diào)接口
public interface ToggleBtnStateChangeListener {
void onToggleBtnStateChange(boolean currentState);
}
}
大功告成O(∩_∩)O哈哈~ 下面就是使用啦~
xml布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:jieyao="http://schemas.android.com/apk/res/com.jieyao.togglebuttondemo"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.jieyao.togglebuttondemo.view.ToggleButton
android:id="@+id/togglebutton"
android:layout_width="wrap_content"
android:layout_centerInParent="true"
jieyao:SwitchBtnBackgroud="@drawable/switch_background"
jieyao:SlidBtnBackgroud="@drawable/slide_button_background"
jieyao:CurrentState="false"
android:layout_height="wrap_content"/>
</RelativeLayout>
activity中使用~
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化滑動開關(guān)
ToggleButton togglebutton = (ToggleButton) findViewById(R.id.togglebutton);
// 設(shè)置滑動開關(guān)的背景圖片
togglebutton.setSwitchBtnBackgroudResource(R.drawable.switch_background);
// 設(shè)置滑動塊的背景圖片
togglebutton.setSlidBtnBackgroudResource(R.drawable.slide_button_background);
// 設(shè)置滑動開關(guān)的默認(rèn)狀態(tài)
togglebutton.setCurrentState(true);
// 設(shè)置滑動開關(guān)狀態(tài)監(jiān)聽
togglebutton.setToggleBtnStateChangeListener(new ToggleBtnStateChangeListener() {
@Override
public void onToggleBtnStateChange(boolean currentState) {
//下面就是根據(jù)currentState狀態(tài)做相應(yīng)的邏輯咯,根據(jù)需求來做
if (currentState) {
Toast.makeText(getApplicationContext(), "開關(guān)打開",Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "開關(guān)關(guān)閉",Toast.LENGTH_SHORT).show();
}
}
});
}
}
效果圖如下:
以上就是自定義View的全過程啦~ 希望能對你們有幫助~! 本人技術(shù)有限,如有錯誤,還請指出,謝謝!