转载请注明出处:
以前的项目中有关于手势开关的按钮,发现自带的ToggleButton不是很好看,于是花了些时间,自定义了个ToggleButton。
先上效果图:
其中,圆形button是可以随手指左右滑动的。
大体思路是分以下几步: 首先是需要自定义属性:
在values文件夹下的attrs xml文件中自定义属性:
<declare-styleable name="ToggleButton">
<!-- 开关背景图片的属性 -->
<attr name="switchBackgroundResource" format="reference"/>
<attr name="switchBackgroundTrueResource" format="reference"/>
<!-- 开关滑动块的属性 -->
<attr name="slideButtonBackgroundResource" format="reference"/>
<!-- 开关当前状态的属性, true 打开, false 关闭 -->
<attr name="currentState" format="boolean"/>
</declare-styleable>
然后ToggleButton 是继承自View基类,然后需要在xml文件中引入此控件,需要在一下构造函数中这样写:
/**
* 当在xml代码中引用此自定义控件时, 需要使用此构造函数.
*
* @param context
* @param attrs
* 在布局文件中声明的各种属性.
*/
public ToggleButton(Context context, AttributeSet attrs) {
……
// 自定义的命名空间
String namespace = "http://schemas.android.com/apk/res/com.zchz.app";
// 取出自定义属性中背景图片
int switchBackgroundID = attrs.getAttributeResourceValue(namespace, "switchBackgroundResource", -1);
int switchBackgroundTrueID = attrs.getAttributeResourceValue(namespace, "switchBackgroundTrueResource", -1);
// 取出自定义属性中滑动块图片
int slideButtonBackgroundID = attrs.getAttributeResourceValue(namespace, "slideButtonBackgroundResource", -1);
// 取出自定义属性中当前状态
currentState = attrs.getAttributeBooleanValue(namespace, "currentState", false);
……
}
在该构造函数中需要进行命名空间自定义,一些自定义属性的读取等等。
然后是提供对外修改当前开关的状态的方法currentState,
自定义控件比较重要的三个API分别是 onMesure(), onLayout(), onDraw(), 分别是进行对当前需要自定义View的内容属性的大小测量,具体在视图窗口中的布局(即位置的确定),以及最重要的一步:利用paint及cavas(画布)将自定义view展示出来。
为了实现能够手动滑动按钮实现开关切换的效果,需要复写onTouchEvent()方法来进行当前手指触碰控件的判断,当手指按下,手指移动,手指抬起的状态分别记录圆形按钮的位置来进行控件的位置刷新和控件当前状态的改变。
附上源码:
/**
* 自定义控件: 滑动开关.
*
* @author Dch
* @since 2015年7月29日 11:48:07
*
*/
public class ToggleButton extends View {
private boolean currentState = false; // 当前开关的状态, 默认为: 关闭
private Bitmap mSwitchBackground; // 开关的背景图片(false)
private Bitmap mSwitchBackgroundTrue; // 开关的背景图片(true)
private Bitmap mSlideButtonBackground; // 滑动块的图片
private int currentX; // 当前移动时x轴的值.
private boolean isTouching = false; // 是否正在触摸滑动中, 默认为: 没有正在滑动
private OnToggleStateChangedListener mOnToggleStateChangedListener; // 用户回调的接口
/**
* 当在java代码中创建此对象时, 需要使用此构造函数.
*
* @param context
*/
public ToggleButton(Context context) {
super(context);
}
/**
* 当在xml代码中引用此自定义控件时, 需要使用此构造函数.
*
* @param context
* @param attrs
* 在布局文件中声明的各种属性.
*/
public ToggleButton(Context context, AttributeSet attrs) {
super(context, attrs);
// 自定义的命名空间
String namespace = "http://schemas.android.com/apk/res/com.zchz.app";
// 取出自定义属性中背景图片
int switchBackgroundID = attrs.getAttributeResourceValue(namespace, "switchBackgroundResource", -1);
int switchBackgroundTrueID = attrs.getAttributeResourceValue(namespace, "switchBackgroundTrueResource", -1);
// 取出自定义属性中滑动块图片
int slideButtonBackgroundID = attrs.getAttributeResourceValue(namespace, "slideButtonBackgroundResource", -1);
// 取出自定义属性中当前状态
currentState = attrs.getAttributeBooleanValue(namespace, "currentState", false);
setSwitchBackgroundResource(switchBackgroundID);
setSwitchBackgroundTrueResource(switchBackgroundTrueID);
setSlideButtonBackgroundResource(slideButtonBackgroundID);
}
/**
* 设置当前开关的背景图片id, 例: R.drawable.xxx
*
* @param switchBackground
*/
public void setSwitchBackgroundResource(int switchBackground) {
mSwitchBackground = BitmapFactory.decodeResource(getResources(), switchBackground);
}
/**
* 设置当前开关的背景图片id, 例: R.drawable.xxx
*
* @param switchBackground
*/
public void setSwitchBackgroundTrueResource(int switchBackground) {
mSwitchBackgroundTrue = BitmapFactory.decodeResource(getResources(), switchBackground);
}
/**
* 设置滑动块的图片id, 例: R.drawable.xxx
*
* @param slideButtonBackground
*/
public void setSlideButtonBackgroundResource(int slideButtonBackground) {
mSlideButtonBackground = BitmapFactory.decodeResource(getResources(), slideButtonBackground);
}
/**
* 设置当前开关的状态.
*
* @param b
*/
public void setCurrentState(boolean b) {
this.currentState = b;
}
public boolean getCurrentState() {
return currentState;
}
/**
* 当前控件需要测量宽和高时, 触发此方法.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 设置当前控件的宽和高和背景图片的宽和高一样.
if (currentState) {
setMeasuredDimension(mSwitchBackgroundTrue.getWidth(), mSwitchBackgroundTrue.getHeight());
} else {
setMeasuredDimension(mSwitchBackground.getWidth(), mSwitchBackground.getHeight());
}
}
/**
* 当前控件需要绘制出来时, 调用此方法.
*
* @param canvas
* 画画板, 使用canvas所画出来的东西, 都是作为当前控件, 在屏幕上显示.
*/
@Override
protected void onDraw(Canvas canvas) {
// 1. 把背景图片平铺在当前控件中.
if (currentState) {
canvas.drawBitmap(mSwitchBackgroundTrue, 0, 0, null);
} else {
canvas.drawBitmap(mSwitchBackground, 0, 0, null);
}
if (isTouching) { // 当前正在拖动中, 应该根据currentX来处理滑动块的位置.
// 根据当前x轴的值, 来动态的修改滑动块的位置.
int left = currentX - mSlideButtonBackground.getWidth() / 2;
if (left < 0) { // 超出了左边界, 应该一直设置为0
left = 0;
} else if (left > mSwitchBackground.getWidth() - mSlideButtonBackground.getWidth()) {
left = mSwitchBackground.getWidth() - mSlideButtonBackground.getWidth();
}
canvas.drawBitmap(mSlideButtonBackground, left, 0, null);
} else { // 当前没有触摸滑动中, 需要根据currentState来处理滑动块的位置.
// 2. 根据当前的状态currentState来绘制滑动块的位置.
if (currentState) {
// 当前开关处于打开的状态, 把滑动块画在当前控件的右边.
int left = mSwitchBackground.getWidth() - mSlideButtonBackground.getWidth();
canvas.drawBitmap(mSlideButtonBackground, left, 0, null);
} else {
// 当前开关处于关闭的状态, 把滑动块画在当前控件的左边.
canvas.drawBitmap(mSlideButtonBackground, 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:
currentX = (int) event.getX();
break;
case MotionEvent.ACTION_UP:
isTouching = false;
currentX = (int) event.getX();
// 判断当前偏向于那一边
int center = mSwitchBackground.getWidth() / 2;
// 如果当前滑动块的中心点 > 背景图片中心点的位置时, 把当前状态置为true, 是打开的状态.
// 如果当前滑动块的中心点 < 背景图片中心点的位置时, 把当前状态置为false, 是关闭的状态.
boolean state = currentX > center; // 最新的状态
// 当前是抬起, 判断当前状态是否已经改变了, 如果改变, 调用用户的回调接口把改变后的状态, 传递过去.
if (state != currentState && mOnToggleStateChangedListener != null) {
// 当前状态已经被修改了, 需要回调用户.
mOnToggleStateChangedListener.onToggleStateChanged(state);
}
// 把最新的状态赋值给当前类的成员变量: 当前开关的状态.
currentState = state;
break;
default:
break;
}
// 手动触发onDraw方法的重绘.
invalidate(); // 刷新当前控件, 会引起onDraw方法调用.
return true; // 自己处理滑动的事件
}
public void setOnToggleStateChangedListener(OnToggleStateChangedListener listener) {
this.mOnToggleStateChangedListener = listener;
}
/**
* @author andong 当开关状态改变时的监听事件.
*/
public interface OnToggleStateChangedListener {
/**
* 开关状态改变时, 触发此方法.
*
* @param currentState
* true 打开, false 关闭
*/
public void onToggleStateChanged(boolean currentState);
}
}
在代码中使用:
根布局: xmlns:app="http://schemas.android.com/apk/res/com.zchz.app"
<com.example.ToggleButton
android:layout_width="50dp"
android:layout_height="16dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="15dp"
app:currentState="false"
app:slideButtonBackgroundResource="@drawable/gesture_button"
app:switchBackgroundResource="@drawable/gesture_bg1"
app:switchBackgroundTrueResource="@drawable/gesture_bg2" />