Android原生的开关,单从视觉上来看已经不能满足我们日常App的需求了,所以很多时候,App的开关按键会选择自定义,那么今天,我就分享一下我的学习成果。
我今天学习到的自定义开关,它实现了这样的效果,第一:可以 通过点击选择开关;第二,可以通过滑动选择开关。先看一下效果gif:
这是通过点击选择开关的gif效果:
这是通过滑动选择开关的效果:
其实学习过后发现,实现自定义的原理是非常的简单的,这个自定义的开关,其实归根到底是两张图片:
那张带有“开关”字样的图片(以下简称A)拿来打底做背景,按钮样式的图片(以下简称B)就盖在它的上方,然后对B实现点击/滑动事件之后,就刚好可以盖住背景A,从而达到了我们一个视觉上开或者关的效果。
以下是代码部分:
public class MyButton extends View implements View.OnClickListener {
private Bitmap bgBm;//背景
private Bitmap slidingBm;//滑动开关
private int slidingMax;//滑动的最大距离
private Paint paint;//画笔
private float slidingLeft;//距离左边的距离
private float startX;//开始时X的位置
private boolean isOpen = false;//true为开 false为关
private boolean isClick = true;//true点击事件生效 false滑动事件生效
private float lastX;//移动中X的最后位置
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
paint = new Paint();
paint.setAntiAlias(true);//设置抗锯齿
bgBm = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
slidingBm = BitmapFactory.decodeResource(getResources(), R.drawable.button_background);
slidingMax = bgBm.getWidth() - slidingBm.getWidth();
setOnClickListener(this);
}
/**
* 视图测量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(bgBm.getWidth(), bgBm.getHeight());//通过setMeasuredDimension()最终去确定视图大小,保存计算结果
}
/**
* 绘制
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// canvas.drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
canvas.drawBitmap(bgBm, 0, 0, paint);
canvas.drawBitmap(slidingBm, slidingLeft, 0, paint);
}
@Override
public void onClick(View view) {
if (isClick) {
isOpen = !isOpen;
flushView();
}
}
private void flushView() {
if (isOpen) {//开关处于开的状态
slidingLeft = slidingMax;
} else {
slidingLeft = 0;
}
invalidate();//会导致onDraw()执行 强制绘制
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = startX = event.getX();//记录按下时候的X坐标 用以判断是否是点击状态 如果lastX = startX,就意味着起始位置和结束位置是一个位置,说明用户只是点击了一下
isClick = true;
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();//记录移动最终结束值
float distanX = endX - startX;//计算偏移量
slidingLeft = slidingLeft + distanX;
//判断非法值
if (slidingLeft < 0) {
slidingLeft = 0;
} else if (slidingLeft > slidingMax) {
slidingLeft = slidingMax;
}
invalidate();//强制绘制
startX = event.getX();//还原数据
if (Math.abs(endX - lastX) > 5) {//如果说最后停留的X的位置-移动中的X的位置的绝对值>5个像素,我们就认为,它是滑动了的
isClick = false;
}
break;
case MotionEvent.ACTION_UP:
if (isClick == false) {//只有处于滑动状态下,才会走这里
if (slidingLeft > slidingMax / 2) {//这个判断是针对于,滑动到一半的情况,如果不做这个判断,控件滑动到一半,就会停留在一半的位置,就不好判断到底是关还是开了
isOpen = true;
} else {
isOpen = false;
}
flushView();
}
break;
}
return true;
}
}
那么代码之中,有两处地方是需要分析的:
第一处是initView()中的这行代码:
slidingMax = bgBm.getWidth() - slidingBm.getWidth();
那么这个slidingMax是怎么来的呢,见图说话:
看图之后就非常的清晰明了了,所谓的slidingMax,其实就是那个B图的最大可移动的位置,那么所谓的最大的可以移动的位置,无非就是B图的最边和A图的最边齐平,这样,它们俩相减的差,就刚好可以显示出来“开”或“关”(“关”在此我就不分析了,差不多的)。
case MotionEvent.ACTION_UP:
if (isClick == false) {//只有处于滑动状态下,才会走这里
if (slidingLeft > slidingMax / 2) {//这个判断是针对于,滑动到一半的情况,如果不做这个判断,控件滑动到一半,就会停留在一半的位置,就不好判断到底是关还是开了
isOpen = true;
} else {
isOpen = false;
}
flushView();
}
break;
在我们拖拽的过程中,难免会有一个开关状态改变的一个临界值,那么,所谓的临界值是什么呢,就是slidingMax/2,我们的想法是,即便我们没有人为的将按钮拖拽到最右边而是只拖拽到某个临界位置,也一样可以使它呈现一个开的状态,而不是像上图一样停留在中间,所以这个时候就做出case MotionEvent.ACTION_UP的判断了,if (slidingLeft > slidingMax / 2) {isOpen = true; }
布局之中,直接调用我们自定义的控件就可以了:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.honey.zidingyikaiguan.MainActivity">
<com.honey.zidingyikaiguan.MyButton
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
MainActivity中也是非常简单的设置了一下布局:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
代码中已经尽量的写注释了,如果有需要源码的同学可以自行点击下载:打开链接进行下载