自定义view-滑动开关

介绍

前段时间,我看到了一篇关于可滑动开关Switch组件的文章,效果图如下:
这里写图片描述
思路也挺简单的:这个控件主要由田径场式背景和滑块组成。他将田径场式背景分为3部分,最左边的半圆,中间的两条直线部分和最右边的半圆。假设线的宽度为lx,半圆的半径则为lx的一半,通过监听touch事件,不停的绘制两个半圆和两条线段、滑块,从而达到滑块跟着手指滑动的显示效果。
虽然效果是实现了,但是田径场式背景被拆分绘制,我感觉还是有点繁琐,不统一,我就想有没有什么办法可以一次性将这个背景画出来?答案是有的(你这不是废话~)。

两种方法的差异

我们都知道在Android中有提供用来绘制各种图案的类:Path。Path主要用于绘制复杂的图形轮廓,比如折线,圆弧以及各种复杂图案。现在再来看这个田径场式背景,说白了就是一个圆角矩形形状,这个圆角设置足够大就可以了。我们可以使用Path中的addRoundRect(RectF rect, float[] radii, Direction dir)绘制出圆角矩形。其中第二个参数是8个值(矩形的4个角)的数组,4对[ x,y ]半径。

实现

这个控件支持自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- JYSwitchButton的自定义属性
     openColor:开启状态的颜色
     closeColor:关闭状态的颜色
     circleColor:滑动圆形图标的颜色
     openText:开启状态的文本
     closeText:关闭状态的文本
     openTextColor:开启状态的文本颜色
     closeTextColor:关闭状态的文本颜色
     textSize:字体大小
     -->
    <declare-styleable name="switchbutton">
        <attr name="openColor" format="integer"></attr>
        <attr name="closeColor" format="integer"></attr>
        <attr name="circleColor" format="integer"></attr>
        <attr name="openText" format="string"></attr>
        <attr name="closeText" format="string"></attr>
        <attr name="openTextColor" format="integer"></attr>
        <attr name="closeTextColor" format="integer"></attr>
        <attr name="textSize" format="dimension"></attr>
    </declare-styleable>
</resources>

也提供了一些设置的方法:

 * 支持自定义颜色值setOpenColor/setCloseColor/setCircleColor;
 * 支持设置偏移量setOffset;
 * 支持设置初始状态changeState;
 * 支持获取默认状态getDefaultState;
 * 支持开启/关闭状态的监听setListener(OnSwitchStateChangeListener);
 * 支持设置滑动圆形图标的边距setCirclePadding;
 * 支持设置开启/关闭文本和颜色setOpenText、setCloseText、setOpenTextColor、setCloseTextColor
 * 支持设置文本字体大小setTextSize

话不多说了,撸代码去:

package com.ha.cjy.jyswitchbutton;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * 滑动开关按钮
 *
 * Created by cjy on 17/11/14.
 */

public class JYSwitchButton extends View {
    private Context mContext;

    //画笔
    private Paint mPaint;
    //移动距离
    private int mCurrentX;
    //宽度
    private int mViewWidth;
    //高度
    private int mViewHeight;
    //Y中心
    private int mCenterY;
    //左边圆的X中心点
    private int mStartX;
    //右边圆的X中心点
    private int mEndX;
    //滑动圆形图标的半径
    private int mRadius;
    //滑动圆形图标的边距
    private int mCirclePadding = 2;
    //是否已经初始化好宽高了
    private boolean mIsInit = false;
    //是否开启,默认是关闭状态
    private boolean mIsOpen = false;
    //状态监听器
    private OnSwitchStateChangeListener mListener;
    //矩形4个角的半径坐标,左上,右上,右下,左下(顺时针)
    private float[] mRadiusArr = new float[]{0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f};
    //偏移量,用来控制view的显示大小
    private int mOffset = 20;
    //开启状态的背景色
    private int mOpenColor = Color.BLUE;
    //关闭状态的背景色
    private int mCloseColor = Color.GRAY;
    //滑动圆形图标的颜色
    private int mCircleColor = Color.LTGRAY;
    //字体最大值、最小值
    private float mTextMaxSize = 32;
    private float mTextMinSize = 10;
    //开启/关闭文本
    private String mOpenText="";
    private String mCloseText="";
    //开启/关闭文本的颜色
    private int mOpenTextColor = Color.WHITE;
    private int mCloseTextColor = Color.WHITE;

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

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

