Android最易懂的自定义View讲解
前言: 最近开发的时候, 频繁的需要使用到自定义控件。自定义控件是成为高级工程师必不可少的条件之一,所以今天决定认真总结一下。其实自定义控件也没有想象中的那么复杂,无非只要掌握其中的几个关键方法就能满足绝大部分需求。但是若要真的要深入进去,都能写一本书了,这里就不做那么深入了。能满足日常的需求即可, 想深入了解的可自行查阅其他资料进行学习。
在学习本篇自定义View之前,读者有必要先学习一下View的绘制流程,这样才能更好的理解文字的内容。必知必会 | 面试官装逼失败之View的绘制流程
首先我们要明白,为什么要自定义View?主要是Android
系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。简单来说自定义控件无非就两种,自定义View
和自定义ViewGroup
:
-
自定义View
可以理解为自定义View的父类,是一个单独的控件,里面无法存放子View。例如TextView,ImageView等都是继承View的,View里面最关键的方法是onMeasure
和onDraw
。 -
自定义ViewGroup
ViewGroup是View的子类,相当于一个容器,里面可以放子View。例如LinearLayout,RelativeLayout等都是继承ViewGroup的。ViewGroup里面最关键的方法是onMeasure和onDraw和onLayout。其中onLayout是ViewGroup中特有的方法,用来实现子View的摆放。
1. 自定义View
自定义View的话我们大部分时候只需重写两个函数:onMeasure()
、onDraw()
。onMeasure负责对当前View的尺寸进行测量,onDraw负责把当前这个View绘制出来。当然了,你还得写至少写2个构造函数:
// 一个参数的构造方法,在代码中创建该控件时,调用该构造方法
public MyView(Context context) {
super(context);
}
// 在xml 中引用该控件时,调用该方法。attrs是定义在xml布局中的属性集合
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
1.1 重写onMeasure
我们自定义View,首先得要测量宽高尺寸。为什么要测量宽高尺寸?有的人要问了,我不是在xml文件中已经指定好了宽高尺寸了吗, 我自定义的View有必要再一次获取宽高去设置宽高吗?既然我自定义的View是继承自View类,google团队直接在View类中直接把xml设置的宽高获取,并且设置进去不就好了吗?为什么要让我们自己来做,真可恨!别着急,既然google让我们做这样的“重复工作”,自然有他的道理。
在学习Android的时候,我们就知道,在xml布局文件中,我们的layout_width
和layout_height
参数可以不用写具体的尺寸,而是wrap_content
或者是match_parent
。其意思我们都知道,就是将尺寸设置为“包住内容”和“填充父布局给我们的所有空间”。这两个设置并没有指定真正的大小,可是我们绘制到屏幕上的View必须是要有具体的宽高的,这回知道了吧?并不是所有情况下我们都会给某个View特定的尺寸的。正是因为这个原因,我们必须自己去处理和设置尺寸。当然了,View类给了默认的处理,但是如果View类的默认处理不满足我们的要求,我们就得重写onMeasure函数啦。这里举个例子,比如我们希望我们的View是个正方形,如果在xml中指定宽高为wrap_content
,如果使用View类提供的measure处理方式,显然无法满足我们的需求。
关于onMeasure函数的源码解析,我已经在上一篇文章中做了详细的解释了,不了解的请移步必知必会 | 面试官装逼失败之View的绘制流程。了解了onMeaSure方法的实现原理,在自定义View时我们需要对其进行重写。
讲了太多理论,我们来实际操作一下吧,感受一下onMeasure的使用,现在假设我们要实现这样一个效果:将当前的View以正方形的形式显示,即要宽高相等,并且默认的宽高值为100像素。代码如下:
// defaultSize 默认尺寸,这里为100像素
// measureSpec 测量规格
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: {//如果没有指定大小,就设置为默认大小
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;
}
// 设置测量之后的参数
setMeasuredDimension(width, height);
}
布局中使用它:
<com.jieyao.test.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#ff0000" />
使用了我们自己定义的onMeasure函数后的效果:
正方形显示View而如果我们不重写onMeasure,效果则是如下:
未重写onMeasure的效果显然重写之后按照了我们意愿去显示的,实现了我们的需求。
1.2 重写onDraw
上面我们学会了自定义尺寸大小,尺寸我们会设定了,接下来就是把我们想要的效果画出来吧~绘制我们想要的效果很简单,直接在画板Canvas
对象上绘制就好啦,逻辑过于简单,我们以一个简单的例子去学习:假设我们需要实现的是,我们的View显示一个圆形,我们在上面已经实现了宽高尺寸相等的基础上,继续往下做:
@Override
protected void onDraw(Canvas canvas) {
//调用父View的onDraw函数,因为View这个类帮我们实现了一些
// 基本的而绘制功能,比如绘制背景颜色、背景图片等
super.onDraw(canvas);
//也可以是getMeasuredHeight()/2。
//本例中我们已经将宽高设置相等了。
int r = getMeasuredWidth() / 2;
//圆心的横坐标为当前的View的左边起始位置+半径
int centerX = getLeft() + r;
//圆心的纵坐标为当前的View的顶部起始位置+半径
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.GREEN);
//绘制圆形
canvas.drawCircle(centerX, centerY, r, paint);
}
效果图如下:
圆形显示1.3 自定义属性
有时候有些属性我们希望由用户指定,只有当用户不指定的时候才用我们硬编码的值,比如上面的默认尺寸,我们想要由用户自己在布局文件里面指定该怎么做呢?那当然是通我们自定属性,让用户用我们定义的属性啦~
- 首先我们需要在res/values/attrs.xml文件(如果没有请自己新建)里面声明一个我们自定义的属性:
<resources>
<!--name为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的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>
注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如 jieyao,命名空间后面取的值是固定的:"http://schemas.android.com/apk/res-auto"
- 最后就是在我们的自定义的View的代码里面把我们自定义的属性的值取出来,在构造函数中,还记得有个
AttributeSet
属性吗?就是靠它帮我们把布局里面的属性取出来:
private int defalutSize;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//第二个参数就是我们在attrs.xml文件中的<declare-styleable>标签
//即属性集合的标签,在R文件中名称为R.styleable.name
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
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);
//第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
//即属性集合的标签,在R文件中名称为R.styleable.name
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
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: {//如果没有指定大小,就设置为默认大小
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) {
//调用父View的onDraw函数,因为View这个类帮我们实现了一些
// 基本的而绘制功能,比如绘制背景颜色、背景图片等
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
//圆心的横坐标为当前的View的左边起始位置+半径
int centerX = getLeft() + r;
//圆心的纵坐标为当前的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并且负责把child View放入指定的位置。我们结合一个具体案例来一步步实现自定义ViewGroup的过程:将子View按从上到下以垂直顺序一个挨着一个摆放,即模仿实现LinearLayout的垂直布局。
2.1 重写onMeasure
重写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();// 子View个数
if (childCount == 0) {//如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
setMeasuredDimension(0, 0);
} else { // 有子View,对MeasureSpec为AT_MOST时进行特殊处理
//如果宽高都是包裹内容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
int height = getTotleHeight();
int width = getMaxChildWidth();
setMeasuredDimension(width, height);
} else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹内容
//宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
setMeasuredDimension(widthSize, getTotleHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容
//宽度设置为子View中宽度最大的值,高度设置为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;
}
代码中的注释我已经写得很详细,不再对每一行代码进行讲解,相信很容易理解吧。
2.2 重写onLayout
上面的onMeasure将子View测量好了,以及把自己的尺寸也设置好了,接下来我们去摆放子View吧~只需要重写onLayout方法即可,代码如下:
@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 height = child.getMeasuredHeight();
int width = child.getMeasuredWidth();
//摆放子View,参数分别是子View矩形区域的左、上、右、下边
child.layout(l, curHeight, l + width, curHeight + height);
curHeight += height;
}
}
自定义ViewGroup已经完成, 我们来测试一下效果,将我们自定义的ViewGroup里面放3个Button ,将这3个Button的宽度设置不一样,把我们的ViewGroup的宽高都设置为包裹内容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>
看看最后的效果吧~是不是很激动我们自己也可以实现LinearLayout的效果啦
自定义ViewGroup最后附上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进行测量,这会触发每个子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();//子View个数
if (childCount == 0) {//如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
setMeasuredDimension(0, 0);
} else { // 有子View,对MeasureSpec为AT_MOST时进行特殊处理
//如果宽高都是包裹内容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
int height = getTotleHeight();
int width = getMaxChildWidth();
setMeasuredDimension(width, height);
} else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹内容
//宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
setMeasuredDimension(widthSize, getTotleHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容
//宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
setMeasuredDimension(getMaxChildWidth(), heightSize);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
//记录当前的高度位置
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. 实战项目
本人虽然是一个Android开发者,却对苹果手机有独特的爱好。经常使用苹果手机的朋友可能知道, 苹果的设置界面有很多滑动的开关按钮, 可以左滑右滑实现某个功能的开启和关闭, 看上去也是很酷炫有没有~今天, 就来实现一下这个功能。
首先,来看一下我实现的滑动开关效果图:
滑动开关效果图这个滑动开关是一个纯粹的自定义控件,上面的按钮会随着我们的左右滑动而滑动,并且在状态改变时通知用户,这也是应用中设置某些状态信息时最常见的控件。
在实际开发中,完整的实现一个自定义控件,并让该控件具备某个功能,一般来说要有以下几个步骤:
- 创建一个view继承自View或者ViewGroup
- 定义自定义view的属性
- 在代码中获取属性,并给自定义属性相应的设置事件
- 根据实际重写自定义view的onMeasure,onLayout,onDraw方法
- 与用户进行交互的逻辑实现
- 自定义view的代码优化
- 1.创建view
public class ToggleButton extends View { // 滑动开关类
}
- 自定义view属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ToggleButton">
<!-- 滑动开关背景图片属性-->
<attr name="SwitchBtnBackgroud" format="reference" />
<!-- 滑动块背景图片属性-->
<attr name="SlidBtnBackgroud" format="reference" />
<!-- 滑动开关的状态-->
<attr name="CurrentState" format="boolean" />
</declare-styleable>
</resources>
- 在代码中获取属性并给自定义属性相应的设置事件
private Bitmap switchBitmap;//滑动开关的背景图片
private Bitmap slidBitmap;//滑动块的背景图片
private boolean currentState;// 滑动开关的状态
//在xml 中引用该控件时,调用该方法
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);
}
//在代码中创建该控件时,调用该构造方法
public ToggleButton(Context context) {
super(context);
}
// 为了可以高度自定义和增强可扩展性,我们给滑动按钮背景和滑动块背景都提供了设置方法
//设置滑动开关的背景图片
public void setSwitchBtnBackgroudResource(int switchBackground) {
switchBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
}
// 设置滑动块的背景图片
public void setSlidBtnBackgroudResource(int slideButtonBackground) {
slidBitmap = BitmapFactory.decodeResource(getResources(), slideButtonBackground);
}
//设置滑动开关的默认状态
public void setCurrentState(boolean b) {
currentState = b;
}
- 重写onMeasure方法和onDraw方法
// 1、测量滑动开关的宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
}
// 2、绘制,画出我们的滑动开关
//canvas:画布,将图形绘制在canvas,才能显示到屏幕上
@Override
protected void onDraw(Canvas canvas) {
//绘制滑动开关的背景图片
canvas.drawBitmap(switchBitmap, 0, 0, null);
//绘制滑动块的背景图片,要根据手势实时绘制
if(isTouching){//手指触摸的时候,根据currentx 的值来绘制滑动块
//根据手指的X 值,来绘制滑动块图片
int left = currentX - slidBitmap.getWidth()/2;
if(left < 0){//设置左边界
left = 0;//左边零点
}else if(left > (switchBitmap.getWidth() - slidBitmap.getWidth())){//设置右边界
left = switchBitmap.getWidth() - slidBitmap.getWidth();//中心点
}
canvas.drawBitmap(slidBitmap, left, 0, null);//根据左边界位置绘制滑动块背景
}else{ // 手指已经离开控件的时候,根据状态来绘制滑动块
// 根据状态值,来绘制滑动块
if(currentState){ //当前为true,开关打开,滑动块显示在最右边
canvas.drawBitmap(slidBitmap,switchBitmap.getWidth() - slidBitmap.getWidth(),0, null);
}else{//当前为false,开关关闭,滑动块显示在最左边
canvas.drawBitmap(slidBitmap, 0, 0, null);
}
}
}
5.与用户进行交互的逻辑实现
// 当控件被触摸后,会调用该方法(通过改动isTouching 和currentState的值动态绘制滑动块)
@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;
// 当滑动块中心点大于滑动开关背景图片的中心线时,显示到右边,状态改为true
boolean state = currentState;
// 获取滑动块的状态
currentState = currentX > center;
// 设置滑动块的状态
// state != currentState说明开关状态发生了改变
if (mToggleBtnStateChangeListener != null && state != currentState) {
mToggleBtnStateChangeListener.onToggleBtnStateChange(currentState);
}
break;
default:
break;
}
// 强制让控件重新绘制,
invalidate(); //此方法可以强制重新调用onDraw方法
// 自己处理触摸事件
return true;
}
// 给滑动块设置状态改变监听(方便在activity代码中做相应逻辑处理)
// 参数为ToggleBtnStateChangeListener 接口,传入之后会回调onToggleBtnStateChange方法。
// 根据回调方法中的currentState做对应逻辑判断和逻辑处理
public void setToggleBtnStateChangeListener(
ToggleBtnStateChangeListener listener) {
this.mToggleBtnStateChangeListener = listener;
}
// 滑动开关状态改变的回调接口
public interface ToggleBtnStateChangeListener {
void onToggleBtnStateChange(boolean currentState);
}
- 自定义view的代码优化:
在上面的步骤结束之后,其实一个完善的自定义控件已经出来了。接下来你要做的只是确保自定义控件运行得流畅,官方的说法是:为了避免你的控件看得来迟缓,确保动画始终保持每秒60帧.
下面是官网给出的优化建议:
1、避免不必要的代码
2、在onDraw()方法中不应该有会导致垃圾回收的代码。
3、尽可能少让onDraw()方法调用,大多数onDraw()方法调用都是手动调用了invalidate()的结果,所以如果不是必须,不要调用invalidate()方法。
下面贴出自定义滑动开关的完整源码:
/**
* 自定义滑动开关
*/
public class ToggleButton extends View {
private Bitmap switchBitmap;// 滑动开关的背景图片
private Bitmap slidBitmap;// 滑动块的背景图片
private boolean currentState; // 当前滑动开关的状态
private int currentX;// 手指触摸点的X值
private boolean isTouching = false; // 是否触摸到屏幕
private ToggleBtnStateChangeListener mToggleBtnStateChangeListener;// 状态改变监听器
// 在xml中引用该控件时,调用该方法
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", false);
// 获取布局中滑动开关背景的属性
int switchBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
"SwitchBtnBackgroud", -1);
// 获取布局中滑动开关滑动块的背景的属性
int slidBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
"SlidBtnBackgroud", -1);
// 根据布局中的属性设置滑动开关背景
setSwitchBtnBackgroudResource(switchBtnBackgroudId);
// 根据布局中的属性设置滑动开关滑动块的背景
setSlidBtnBackgroudResource(slidBtnBackgroudId);
}
// 在代码中创建该控件时,调用该构造方法
public ToggleButton(Context context) {
super(context);
}
// 设置滑动开关的背景图片
public void setSwitchBtnBackgroudResource(int switchBackground) {
switchBitmap = BitmapFactory.decodeResource(getResources(),
switchBackground);
}
// 设置滑动块的背景图片
public void setSlidBtnBackgroudResource(int slideButtonBackground) {
slidBitmap = BitmapFactory.decodeResource(getResources(),
slideButtonBackground);
}
// 设置滑动开关的默认状态
public void setCurrentState(boolean b) {
currentState = b;
}
// 1、测量滑动开关的宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
}
// 2、绘制,画出我们的滑动开关
// canvas:画布,将图形绘制在canvas,才能显示到屏幕上
@Override
protected void onDraw(Canvas canvas) {
// 绘制滑动开关的背景图片
canvas.drawBitmap(switchBitmap, 0, 0, null);
// 绘制滑动块的背景图片
if (isTouching) {// 手指触摸的时候,根据currentX的值来绘制滑动块
// 根据手指的X值,来绘制滑动块图片
int left = currentX - slidBitmap.getWidth() / 2;
if (left < 0) { // 设置左边界
left = 0;
} else if (left > (switchBitmap.getWidth() - slidBitmap.getWidth())) {// 设置右边界
left = switchBitmap.getWidth() - slidBitmap.getWidth();
}
canvas.drawBitmap(slidBitmap, left, 0, null);
} else {// 手指离开控件的时候,根据状态来绘制滑动块
// 根据状态值,来绘制滑动块
if (currentState) {// 当前为true,开关打开,滑动块显示在最右边
canvas.drawBitmap(slidBitmap, switchBitmap.getWidth()
- slidBitmap.getWidth(), 0, null);
} else {// 当前为false,开关关闭,滑动块显示在最左边
canvas.drawBitmap(slidBitmap, 0, 0, null);
}
}
}
// 当控件被触摸后,会调用该方法
@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;
// 当滑动块中心点大于滑动开关背景图片的中心线时,显示到右边,当前状态为true
boolean state = currentState;
// 获取滑动块的状态
currentState = currentX > center;
// 设置滑动块的状态
if (mToggleBtnStateChangeListener != null && state != currentState) {
mToggleBtnStateChangeListener
.onToggleBtnStateChange(currentState);
}
break;
default:
break;
}
// 强制让控件重新绘制,重新调用onDraw方法
invalidate();
// 自己处理触摸事件
return true;
}
// 给滑动块设置状态改变监听
public void setToggleBtnStateChangeListener(
ToggleBtnStateChangeListener listener) {
this.mToggleBtnStateChangeListener = listener;
}
// 滑动开关状态改变的回调接口
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);
// 初始化滑动开关
ToggleButton togglebutton = (ToggleButton) findViewById(R.id.togglebutton);
// 设置滑动开关的背景图片
togglebutton.setSwitchBtnBackgroudResource(R.drawable.switch_background);
// 设置滑动块的背景图片
togglebutton.setSlidBtnBackgroudResource(R.drawable.slide_button_background);
// 设置滑动开关的默认状态
togglebutton.setCurrentState(true);
// 设置滑动开关状态监听
togglebutton.setToggleBtnStateChangeListener(new ToggleBtnStateChangeListener() {
@Override
public void onToggleBtnStateChange(boolean currentState) {
//下面就是根据currentState状态做相应的逻辑咯,根据需求来做
if (currentState) {
Toast.makeText(getApplicationContext(), "开关打开",Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "开关关闭",Toast.LENGTH_SHORT).show();
}
}
});
}
}
效果图如下:
滑动开关效果以上就是自定义View的全过程啦~ 希望能对你们有帮助~! 本人技术有限,如有错误,还请指出,谢谢!