自定义控件 - CustomToggleButton
需求
- 绘图:滑块 + 背景
- 自定义属性:isOpen - 布局或代码控制开关
- 动作:触摸滑动开关,点击开关
- 监听:OnToggleChangeListener - 开关状态改变监听器
构建
- 拷贝滑块和背景到资源目录
- 自定义控件继承 View
自定义参数
- 新建attrs.xml,定义参数isOpen,类型boolean
<resources>
<declare-styleable name="CustomToggleButton">
<attr name="isOpen" format="boolean"/>
</declare-styleable>
</resources>
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomToggleButton);
isOpen = typedArray.getBoolean(R.styleable.CustomToggleButton_isOpen, false);
typedArray.recycle();
加载数据
background = BitmapFactory.decodeResource(getResources(), R.drawable.ctb_switch_background);
button = BitmapFactory.decodeResource(getResources(), R.drawable.ctb_slide_button);
初始化
- 获取滑动最大范围
- 获取初始滑动距离
- 经过测量后,大小数值会变化,需要重新初始化
// max = background.getWidth() - button.getWidth();
// left = isOpen ? max : 0;
scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // 滑动临界值
加载完成
测量
设置控件大小
- 调用setMeasuredDimension设置控件大小,以背景大小为准
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = measureDimension(widthMeasureSpec, background.getWidth());
int height = measureDimension(heightMeasureSpec, background.getHeight());
setMeasuredDimension(width, height); // 测量完成后设置控件大小
}
- measureSpec 为32位二进制数,前2位表示Mode,后30位表示Size
- 一般测量方法
private int measureDimension(int measureSpec, int content) {
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: // 未指定,例如ScrollView
result = content;
break;
case MeasureSpec.EXACTLY: // 确定值,match_parent和确定的数值
result = size;
break;
case MeasureSpec.AT_MOST: // 最大值,wrap_content
result = Math.min(content, size); // 在最大可用值和内容的大小中取最小值
break;
}
return result;
}
缩放
- 测量完成,调用
onSizeChanged(int w, int h, int oldw, int oldh)
- 调整控件各部分大小
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
button = Bitmap.createScaledBitmap(button, button.getWidth() * w / background.getWidth(), button.getHeight() * h / background.getHeight(), true);
background = Bitmap.createScaledBitmap(background, w, h, true);
// 此时需初始化和大小相关的变量
max = background.getWidth() - button.getWidth();
left = isOpen ? max : 0;
}
布局
- 调用
onLayout(boolean changed, int left, int top, int right, int bottom)
,对子View进行布局
绘图
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(background, 0, 0, null);
canvas.drawBitmap(button, left, 0, null);
}
动作
-
MotionEvent.ACTION_DOWN
/MotionEvent.ACTION_MOVE
/MotionEvent.ACTION_UP
分别代表触摸移动和放开
-
return true
消费事件
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
滑块滑动开关
-
getX()
/getY()
相对View左上角坐标; getRawX()
/getRawY()
相对屏幕左上角坐标
-
MotionEvent.ACTION_DOWN
-
MotionEvent.ACTION_MOVE
-
MotionEvent.ACTION_UP
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
float dx = moveX - startX;
left += dx;
// 限制范围
if (left < 0) {
left = 0;
}
if (left > max) {
left = max;
}
// 移动重绘
invalidate();
startX = moveX;
break;
case MotionEvent.ACTION_UP:
isOpen = left > max / 2;
change();
break;
}
return true;
}
private void change() {
// 按开关重绘
left = isOpen ? max : 0;
invalidate();
}
点击空白开关
- 通过时间和距离区分点击和滑动
- 通过isOpen状态和释放坐标判断点击位置
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
startTime = SystemClock.uptimeMillis();
break;
case MotionEvent.ACTION_MOVE:
...
break;
case MotionEvent.ACTION_UP:
float upX = event.getX();
if (upX - startX < scaledTouchSlop && SystemClock.uptimeMillis() - startTime < 500) {
// 点击
if (isOpen) {
// 状态开,点击关
isOpen = !(upX > 0 && upX < max);
} else {
// 状态关,点击开
isOpen = upX > button.getWidth() && upX < background.getWidth();
}
} else {
// 滑动
isOpen = left > max / 2;
}
change();
break;
}
return true;
}
监听
- 创建监听器
- 比对按压和释放的开关状态判断开关状态是否改变
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
...
isCurrent = isOpen;
break;
...
}
return true;
}
private void change() {
// 按开关重绘
left = isOpen ? max : 0;
invalidate();
// 有改变回调监听
if (isCurrent != isOpen && listener != null) {
listener.change(isOpen);
}
}
// 监听器
private OnStateChangeListener listener;
public interface OnStateChangeListener {
void change(boolean isOpen);
}
public void setOnStateChangeListener(OnStateChangeListener listener) {
this.listener = listener;
}
开关方法
public boolean isOpen() {
return isOpen;
}
public void open() {
isCurrent = isOpen;
isOpen = true;
change();
}
public void close() {
isCurrent = isOpen;
isOpen = false;
change();
}
使用
布局
<com.library.customtogglebutton.CustomToggleButton
android:id="@+id/custom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:isOpen="true" />
监听
CustomToggleButton customToggleButton = (CustomToggleButton) findViewById(R.id.custom);
customToggleButton.setOnStateChangeListener(new CustomToggleButton.OnStateChangeListener() {
@Override
public void change(boolean isOpen) {
Toast.makeText(CtbActivity.this, isOpen ? "开" : "关", Toast.LENGTH_SHORT).show();
}
});
开关
if (customToggleButton.isOpen()) {
customToggleButton.close();
} else {
customToggleButton.open();
}