Android-一步一步教你自定义IOS样式的UISwitch

在这一章节,我们将开始在Android上做一个IOS风格的UISwitch控件。这篇文章主要涉及如下知识点:

  1. Android属性动画Animator
  2. 自定义手势GestureDetector
  3. 自定义View属性attrs

首先,让我们来看一下效果图:

uiswitch

自定义View属性

接下来,我们要先新建IOSSwitchView类,继承自View类,重写其构造方法,代码如下:

/**
 * 高仿IOS风格的UISwitchView
 * Created by HanHailong on 15/10/15.
 */
public class IOSSwitchView extends View {
    public IOSSwitchView(Context context) {
        this(context, null);
    }

    public IOSSwitchView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

然后,自定义我们需要的View属性,代码如下:

<declare-styleable name="IOSSwitchView">
    <attr name="tintColor" format="reference|color" />
    <attr name="thumbTintColor" format="reference|color" />
    <attr name="strokeWidth" format="reference|dimension" />
    <attr name="isOn" format="reference|boolean" />
</declare-styleable>

其中,tintColor是Switch控件选中的颜色,,thumbTintColor是里面可拖动开关扳手的颜色,,strokeWidth是开关扳手的外边距宽度,isOn是开关的打开状态,true是打开状态,false是关闭状态。

代码里怎么接收在布局里设置的属性值呢?答案:TypedArray,我们继续看代码:

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IOSSwitchView);
       mTintColor = a.getColor(R.styleable.IOSSwitchView_tintColor, Color.GREEN);
       mThumbTintColor = a.getColor(R.styleable.IOSSwitchView_thumbTintColor, Color.WHITE);

       int defaultStrokeWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.5f, context.getResources()
               .getDisplayMetrics());
       mStrokeWidth = a.getDimensionPixelOffset(R.styleable.IOSSwitchView_strokeWidth, defaultStrokeWidth);
       isOn = a.getBoolean(R.styleable.IOSSwitchView_isOn, false);

上面的代码都很简单,都是自定义View常规的步骤,接下来我们再看添加动画的代码,看我们的gif效果图,你会发现,中间的开关把手在你手指点击它(此时没松手)的时候,它会有一个拉伸的动画,松手的时候,它会移动到另一侧,所以我们这里需要两个属性动画,一个是:拉伸动画,一个是移动动画。另外,因为我们这里有个背景颜色的变化:在默认关闭的情况下,背景色是灰白色的,打开的情况下是我们设置的tintColor颜色的,这里我们采用的是画了两个圆角矩形实现的,先画的tintColor颜色的圆角矩形,后画的灰白色的圆角矩形,刚开始开关关闭的时候灰白色的圆角矩形完全覆盖在上面,此时它的大小和它下面的矩形大小是一样的,之后在我们打开开关thumb的时候,灰白色的圆角矩形大小变成0,这样我们就看到tintColor颜色的圆角矩形了,这就实现了我们的背景切换。所以,这里又加了一个属性动画:灰白色圆角矩形的放大缩小动画。介绍到这里,我们总算理清了,我们要实现三个动画:一个thumb拉伸动画、一个thumb位移动画、一个灰白色圆角矩形形变动画(放大缩小动画)。这里我们添加初始化动画代码:

//灰白色矩形形变动画
        mInnerContentAnimator = ObjectAnimator.ofFloat(this, new Property<IOSSwitchView, Float>(Float.class, "innerbound") {
            @Override
            public void set(IOSSwitchView object, Float value) {
                object.setInnerContentRate(value);
            }

            @Override
            public Float get(IOSSwitchView object) {
                return object.getInnerContentRate();
            }
        }, innerContentRate, 1.0f);
        mInnerContentAnimator.setDuration(300);
        mInnerContentAnimator.setInterpolator(new DecelerateInterpolator());

        //thumb拉伸动画
        mThumbExpandAnimator = ObjectAnimator.ofFloat(this, new Property<IOSSwitchView, Float>(Float.class, "thumbExpand") {
            @Override
            public void set(IOSSwitchView object, Float value) {
                object.setThumbExpandRate(value);
            }

            @Override
            public Float get(IOSSwitchView object) {
                return object.getThumbExpandRate();
            }
        }, thumbExpandRate, 1.0f);
        mThumbExpandAnimator.setDuration(300);
        mThumbExpandAnimator.setInterpolator(new DecelerateInterpolator());

        //thumb位移动画
        mThumbMoveAnimator = ObjectAnimator.ofFloat(this, new Property<IOSSwitchView, Float>(Float.class, "thumbMove") {
            @Override
            public void set(IOSSwitchView object, Float value) {
                object.setThumbMoveRate(value);
            }

            @Override
            public Float get(IOSSwitchView object) {
                return object.getThumbMoveRate();
            }
        }, thumbMoveRate, 1.0f);
        mThumbMoveAnimator.setDuration(300);
        mThumbMoveAnimator.setInterpolator(new DecelerateInterpolator());