    public JYSwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        getProperty(context,attrs);
        init();
        defaultRoundRadius();
    }

    /**
     * 初始化操作
     */
    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.BLACK);
    }

    /**
     * 获取自定义属性
     * @param context
     * @param attrs
     */
    private void getProperty(Context context, AttributeSet attrs){
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.switchbutton);
        mOpenColor = typedArray.getInteger(R.styleable.switchbutton_openColor,mOpenColor);
        mCloseColor = typedArray.getInteger(R.styleable.switchbutton_closeColor,mCloseColor);
        mCircleColor = typedArray.getInteger(R.styleable.switchbutton_circleColor,mCircleColor);
        mOpenText = typedArray.getString(R.styleable.switchbutton_openText);
        mCloseText = typedArray.getString(R.styleable.switchbutton_closeText);
        if (mOpenText == null)
            mOpenText = "";
        if (mCloseText == null)
            mCloseText = "";
        mOpenTextColor = typedArray.getInteger(R.styleable.switchbutton_openTextColor,mOpenTextColor);
        mCloseTextColor = typedArray.getInteger(R.styleable.switchbutton_closeTextColor,mCloseTextColor);
        mTextMaxSize = typedArray.getDimension(R.styleable.switchbutton_textSize,mTextMaxSize);

        //取完属性,记得释放
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredWidth() / 2 + mOffset;
        setMeasuredDimension(mViewWidth, mViewHeight);
        mCenterY = mViewHeight / 2 - mOffset;
        mRadius = mViewHeight / 2 - mOffset;
        mCurrentX = mRadius;
        mStartX = mRadius;
        mEndX = mViewWidth - mRadius;

        mIsInit = true;
    }

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

        if (mIsInit) {
            //控制滑动区域
            mCurrentX = mCurrentX > mStartX ? mCurrentX : mStartX;
            mCurrentX = mCurrentX < mEndX ? mCurrentX : mEndX;

            Path path = new Path();
            RectF rectF = new RectF();
            rectF.top = 0;
            rectF.left = 0;
            rectF.right = mViewWidth;
            rectF.bottom = mRadius * 2;
            //画圆角矩形背景图
            path.addRoundRect(rectF, mRadiusArr, Path.Direction.CW);

            if (mIsOpen) {
                //左边色块
                mPaint.setColor(mOpenColor);
                canvas.drawPath(path, mPaint);
                //绘制文本
                drawText(canvas,mOpenText,mOpenTextColor);
            } else {
                //右边色块
                mPaint.setColor(mCloseColor);
                canvas.drawPath(path, mPaint);
                //绘制文本
                drawText(canvas,mCloseText,mCloseTextColor);
            }
            //滑动圆形
            mPaint.setColor(mCircleColor);
            int realRadius = mRadius-mCirclePadding;
            if (realRadius < mRadius/2 ){
                realRadius = mRadius/2;
            }else if(realRadius > mRadius){
                realRadius = mRadius;
            }
            canvas.drawCircle(mCurrentX, mCenterY,realRadius, mPaint);
        }
    }

    /**
     * 绘制文本
     * @param canvas
     * @param text 文本
     * @param color 文本颜色
     */
    private void drawText(Canvas canvas,String text,int color){
        if(text.isEmpty())
            return;
        mPaint.setColor(color);
        mPaint.setTextSize(mTextMaxSize);
        //文本宽度
        int textWidth = mViewWidth-mRadius*2;
        float trySize = mTextMaxSize;
        //根据文本宽度,字体大小适配
        while (mPaint.measureText(text)<textWidth){
            trySize += 1;
            mPaint.setTextSize(trySize);
        }
        while (mPaint.measureText(text)>textWidth){
            trySize -= 1;
            if (trySize < mTextMinSize){
                trySize = mTextMinSize;
                break;
            }
            mPaint.setTextSize(trySize);
        }
        mPaint.setTextSize(px2sp(mContext,trySize));

        int x = mIsOpen?mStartX+10:mEndX-mRadius-10;
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        int textHeight = (int)(fontMetrics.descent-fontMetrics.ascent);
        int baseline = (int) (mCenterY+(mCenterY*1.0/3.0));
        canvas.drawText(text,x,baseline,mPaint);
    }

    /**
     * 默认的圆角数据
     */
    private void defaultRoundRadius() {
        mRadiusArr[0] = 120;
        mRadiusArr[1] = 120;
        mRadiusArr[2] = 120;
        mRadiusArr[3] = 120;
        mRadiusArr[4] = 120;
        mRadiusArr[5] = 120;
        mRadiusArr[6] = 120;
        mRadiusArr[7] = 120;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int lastX = 0;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                lastX = (int) event.getX();
                break;
            }
            case MotionEvent.ACTION_MOVE: {
	            //滑动的偏移量
                mCurrentX = (int) (event.getX() - lastX);
                break;
            }
            case MotionEvent.ACTION_UP: {
                mCurrentX = (int) (event.getX() - lastX);
                if (mCurrentX > mViewWidth / 2) {//从左到右滑动
                    mCurrentX = mEndX;
                    if (mIsOpen == false) {
                        mIsOpen = true;
                        if (mListener != null) {
                            mListener.onSwitchStateChange(mIsOpen);
                        }
                    }
                } else {//从右向左滑动
                    mCurrentX = mStartX;
                    if (mIsOpen == true) {
                        mIsOpen = false;
                        if (mListener != null) {
                            mListener.onSwitchStateChange(mIsOpen);
                        }
                    }
                }
                break;
            }
        }
        postInvalidate();
        return true;
    }

    /**
     * 获取默认状态
     * @return
     */
    public boolean getDefaultState(){
        return this.mIsOpen;
    }

    /**
     * 设置状态:开启/关闭
     * @param isOpen 是否开启 true-开启 false-关闭
     */
    public void changeState(boolean isOpen){
        this.mIsOpen = isOpen;
        postDelayed(new Runnable() {//延迟100毫秒,等计算好宽高再进行重新绘制
            @Override
            public void run() {
                if (mIsInit) {
                    if (mIsOpen) {
                        mCurrentX = mEndX;
                    } else {
                        mCurrentX = mStartX;
                    }
                }
                invalidate();
            }
        },100);

    }

    /**
     * 设置滑动圆形图标的边距
     * @param padding
     */
    public void setCirclePadding(int padding){
        this.mCirclePadding = padding;
    }

    /**
     * 设置开启状态的颜色
     * @param color
     */
    public void setOpenColor(int color){
        this.mOpenColor = color;
    }
    /**
     * 设置关闭状态的颜色
     * @param color
     */
    public void setCloseColor(int color){
        this.mCloseColor = color;
    }
    /**
     * 设置滑动圆形的颜色
     * @param color
     */
    public void setCircleColor(int color){
        this.mCircleColor = color;
    }

    /**
     * 设置开启状态的文本
     * @param value
     */
    public void setOpenText(String value){
        this.mOpenText = value;
    }

    /**
     * 设置关闭状态的文本
     * @param value
     */
    public void setCloseText(String value){
        this.mCloseText = value;
    }

    /**
     * 设置开启状态的文本
     * @param color
     */
    public void setOpenTextColor(int color){
        this.mOpenTextColor = color;
    }

    /**
     * 设置关闭状态的文本颜色
     * @param color
     */
    public void setCloseTextColor(int color){
        this.mCloseTextColor = color;
    }

    /**
     * 设置字体大小
     * @param textSize
     */
    public void setTextSize(int textSize){
        this.mTextMaxSize = textSize;
    }

    /**
     * 设置偏移量
     * @param offset 偏移量
     */
    public void setOffset(int offset){
        this.mOffset = offset;
    }

    /**
     * 设置监听器
     * @param listener
     */
    public void setListener(OnSwitchStateChangeListener listener) {
        this.mListener = listener;
    }

    /**
     * 将px值转换为sp值,保证文字大小不变
     * @param context
     * @param pxValue
     * @return
     */
    private float px2sp(Context context, float pxValue) {
        float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (pxValue / fontScale);
    }


    /**
     * 开启/关闭状态的监听
     */
    interface OnSwitchStateChangeListener {
        /**
         * 状态变化的事件
         * @param isOpen true-开启 false-关闭
         */
        void onSwitchStateChange(boolean isOpen);
    }
}

