最近Android开发中遇到了一个开关的需求,就自己绘制了一个。
不多bb,直接看效果图:
由上图可看出,空间上方和下方由两个TextView构成,属于Android系统控件就不谈了,接下来介绍中间的自定义开关的绘制原理。
先贴出完整代码,如下:
public class SlideButton extends View {
public interface OnChangeListener {
//回调接口
void onChanged();
}
private OnChangeListener listener;
int width,height;
int r;
private Path groundPath;
private Paint mPaint;
float downY;
float circleX,circleY;
Region region;
public boolean open = false;
public SlideButton(Context context) {
super(context);
init();
}
public SlideButton(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SlideButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawGround(canvas,mPaint);
drawRegion(canvas,mPaint);
drawCircle(canvas,mPaint);
}
private void drawRegion(Canvas canvas,Paint paint) {
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.FILL);
region.setPath(groundPath,new Region(0, (int) circleY, width, (int) (circleY+height)) );
RegionIterator iter = new RegionIterator(region);
Rect r = new Rect();
while (iter.next(r)) {
canvas.drawRect(r, paint);
}
}
private void drawCircle(Canvas canvas,Paint paint) {
//绘制圆形按钮
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(circleX,circleY,0.5f*width,paint);
}
private void drawGround(Canvas canvas,Paint paint) {
//绘制操场形状边框
paint.setColor(Color.GRAY);
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.STROKE);
groundPath.addRoundRect(0.5f*width-r, 0.5f*width-r,0.5f*width+r,height-0.5f*width+r,r,r, Path.Direction.CCW);
canvas.drawPath(groundPath,paint);
}
private void init() {
mPaint = new Paint();
groundPath = new Path();
region = new Region();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
r = (int)(0.3f*width);
circleX = 0.5f*width;
circleY = height-0.5f*width;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
downY = event.getY();
break;
}
case MotionEvent.ACTION_MOVE: {
float deltaY = event.getY()-downY;
circleY += deltaY;
if (circleY < 0.5f*width) {
//到顶部
circleY = 0.5f*width;
open = true;
listener.onChanged();
} else if (circleY > height - 0.5f*width) {
circleY = height - 0.5f*width;
open= false;
listener.onChanged();
}
break;
}
}
postInvalidate();
return true;
}
public void setOnChangeListener (OnChangeListener onChangeListener) {
this.listener = onChangeListener;
}
}
对其做一下简单解释:
类变量和构造函数就不谈了,就是做了一些初始化的工作,都是自定义控件的套路。在重写的onSizeChanged()方法中给一些参数赋值:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
r = (int)(0.3f*width);
circleX = 0.5f*width;
circleY = height-0.5f*width;
}
width和height为控件宽高,r为圆角矩形的圆角半径,circleX和circleY为白色圆形按钮圆心坐标
首先看重写的onDraw方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawGround(canvas,mPaint);
drawRegion(canvas,mPaint);
drawCircle(canvas,mPaint);
}
onDraw方法里调用了3个方法,drawGround()方法绘制的是开操场形状的外围,drawRegion()方法绘制的是绿色的区域(即效果图中白色按钮和操场外圈围成的区域),drawCircle()绘制的是白色的滑动按钮。先来看drawGround()方法:
private void drawGround(Canvas canvas,Paint paint) {
//绘制操场形状边框
paint.setColor(Color.GRAY);
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.STROKE);
groundPath.addRoundRect(0.5f*width-r, 0.5f*width-r,0.5f*width+r,height-0.5f*width+r,r,r, Path.Direction.CCW);
canvas.drawPath(groundPath,paint);
}
首先设置画笔的颜色,线宽和风格,接着调用的是Path类提供的一个addRoundRect方法,就是添加一个圆角矩形,画出来类似于这样的:
外面的框是控件实际大小,可以看到里面的圆角矩形要比实际大小小一些的(因为白色按钮的宽度要比圆角矩形宽度大一些)。
再来看drawCircle()方法:
private void drawCircle(Canvas canvas,Paint paint) {
//绘制圆形按钮
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(circleX,circleY,0.5f*width,paint);
}
通过canvas类提供的drawCircle()方法画了一个圆圈,注意要先把画笔风格设置为FILL_AND_STROKE,否则只会画出圆的外圈。画完之后是大概这样是儿的:
最后是drawReigon()方法:
private void drawRegion(Canvas canvas,Paint paint) {
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.FILL);
region.setPath(groundPath,new Region(0, (int) circleY, width, (int) (circleY+height)) );
RegionIterator iter = new RegionIterator(region);
Rect r = new Rect();
while (iter.next(r)) {
canvas.drawRect(r, paint);
}
}
这里面为了绘制出绿色的区域,采用了Reigon类里面的setPath()方法,该方法有两个参数,第一个是路径,第二个是区域。次方法的作用是将路径围成的区域和第二个参数的区域取交集,示意图如下:
绿色的是操场形区域(第一个参数),红色是第二个参数的区域,取交集得到图中黑色阴影部分的区域(图中省略了白色按钮为了看的更清楚一些),之后用矩形迭代的方式把这个阴影部分区域绘制出来。迭代方法参考启舰大佬的博客:自定义控件区域
这里面有个细节:drawCircle()方法是放在drawReigon()之后执行的,这样就可以让白色按钮将阴影区域的一小部分遮挡住,否则直接通过区域相交的方法构造效果图中的绿色区域是比较复杂的,效果图如下:
接下来就是对控件添加触摸滑动的监听了:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
downY = event.getY();
break;
}
case MotionEvent.ACTION_MOVE: {
float deltaY = event.getY()-downY;
circleY += deltaY;
if (circleY < 0.5f*width) {
//到顶部
circleY = 0.5f*width;
open = true;
listener.onChanged();
} else if (circleY > height - 0.5f*width) {
circleY = height - 0.5f*width;
open= false;
listener.onChanged();
}
break;
}
}
postInvalidate();
return true;
}
在 MotionEvent.ACTION_MOVE中根据白色按钮圆心纵坐标circleY的位置来判断按钮是否到顶部或者底部。到顶部用Boolean变量open设为true表示开关被打开,到底部为false表示开关没被打开,之后用postInvalidate()让界面重绘。
最后给控件添加监听器,当开关打开或关闭会执行回调函数,这样就可以实现当开关打开让上面的TextView变绿色,关闭让下面TextView变绿色了(当然得在activity中添加相关代码,这里只是给了一个接口和回调函数)。
public interface OnChangeListener {
//回调接口
void onChanged();
}
private OnChangeListener listener;
public void setOnChangeListener (OnChangeListener onChangeListener) {
this.listener = onChangeListener;
}
到这里一个简单的开关功能就实现了,代码较少,就不传到GitHub上了