由于不同的系统有自己定义的不同的Switch样式,所以导致一个问题,在不同的设备上显示出来的switch样子并不是一样子的,并且如果你的ui设计师很注重你的还原度的话,使用默认的Switch基本上是不可能实现的。

刚开始想实现这个功能是想通过自定义样式去实现一个统一的展示ui,自定义样式的步骤:


1.Switch控件支持设置Switch中的thumb,也就是里面那个可以滑动的部分,属性为android:thumb="";然后还有Switch的背景,不同选中状态有不同的背景颜色,属性为android:track=""。Switch本身也有checked状态,这就不免让我们想到了radiobutton控件,通过代码设置选择器按钮来实现不同的状态下的不同展示效果。下面上代码:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
	<item android:width="15dp" android:height="15dp" android:gravity="center_vertical|right" android:right="3dp" android:left="3dp">
		<shape android:shape="rectangle">
			<corners android:radius="100dp"/>
			<solid android:color="@color/dab96b"/>
		</shape>
	</item>
</layer-list>



通过width和height设置中心滑动圆的大小,left和right设置圆到边框的距离(ps:经过试验,当radius设置大小大于长款中的最小值时,默认就会让小的那一边变成半圆,如果长款相等的话那就成了一个圆形,大家可以自己试试)。

这个是选中的情况下滑动圆的样式,不选中下的样式就不贴出来了。然后是背景:


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
	android:shape="rectangle">
	<corners android:bottomLeftRadius="12dp" android:bottomRightRadius="12dp"
	         android:topLeftRadius="12dp" android:topRightRadius="12dp"/>
	<solid android:color="@color/eddcb5"/>
	<size android:height="24dp" android:width="38dp"/>
</shape>




样式也差不多,通过<size>标签设置背景的大小,记得与上面滑动圆进行匹配。

样式写完了,最后只需要用到Switch样式里面就好了,效果也成功出来了。然而你以为这样就完了吗,不可能的。。


android最经常遇到的问题就是是配上的问题了,在现在一般的手机上这里的样式展示完全是没有问题的。然而如果版本过低的话,很多情况下size所设置的宽高是不起作用的,这种情况在一些机型上很明显,所以你的样式在这种设备上面展示下来的话根本不行,完全没有效果,还不如用原生的。。


所以再经历这么个问题以后,我选择了另外一种方案,通过自定义view来实现一个switch开关。

首先考虑自定义一个view需要哪些东西:

1.需要画外边框

2.需要画里面滑动的圆点

3.需要画里面的填充色

看上去其实挺简单的,实际上也确实比较简单,不过在自定义的过程中需要记得把动画加上去,因为switch切换状态的过程是有一个动画效果的。其他话不多说,上代码:


public class Switch extends View {
    private Context mContext;
    private int mHeight,mWidth;
    private Path mPath;
    private Paint mStrokePaint;         //边框画笔
    private Paint mSolidPaint;         //填充色画笔
    private Paint mCirclePaint;        //小圆球画笔
    private float mMarginLeft;                  //小圆球到左边距离
    private boolean mIsCheck;          //是否被选中
    private final static int CIRCLEPADDING = DensityUtils.dp2px(TApplication.getInstance(),2);
    private int mCircleWidth;
    private int mDefaultSolidColor,mTargetSolidColor;
    private int mDefaultStrokeColor,mTargetStrokeColor;
    private int mDefaultCircleColor,mTargetCircleColor;
    private int mSolidColor_;
    private int mCircleColor_;
    private int mStrokeColor_;
    private ObjectAnimator mTranslateAnim, mSolidColorAnim,mCircleColorAnim,mStrokeColorAnim;
    private int mStrokeWidth;
    private int mRealWidth;
    private int mRealHeight;

    public interface OnCheckedChange{
        void onCheckChange(boolean isChecked);
    }

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

