仿Ios滑动开关按钮

效果图

实现思路

外层是一个圆角矩形,内层则是一个小圆形,如果细细观看,会发现外层还是有一层灰色的边框的,至少滑动的动画,我们可以考虑Scroller或者其他的动画效果。

具体代码

这里我们使用了自定义属性,所以要在values目录下建立一个attrs.xml文件,

 开启时背景的颜色
 关闭时背景的颜色
 圆形的颜色
 动画执行的时间
 默认宽度
 默认高度

自定义一个控件这里我们就叫ButtonView,可以继承于Button也可以继承于View,这里我们就继承自View,正好复习一个看看Android内部如何实现的单击事件,看一下其原理。

重写第构造函数,这里重写构造函数我习惯重写第三个构造函数,但是遇到一些问题,记录一下,避免大家犯同样的错误。

例如

public ButtonView(Context context) {
    this(context,null);
}

public ButtonView(Context context, AttributeSet attrs) {
    this(context, attrs,-1);
    //初始化的一些代码
}

public ButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

这么写本身是没问题,运行起来效果也是没问题的,但是当我们使用findViewById()的话获取到的则是null,其实这里只需要把-1改成0即可,至于为什么,大家可以参考一下Android的源代码。这里我就直接重写第二个构造函数了。

public ButtonView(Context context, AttributeSet attrs) {
    super(context, attrs);
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.buttonView);//加载自定义的属性
    offBackground=a.getColor(R.styleable.buttonView_offBackground, Color.parseColor("#F6F6F6"));//获取关闭的颜色并设置默认值
    onBackground=a.getColor(R.styleable.buttonView_onBackground, Color.parseColor("#38DA4E"));//获取开启的颜色并设置默认值
    time=a.getInteger(R.styleable.buttonView_time, 600);//时间这里代表的是毫秒数
    circleColor=a.getColor(R.styleable.buttonView_circleColor, Color.parseColor("#D7D7D7"));//圆形颜色
    layout_height=a.getDimension(R.styleable.buttonView_layout_height, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, context.getResources().getDisplayMetrics()));//默认高度
    layout_width=a.getDimension(R.styleable.buttonView_layout_width, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60, context.getResources().getDisplayMetrics()));//默认宽度
    mPaint=new Paint();//对画笔进行一些初始化
    mConfig=ViewConfiguration.get(context);//ViewConfiguration内有一个Android内部的初始值,比如多长时间是单击时间,多长时间是长按时间。
}

我们现在需要重写onMeasure方法,测量一下其宽高

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int heightMode= MeasureSpec.getMode(heightMeasureSpec);
    int heightSize= MeasureSpec.getSize(heightMeasureSpec);
    int widthMode= MeasureSpec.getMode(widthMeasureSpec);
    int widthSize= MeasureSpec.getSize(widthMeasureSpec);
    int measureView_width = measureView(widthMode, widthSize, layout_width);
    int measureView_height= measureView(heightMode, heightSize, layout_height);
    //计算最大移动位置
    max_move=(int) (layout_width-RADIO);
    //计算最小移动位置
    min_move=(int)(circleX+layout_height/2-4);
    setMeasuredDimension(measureView_width, measureView_height);
}

private int measureView(int mode,int size,float defaultSize){
    switch (mode) {
    case MeasureSpec.UNSPECIFIED:
    case MeasureSpec.AT_MOST:
        if(size>defaultSize){
            size=(int) Math.ceil(defaultSize);
        }
        return MeasureSpec.makeMeasureSpec(size,MeasureSpec.EXACTLY);
    case MeasureSpec.EXACTLY:
        return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
    }
    return -1;
}

int min_move=0;最小移动的位置
int max_move=0;最大移动的位置
int current_move=min_move;当前移动的位置
int currentColor=offBackground;当前渐变的颜色
static final int RADIO=30;默认矩形圆角大小
int circleX=0;圆形X轴

这里要说一下MeasureSpec内部有三个值

  1. UNSPECIFIED 子控件想多大就多大
  2. AT_MOST 在最大范围内,或者设置了warp_content
  3. EXACTLY 设置了固定值,或者Match_Parent

