欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

自定义View,自定义Switch样式

程序员文章站 2022-07-13 16:51:57
...

最近在项目中要用到一个开关功能,控制是否要打开app的手势密码功能,想到android自带的两个原生控件Switch与ToggleButton,根据ui设计图最后选择了使用Switch。

由于不同的系统有自己定义的不同的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方法不清楚可以自行去查阅。

附上效果图:

自定义View,自定义Switch样式

自定义View,自定义Switch样式