    public Switch(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public Switch(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        TypedArray a = mContext.obtainStyledAttributes(attrs,R.styleable.Switch);
        mDefaultStrokeColor = a.getColor(R.styleable.Switch_default_stroke_color, Color.BLACK);
        mTargetStrokeColor = a.getColor(R.styleable.Switch_target_stroke_color, Color.BLACK);
        mDefaultCircleColor = a.getColor(R.styleable.Switch_default_circle_color, Color.RED);
        mDefaultSolidColor = a.getColor(R.styleable.Switch_default_solid_color, Color.WHITE);
        mTargetCircleColor = a.getColor(R.styleable.Switch_target_circle_color, Color.RED);
        mTargetSolidColor = a.getColor(R.styleable.Switch_target_solid_color, Color.WHITE);
        mIsCheck = a.getBoolean(R.styleable.Switch_checked,false);
        mSolidColor_ = mIsCheck ? mTargetSolidColor : mDefaultSolidColor;
        mCircleColor_ = mIsCheck ? mTargetCircleColor : mDefaultCircleColor;
        mStrokeColor_ = mIsCheck ? mTargetStrokeColor : mDefaultStrokeColor;
        mTranslateAnim = initAnim("mMarginLeft",new FloatEvaluator());
        mSolidColorAnim = initAnim("mSolidColor_",new ArgbEvaluator());
        mCircleColorAnim = initAnim("mCircleColor_",new ArgbEvaluator());
        mStrokeColorAnim = initAnim("mStrokeColor_",new ArgbEvaluator());

        mStrokeWidth = DensityUtils.dp2px(mContext, 2f);
        mMarginLeft = 0;
        mPath = new Path();
        mCirclePaint = new Paint();
        mStrokePaint = new Paint();
        mSolidPaint = new Paint();
        initPaint(mCirclePaint,mSolidPaint,mStrokePaint);
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mTranslateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mMarginLeft = (float)(animation.getAnimatedValue());
                postInvalidate();
            }
        });
        mCircleColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCircleColor_ = (int) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        mSolidColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mSolidColor_ = (int) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        mStrokeColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mStrokeColor_ = (int) animation.getAnimatedValue();
                postInvalidate();
            }
        });
    }

    private ObjectAnimator initAnim(String propertyName, TypeEvaluator evaluator){
        ObjectAnimator anim = new ObjectAnimator();
        anim.setTarget(this);		//这里千万记住别写错,楼主因为写成了setObjectValue被坑了好几个小时
        anim.setPropertyName(propertyName);
        anim.setEvaluator(evaluator);
        return anim;
    }

    public void initPaint(Paint... paints){
        for (int i = 0; i < paints.length; i++) {
            paints[i].setAntiAlias(true);
            paints[i].setStrokeWidth(mStrokeWidth);
        }
    }

    public void startAnim(boolean isCheck){
        if (isCheck){
            mCircleColorAnim.setIntValues(mDefaultCircleColor,mTargetCircleColor);
            mTranslateAnim.setFloatValues(0,mWidth - mHeight);
            mSolidColorAnim.setIntValues(mDefaultSolidColor,mTargetSolidColor);
            mStrokeColorAnim.setIntValues(mDefaultStrokeColor,mTargetStrokeColor);
        }else {
            mCircleColorAnim.setIntValues(mTargetCircleColor,mDefaultCircleColor);
            mTranslateAnim.setFloatValues(mWidth - mHeight,0);
            mSolidColorAnim.setIntValues(mTargetSolidColor,mDefaultSolidColor);
            mStrokeColorAnim.setIntValues(mTargetStrokeColor, mDefaultStrokeColor);
        }
        AnimatorSet set = new AnimatorSet();
        set.setDuration(200);
        set.setInterpolator(new AccelerateDecelerateInterpolator());
        set.playTogether(mTranslateAnim,mCircleColorAnim,mSolidColorAnim,mStrokeColorAnim);
        set.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mWidth == 0){
            mWidth = getWidth();
            mHeight = getHeight();
            mRealWidth = mWidth - mStrokeWidth * 2;
            mRealHeight = mHeight - mStrokeWidth * 2;
            mMarginLeft = mIsCheck ? mWidth - mHeight : 0;
            mCircleWidth = mHeight - CIRCLEPADDING * 2 - mStrokeWidth * 2;
        }
        if (mWidth <= 0 || mHeight <= 0)return;
        mPath.reset();
        mSolidPaint.setColor(mSolidColor_);
        mStrokePaint.setColor(mStrokeColor_);
        mCirclePaint.setColor(mCircleColor_);
        mPath.addRoundRect(mStrokeWidth,mStrokeWidth,mRealWidth + mStrokeWidth,mRealHeight + mStrokeWidth,mRealHeight / 2,mRealHeight / 2, Path.Direction.CW);
        canvas.drawPath(mPath,mStrokePaint);
        canvas.drawPath(mPath,mSolidPaint);
        canvas.drawCircle(mHeight / 2.0f + mMarginLeft,mHeight / 2f,mCircleWidth / 2,mCirclePaint);
    }

    public void setCheck(boolean checked){
        this.setCheck(checked,true);
    }

    private void setCheck(boolean checked,boolean fromOut){
        boolean flag = mIsCheck == checked;
        mIsCheck = checked;
        if (mListener != null && !flag){
            mListener.onCheckChange(mIsCheck);
        }
        if (!fromOut){
            startAnim(mIsCheck);
        }else {
            mSolidColor_ = mIsCheck ? mTargetSolidColor : mDefaultSolidColor;
            mCircleColor_ = mIsCheck ? mTargetCircleColor : mDefaultCircleColor;
            mStrokeColor_ = mIsCheck ? mTargetStrokeColor : mDefaultStrokeColor;
            mMarginLeft = mIsCheck ? mWidth - mHeight : 0;
            postInvalidate();
        }
    }

    public float getMarginLeft() {
        return mMarginLeft;
    }

    private OnCheckedChange mListener;
    public void setOnCheckedChangeListener(OnCheckedChange listener){
        mListener = listener;
    }

    public void setMarginLeft(float marginLeft) {
        mMarginLeft = marginLeft;
    }

    public int getSolidColor_() {
        return mSolidColor_;
    }

    public void setSolidColor_(int solidColor_) {
        mSolidColor_ = solidColor_;
    }

    public int getCircleColor_() {
        return mCircleColor_;
    }

    public void setCircleColor_(int circleColor_) {
        mCircleColor_ = circleColor_;
    }

    public int getStrokeColor_() {
        return mStrokeColor_;
    }

    public void setStrokeColor_(int strokeColor_) {
        mStrokeColor_ = strokeColor_;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (mTranslateAnim.isRunning()){
                    return false;
                }
                return true;
            case MotionEvent.ACTION_CANCEL:
                return false;
            case MotionEvent.ACTION_UP:
                setCheck(!mIsCheck,false);
                return true;
        }
        return true;
    }
}




