最近重构代码,发现了之前偷懒遗留的一个问题。有一个控制设备开关机的控件,由于之前赶项目交期,匆匆忙忙直接在Activity中重写onTouch事件,效果虽然也实现了,但是肯定不是很好的,今天重新将这个小玩意重新封装成一个自定义控件,话不多说,先看看实现的效果。
其实看样子都知道,是一个蛮简单的自定义控件,至于为什么要写这篇博客呢,因为也有段时间没有搞自定义控件了,一时手痒,哈哈 = =,温故而知新,总结一下总归是有好处的。
既然是自定义控件,那么自然少不了自定义属性,先贴上attrs.xml代码。
这里定义了四个属性,分别是文本内容,文本字号,文本颜色以及滑动button的背景图片,如果有拓展需求的话,也可以很简单的拓展。
然后再看布局代码, 简单粗暴,就只有一个控件。
下面开始写我们的自定义控件,写代码之前,不妨先撸一撸思路。
- 我们这个自定义控件跟现有的控件实际上是没有什么联系的,所以我们这里自定义控件继承自View;
- 由于button是要随手势拖动的,所以我们肯定是要重写onTouchEvent方法的,在onTouchEvent中动态的更新button的X坐标值;
- 既然界面会涉及到重绘,所以我们肯定需要重写onDraw方法,为了实现button拖动,这里我们通过drawBitmap的方法动态绘制button;
- 由于我们这个控件会放到各个界面中去用,我们肯定就需要重写onMeasure方法,动态的确定button的宽高以及text的长度等;
- 外界需要知道控件滑动的结果,我们这里定义一个接口将状态回调出去;
思路撸完,上代码。
/**
* 自定义滑动开关
*
* Created by Horrarndoo on 2017/6/1.
*/
public class SlideSwitchView extends View {
private Bitmap slideButtonBitmap; // 滑块图片
private Paint mPaint; // 画笔
private float currentX; //当前滑动的x坐标
private int mBaseLineY; // text基准线
private String mTextContent; //text内容
public SlideSwitchView(Context context) {
this(context, null);
}
public SlideSwitchView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideSwitchView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initPaint();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideSwitchView);
setSlideButtonResource(ta.getResourceId(R.styleable.SlideSwitchView_slide_button, -1));
setText(ta.getString(R.styleable.SlideSwitchView_android_text));
setTextSize(ta.getDimension(R.styleable.SlideSwitchView_android_textSize,30));
setTextColor(ta.getColor(R.styleable.SlideSwitchView_android_textColor, Color.BLACK));
ta.recycle();
}
/**
* 初始化画笔
*/
private void initPaint() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.LEFT);
mPaint.setAntiAlias(true);
}
/**
* 初始化text居中基准线
*/
private void initTextBaseLine() {
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float top = fontMetrics.top;//为基线到字体上边框的距离,即上图中的top
float bottom = fontMetrics.bottom;//为基线到字体下边框的距离,即上图中的bottom
mBaseLineY = (int) (getMeasuredHeight() / 2 - top / 2 - bottom / 2);//基线中间点的y轴计算公式
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) {
int newWidth = (int) (slideButtonBitmap.getWidth() * 2 + getTextWidth());
if (width >= newWidth)
width = newWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
if (height < slideButtonBitmap.getHeight()) {
// 获得图片的宽高
int widthSlide = slideButtonBitmap.getWidth();
int heightSlide = slideButtonBitmap.getHeight();
float scaleHeight = height * 1.0f / slideButtonBitmap.getHeight();
Matrix matrix = new Matrix();
matrix.postScale(scaleHeight, scaleHeight);
slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
heightSlide, matrix, true);
invalidate();
}
}
if (slideButtonBitmap.getWidth() > (width - getTextWidth()) / 2) {
// 获得图片的宽高
int widthSlide = slideButtonBitmap.getWidth();
int heightSlide = slideButtonBitmap.getHeight();
float scaleWidth = (width - getTextWidth()) / 2 / slideButtonBitmap.getWidth();
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleWidth);
slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
heightSlide, matrix, true);
invalidate();
}
setMeasuredDimension(width, slideButtonBitmap.getHeight());
initTextBaseLine();
}
// Canvas 画布, 画板. 在上边绘制的内容都会显示到界面上.
@Override
protected void onDraw(Canvas canvas) {
// 1. 绘制text
canvas.drawText(mTextContent, slideButtonBitmap.getWidth(), mBaseLineY, mPaint);
// 2. 绘制滑块
if (isTouchMode) {
// 根据当前用户触摸到的位置画滑块
// 让滑块向左移动自身一半大小的位置
float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;
int maxLeft = getMeasuredWidth() - slideButtonBitmap.getWidth();
// 限定滑块范围
if (newLeft < 0) {
newLeft = 0; // 左边范围
} else if (newLeft > maxLeft) {
newLeft = maxLeft; // 右边范围
}
canvas.drawBitmap(slideButtonBitmap, newLeft, 0, mPaint);
} else {
//还原button位置
canvas.drawBitmap(slideButtonBitmap, 0, 0, mPaint);
}
}
boolean isTouchMode = false;
private OnSwitchStateUpdateListener onSwitchStateUpdateListener;
// 重写触摸事件, 响应用户的触摸.
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouchMode = true;
currentX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
currentX = event.getX();
break;
case MotionEvent.ACTION_UP:
isTouchMode = false;
currentX = event.getX();
float center = getMeasuredWidth() / 2.0f;
// 根据当前按下的位置, 和控件中心的位置进行比较.
boolean isStateChanged = currentX > center;
// 如果开关状态变化了, 通知界面
if (isStateChanged && onSwitchStateUpdateListener != null) {
onSwitchStateUpdateListener.onStateUpdate();
}
break;
default:
break;
}
// 重绘界面
invalidate(); // 会引发onDraw()被调用, 里边的变量会重新生效.界面会更新
return true; // 消费了用户的触摸事件, 才可以收到其他的事件.
}
/**
* 设置滑块图片资源
*
* @param slideButton 滑块图片资源
*/
public void setSlideButtonResource(int slideButton) {
slideButtonBitmap = BitmapFactory.decodeResource(getResources(), slideButton);
}
/**
* 设置text字号大小
*
* @param textSize text字号大小
*/
public void setTextSize(float textSize) {
mPaint.setTextSize(textSize);
mPaint.setStrokeWidth(textSize / 15.f);
}
/**
* 设置text内容
*
* @param text text内容
*/
public void setText(String text) {
mTextContent = text;
}
/**
* 设置text颜色
*
* @param color text颜色资源
*/
public void setTextColor(int color) {
mPaint.setColor(color);
}
/**
* 获取text文字宽度
*
* @return text文字宽度
*/
private float getTextWidth() {
return mPaint.measureText(mTextContent);
}
/**
* 获取text文字高度
*
* @return text文字高度
*/
private float getTextHeight() {
return mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top;
}
public interface OnSwitchStateUpdateListener {
// 状态回调
void onStateUpdate();
}
public void setOnSwitchStateUpdateListener(
OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}
}
这里我们主要看看onTouchEvent方法和onMeasure方法。
在onTouchEvent中,我们在ACTION_MOVE中不断的刷新button的X坐标,然后刷新button的位置,达到拖动button滑动的效果 ,最后在ACTION_UP中判断滑动位置是否超过控件一半位置来确定是否传递滑动状态变化给回调。
在onMeasure中,我们计算了几种情况。
- 首先要保证宽度为wrap_content的时候,控件要有滑动的空间,所以在wrap_content的时候,设置控件宽度值为button.width*2+text.width,保证控件最低的宽度值。
- 控件高度确定的情况下,button的宽高也根据控件宽高做相应比例的缩放,避免图片超出控件范围。
- 最后要计算图片宽度,保证view.width = button.width*2+text.width。
最后一步,在Activity中调用。
public class MainActivity extends AppCompatActivity {
private SlideSwitchView mSlideSwitchView;
private boolean mIsPowerOn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSlideSwitchView = (SlideSwitchView) findViewById(R.id.slide_switch_view);
mSlideSwitchView.setOnSwitchStateUpdateListener(new SlideSwitchView.OnSwitchStateUpdateListener() {
@Override
public void onStateUpdate() {
mIsPowerOn = !mIsPowerOn;
String content = mIsPowerOn ? "Slide to power off" : "Slide to power on";
mSlideSwitchView.setText(content);
}
});
}
}
附上完整demo地址:https://github.com/Horrarndoo/SwipeButton