以上注释写的很详细了。主要的有几点:

  • 重写了onMeasure方法,使控件的高度依赖于控件宽度,保证控件的宽高比;
  • 控制好滑块的滑动范围;
  • 根据文本宽度进行字体大小的适配;
  • 设置状态监听器OnSwitchStateChangeListener,在控件内部定义其对象和开放方法setListener,以便外部进行状态的监听和调用;
  • 设置状态(changeState)的时候会去重新绘制,但是这时绘制如果控件的宽高还没有计算出来,就会导致数据不正确,滑块的位置就会显示错误,所以需要延迟一段时间去执行绘制工作。
  • ###使用
  • 布局文件:在该布局中添加该控件即可,因为有自定义的属性,需要声明其命名空间:xmlns:jy="http://schemas.android.com/apk/res/com.ha.cjy.jyswitchbutton"
  • ```
    <com.ha.cjy.jyswitchbutton.JYSwitchButton
        android:id="@+id/btnSwitch"
        android:layout_marginTop="40dp"
        android:layout_marginLeft="40dp"
        android:layout_width="60dp"
        android:layout_height="wrap_content"
        jy:textSize="40sp"
        jy:openColor="@color/colorAccent"
        jy:closeColor="@color/colorPrimary"
        jy:circleColor="@color/colorWhite"
        jy:openText="开"
        jy:closeText="关"
        jy:openTextColor="@color/colorWhite"
        jy:closeTextColor="@color/colorWhite"/>
    
    <li>Activity代码如下:</li>
    

    package com.ha.cjy.jyswitchbutton;

    import android.graphics.Color;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.widget.Toast;

    public class MainActivity extends AppCompatActivity implements JYSwitchButton.OnSwitchStateChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    	//滑动开关控件
        JYSwitchButton btnSwitch = (JYSwitchButton) findViewById(R.id.btnSwitch);
    

    // btnSwitch.changeState(true);
    // Log.i("state",btnSwitch.getDefaultState()+"");
    // btnSwitch.setOpenColor(Color.RED);
    // btnSwitch.setCloseColor(Color.GRAY);
    // btnSwitch.setCircleColor(Color.BLUE);
    btnSwitch.setListener(this);
    }

    @Override
    public void onSwitchStateChange(boolean isOpen) {
        Toast.makeText(this,"状态:"+(isOpen?"开启":"关闭"),Toast.LENGTH_SHORT).show();
    }
    

    }

    ###总结
     其实还有一些细节问题我没有在这篇文章上讲出,比如文本绘制的baseline实现还有些问题,希望感兴趣的同学可以自行研究代码并完善它。项目地址传送门摸[我的github](https://github.com/hacjy/JYSwitchButton)。
    posted @ 2017-11-17 17:00  ha_cjy  阅读(820)  评论(0编辑  收藏  举报