这里用到了四个动画,一个是滑块滑动的动画,一个是滑块颜色的渐变动画,还有事边框颜色的渐变动画以及填充色的颜色渐变动画,这些颜色都可以通过xml设置自定义属性设置进来。这里有个地方需要注意,楼主标注的地方有两个问题,

1:如果你使用的是setObjectValue()方法的话在现在主流的机型上是不会有问题的,可以很好的展示出来,但是如果你使用的是低版本的手机,那么就会提示你NullPointerException。这很容易误导是因为版本兼容性问题,其实并不是,属性动画确实存在一些版本兼容性的问题,不过是使用颜色动画的时候使用了ofArgb这个方法去初始化一个颜色动画,那么就会有这个问题。因为ofArgb是api21的方法,其他的那些兼容性问题则是要深究到android3.*版本,现在市场上这些版本基本没有了,如果实在要适配的话可以选择添加一个三方包NineOldAndroids这样就可以兼容android3.*以及以下版本。

2:path中有很多方法也是属于api21的方法比如上方的path.addRoundRect()方法,楼主这里后面通过使用path.lineTo()配合path.arcTo()方法画出来了一个类似操场跑道的图案,path.addRoundRect()也是添加一个跑道图案这种效果的方法。使用者请注意path.arcTo(float left,float top,float right,float bottom,float startAngle,float sweepAngle,boolean forceMoveTo)这个方法才是api11的方法,其他两个重载都是属于api21的,也会存在兼容性问题,使用的时候还请注意,如果大家对一些api方法不清楚可以自行去查阅。

附上效果图:

ios 自定义switchbutton android 自定义switch_ios 自定义switchbutton

ios 自定义switchbutton android 自定义switch_自定义view_02