而makeMeasureSpec()根据提供的大小值和模式创建一个测量值
setMeasuredDimension需要的就是两个测量值作为参数。

重写onDraw方法进行绘制内容

@Override
protected void onDraw(Canvas canvas) {
    // TODO Auto-generated method stub
    super.onDraw(canvas);
    mPaint.reset();//重置画笔
    mPaint.setAntiAlias(true);//设置抗锯齿
    mPaint.setColor(currentColor);//设置画笔颜色
    mPaint.setDither(true);//设置防抖动
    canvas.drawRoundRect(new RectF(0, 0, layout_width, layout_height), RADIO, RADIO, mPaint);//绘制圆角矩形
    mPaint.setStyle(Style.STROKE);//绘制边框
    mPaint.setColor(Color.parseColor("#DADADA"));//设置边框的颜色
    canvas.drawRoundRect(new RectF(0, 0, layout_width, layout_height), RADIO, RADIO, mPaint);//绘制矩形
    mPaint.setStyle(Style.FILL);//设置填充效果
    mPaint.setColor(circleColor);//设置圆形颜色
    canvas.drawCircle(current_move, layout_height/2, layout_height/2-4, mPaint);//绘制圆
}

单击事件

这样基本上就已经成型了,下面我们品位一下Android的单击事件如何形成的。

float x=0;
float y=0;
boolean isClick=false;
Handler mHandler=new Handler();
ClickRunAble clickRunable=new ClickRunAble();

class ClickRunAble implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        isClick=true;
    }
}

private void isMove(float x,float y,int touchSlop){
    if(Math.abs(this.x-x)>touchSlop||Math.abs(this.y-y)>touchSlop){
        isClick=false;
    }
}

上面代码执行进行准备工作,手按下然后松开即单击事件,我们重写onTouchEvent(MotionEvent event)

按下时的动作
x=event.getX();//获取按下的X轴
y=event.getY();//获取按下的Y轴
isClick=false;//默认不是单击
mHandler.postDelayed(clickRunable, mConfig.getScaledTouchSlop());//指定多长时间后 执行ClickRunAble内部代码,其实很简单就把把isClick设置成true

移动时的动作
float x=event.getX();
float y=event.getY();
isMove(x, y, mConfig.getScaledTouchSlop());如果移动距离大于ScaledTouchSlop则不是单击事件。

松开时
mHandler.removeCallbacks(clickRunable);
if(isClick){
    //单击事件所要执行的代码。。。
}
即可。

动画效果

这里动画效果采用Scoller明显不是很好,我们需要不变变化两个值:颜色和位置

这里我们采用属性动画

//定义我们要操作的两个属性
private Property mColor=new    Property(Integer.class,"currentColor") {
    @Override
    public Integer get(ButtonView object) {
        return object.currentColor;
    }

    @Override
    public void set(ButtonView object, Integer value) {
        // TODO Auto-generated method stub
        object.currentColor=value;
        invalidate();
    }
};

private Property mMove=new Property(Integer.class,"current_move") {
    @Override
    public Integer get(ButtonView object) {
        // TODO Auto-generated method stub
        return object.current_move;
    }

    @Override
    public void set(ButtonView object, Integer value) {
        // TODO Auto-generated method stub
        object.current_move=value;
    }
};

//开启动画
public void startAnimator(int start,int end,int startColor,int endColor,int duration){
    set=new AnimatorSet();
    set.playTogether(ObjectAnimator.ofInt(this,mMove, start,end),ObjectAnimator.ofObject(this, mColor, new ArgbEvaluator(),startColor,endColor));
    set.setDuration(duration);
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            // TODO Auto-generated method stub
            isAnimatorStart=true;
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            isAnimatorStart=false;
            if(changeListener!=null){
                changeListener.onChange(isOn);//当动画完毕后,我们希望开启关闭都能做一些时间,此函数进行回调
            }
        }
    });
    set.setInterpolator(new DecelerateInterpolator());
    set.start();
}

上述对关键代码进行了梳理,具体请看源码。

源码

另外这里使用了属性动画,在Android3.0之前是没有效果的,这里可以使用nineoldandroids.jar兼容3.0之前的版本。

此Dome还有一些地方可以优化。

你可能感兴趣的:(Android)