初始化完三个属性动画后,我们下一步就该初始化滑动手势了,因为我们的IOSSwitchView支持thumb滑动而改变打开关闭状态,所以我们要添加支持滑动手势GestureDetector代码:

//手势
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {

                if (!isEnabled()) return false;

                preIsOn = isOn;

                //灰白色矩形缩小到0
                mInnerContentAnimator.setFloatValues(innerContentRate, 0.0f);
                mInnerContentAnimator.start();

                //thumb有个拉伸的动作
                mThumbExpandAnimator.setFloatValues(thumbExpandRate, 1.0f);
                mThumbExpandAnimator.start();

                return true;
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                //手指抬起执行一系列的动画
                isOn = thumbState;

                if (preIsOn == isOn) {//反转
                    isOn = !isOn;
                    thumbState = !thumbState;
                }

                //打开状态
                if (thumbState) {
                    //thumb移动到右侧打开区域
                    mThumbMoveAnimator.setFloatValues(thumbMoveRate, 1.0F);
                    mThumbMoveAnimator.start();

                    //灰白色圆角矩形缩小到0
                    mInnerContentAnimator.setFloatValues(innerContentRate, 0.0F);
                    mInnerContentAnimator.start();
                } else {//关闭状态
                    //thumb移动到左侧关闭区域
                    mThumbMoveAnimator.setFloatValues(thumbMoveRate, 0.0F);
                    mThumbMoveAnimator.start();

                    //灰白色圆角矩形放大到覆盖背景大小
                    mInnerContentAnimator.setFloatValues(innerContentRate, 1.0F);
                    mInnerContentAnimator.start();
                }
                //thumb恢复原大小
                mThumbExpandAnimator.setFloatValues(thumbExpandRate, 0.0F);
                mThumbExpandAnimator.start();

                if (mOnSwitchStateChangeListener != null && preIsOn != isOn) {
                    mOnSwitchStateChangeListener.onStateSwitched(isOn);
                }

                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

                //在打开开关的区域
                if (e2.getX() > centerX) {
                    //并且开关状态是关闭的,就执行打开开关操作
                    if (!thumbState) {
                        thumbState = !thumbState;

                        mThumbMoveAnimator.setFloatValues(thumbMoveRate, 1.0F);
                        mThumbMoveAnimator.start();

                        mInnerContentAnimator.setFloatValues(innerContentRate, 0.0F);
                        mInnerContentAnimator.start();
                    }
                } else {//在关闭区域
                    //开关处于打开状态
                    if (thumbState) {
                        thumbState = !thumbState;
                        //执行关闭开关动画
                        mThumbMoveAnimator.setFloatValues(thumbMoveRate, 0.0F);
                        mThumbMoveAnimator.start();
                    }
                }

                return true;
            }

        });
        //禁止长按
        mGestureDetector.setIsLongpressEnabled(false);

因为OnGestureListener里面的方法没必要都实现,我们这用的是它的子静态类SimpleOnGestureListener,我们重写了里面的onDownonSingleTapUponScroll方法。

onDown方法的作用就是在按下控件的时候,thumb执行一个拉伸动画,灰白色圆角矩形则执行缩小到0的动画。如下图所示:

switch-expand

onSingleTapUp则是单点抬起手指后执行的操作,这里执行的就是开关互斥动画和互斥逻辑,之前如果是打开的状态,就变成关闭状态,相反则反之。。。

onScroll方法则是手指滑动时的坐标大于中间X时则执行一个选中动画,相反则反之…

手势识别添加完后,我们为了保证控件有一个好的UI效果展示,一般指控件的宽度和高度保持一个比例,我们重写其onMeasure方法,添加如下代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//测量宽度和高度
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);

//保持一定的宽高比例
if ((float) height / (float) width < 0.5f) {
height = (int) (width 0.5);

heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec));
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec));
super.setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}

centerX = width
0.5f;
centerY = height 0.5f;
cornerRadius = centerY;

innerContentRectF.left = mStrokeWidth;
innerContentRectF.top = mStrokeWidth;
innerContentRectF.right = width - mStrokeWidth;
innerContentRectF.bottom = height - mStrokeWidth;

intrinsicInnerWidth = innerContentRectF.width();
intrinsicInnerHeight = innerContentRectF.height();

thumbRectF.left = mStrokeWidth;
thumbRectF.top = mStrokeWidth;
thumbRectF.right = width - mStrokeWidth;
thumbRectF.bottom = height - mStrokeWidth;

intrinsicThumbWidth = thumbRectF.height();

//thumb最大拉伸宽度
thumbMaxExpandWidth = width
0.7f;

if (thumbMaxExpandWidth > intrinsicThumbWidth 1.25f) {
thumbMaxExpandWidth = intrinsicThumbWidth
1.25f;
}
}


测量完后,我们就开始执行最后一步也是最重要的一步:将控件画到画布canvas上,我们重写onDraw方法:


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

float w = intrinsicInnerWidth 0.5f innerContentRate;
float h = intrinsicInnerHeight 0.5f innerContentRate;

this.innerContentRectF.left = centerX - w;
this.innerContentRectF.top = centerY - h;
this.innerContentRectF.right = centerX + w;
this.innerContentRectF.bottom = centerY + h;

//thumb拉伸宽度变化,其变化值从1->1.7之间
w = intrinsicThumbWidth + (thumbMaxExpandWidth - intrinsicThumbWidth) thumbExpandRate;

boolean left = thumbRectF.left + thumbRectF.width()
0.5f < centerX;
if (left) {
thumbRectF.left = thumbRectF.right - w;
} else {
thumbRectF.right = thumbRectF.left + w;
}

float kw = thumbRectF.width();
w = (float) (width - kw - (mStrokeWidth 2)) thumbMoveRate;

thumbRectF.left = mStrokeWidth + w;
thumbRectF.right = thumbRectF.left + kw;

//颜色值过渡变化,从深灰白色变化到tintColor色
this.colorStep = transformRGBColor(thumbMoveRate, backgroundColor, mTintColor);

//画TintColor颜色的圆角矩形
mPaint.setColor(colorStep);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
drawRoundRect(0, 0, width, height, cornerRadius, canvas, mPaint);

mPaint.setColor(foregroundColor);
//画灰白色圆角矩形
canvas.drawRoundRect(innerContentRectF, innerContentRectF.height() 0.5f, innerContentRectF.height() 0.5f, mPaint);

//画thumb
mPaint.setColor(mThumbTintColor);
canvas.drawRoundRect(thumbRectF, cornerRadius, cornerRadius, mPaint);

mPaint.setColor(0xFFCCCCCC);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(1);
canvas.drawRoundRect(thumbRectF, cornerRadius, cornerRadius, mPaint);
}


onDraw方法执行了四步绘制:四次绘制圆角矩形,第一次绘制背景tintColor颜色的圆角矩形,第二次绘制灰白色覆盖在上面的圆角矩形,第三次绘制thumb,第四次绘制一个空心的圆角矩形,这个是为了加深thumb和周围的颜色区分。

其中,里面有个方法 transformRGBColor
/**
     * RGB颜色过渡变化
     *
     * @param progress
     * @param fromColor
     * @param toColor
     * @return
     */
    private int transformRGBColor(float progress, int fromColor, int toColor) {
        int fr = (fromColor >> 16) & 0xFF;
        int fg = (fromColor >> 8) & 0xFF;
        int fb = fromColor & 0xFF;

        int tr = (toColor >> 16) & 0xFF;
        int tg = (toColor >> 8) & 0xFF;
        int tb = toColor & 0xFF;

        int rGap = (int) ((float) (tr - fr) * progress);
        int gGap = (int) ((float) (tg - fg) * progress);
        int bGap = (int) ((float) (tb - fb) * progress);

        return 0xFF000000 | ((fr + rGap) << 16) | ((fg + gGap) << 8) | (fb + bGap);
    }

为了将状态变化值传递给页面,一般指的是Activity、Fragment、View,我们需要定义一个接口:

/**
 * SwitchView状态切换
 */
public static interface OnSwitchStateChangeListener {
    /**
     * 是否选中
     *
     * @param isOn
     */
    public void onStateSwitched(boolean isOn);
}

里面的一些细节这里就不讲了,最后怎么使用呢?分两步:

第一步:布局xml

<com.hhl.library.IOSSwitchView
            android:id="@+id/switch_view"
            android:layout_width="55dp"
            android:layout_height="35dp"
            android:layout_gravity="center"
            app:thumbTintColor="#fff"
            app:tintColor="#00ff00" />

第二步:代码

mSwitchView = (IOSSwitchView) findViewById(R.id.switch_view);
        mStatusTv = (TextView) findViewById(R.id.tv_status);

        mSwitchView.setOnSwitchStateChangeListener(new IOSSwitchView.OnSwitchStateChangeListener() {
            @Override
            public void onStateSwitched(boolean isOn) {
                if (isOn) {
                    mStatusTv.setText("状态:开");
                } else {
                    mStatusTv.setText("状态:关");
                }
            }
        });

最后,附上源码:github-IOSSwitchView