自定义滑动控件(SwitchView),同时解决和ScrollView的冲突

网上搜 滑动控件(SwitchView),很多。本来也没什么写的,但是根据新项目需求,和事件界面调试,还是发现了很多问题。把网上的项目改来改去,麻烦。就自己写了。

简单的效果图
在这里插入图片描述

根据本次实际功能和自己调试过程中发现的问题,针对 switchView,总结了以下需要具备的功能
1、可以滑动。以中心为界限,当滑过中点,就变换颜色。如:当前是开启状态,当小圆滑过中点,背景就变成关闭时候的颜色。擡起手,小圆自动移动到终点位置。反之亦然;
2、可以点击。如:当前是开启状态,点击按钮(手指按下位置和最后手指的位置,绝对值不超过5像素,且整个过程中,都没超过5像素。注:假设按下位置是10,滑动到20,再滑动到11,虽然绝对值相差1,但是也是滑动),自动切换为 关闭 状态。反之亦然;
3、自动移动过程屏蔽触摸事件。当小圆在自动移动的过程中,再次触摸(点击或滑动)switchView,也无效;
4、可以自由屏蔽触摸事件。如:需求为,有4个设置按钮,可以随意开关。但是,最好要保留2个是开启的。如果已经关闭了2个,再去关闭的时候,要屏蔽触摸事件,且给出对应提示
5、不会引起滑动冲突。如,设置界面的设置按钮很多,一屏放不下,就需要ScrollView包裹,而switchView也是有滑动事件的,如果不做任何解决,switchView滑动到一半,再上下滑动,就会导致switchView展示错乱,例如,小圆停在控件的中间不动,等等问题。现在要解决这种问题

源码不多,就直接粘贴了,不想下载的,可以直接复制下面的代码。

我这个控件的源码,是写在上一个demo中调试的。想下载的,可以去
上个Demo的GitHub

后面我会写代码中的细节讲解和用法。

res -> values -> attra.xml

<declare-styleable name="MySwitchView">
	<!--开关打开时,背景的颜色-->
	<attr name="bgOpenColor" format="color"/>
	<!--开关关闭时,背景的颜色-->
	<attr name="bgCloseColor" format="color"/>
	<!--控件中小圆的颜色-->
	<attr name="circleColor" format="color"/>
</declare-styleable>

MySwitchView


import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.Handler
import android.os.Message
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.LinearLayout
import android.widget.Toast

class MySwitchView : LinearLayout {

    private var mContext: Context? = null

    //椭圆背景画笔
    private var bgPaint: Paint? = null
    //滑动控件中的 圆 的画笔
    private var circlePaint: Paint? = null

    private var defaultOpenColor: Int = Color.parseColor("#4A79FD")
    private var defaultCloseColor: Int = Color.parseColor("#dddddd")
    private var defaultCircleColor: Int = Color.WHITE

    private var bgOpenColor: Int = defaultOpenColor
    private var bgCloseColor: Int = defaultCloseColor
    private var circleColor: Int = defaultCircleColor

    private var viewWidth: Int = 0
    private var viewHeight: Int = 0
    private var center: Float = 0f

    //圆点半径
    private var radius: Float = 0f

    //手指擡起后,小圆是否在自己滑动。如果是,就要先停止触摸事件
    private var isAnimation: Boolean = false

    //是否允许触摸。某些条件下,将其改为false,可以屏蔽触摸
    private var isCanTouch: Boolean = true

    //屏蔽触摸后,用于提示的内容。 可有可无
    private var tipString = ""

    //小圆圆心当前的X轴座标
    private var currentX: Float = 0f

    private val TO_CLOSE: Int = 100
    private val TO_OPEN: Int = 200

    private val TO_SHOW: Int = 300

    private var isOpen: Boolean = true

    private var leftLimitValue: Float = 0f
    private var rightLimitValue: Float = 0f

    private var offsetValue: Float = 0f

    private var mHandler = object : Handler() {

        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)

            when (msg?.what) {

                TO_CLOSE -> {

                    if (currentX > leftLimitValue) {
                        currentX -= 5
                        isAnimation = true
                        sendEmptyMessage(TO_CLOSE)

                    } else {
                        currentX = leftLimitValue
                        isAnimation = false
                        removeMessages(TO_CLOSE)
                    }
                }

                TO_OPEN -> {

                    if (currentX < rightLimitValue) {
                        currentX += 5
                        isAnimation = true
                        sendEmptyMessage(TO_OPEN)
                    } else {
                        currentX = rightLimitValue
                        isAnimation = false
                        removeMessages(TO_OPEN)
                    }

                }

                TO_SHOW -> {
                    if (isOpen) {
                        bgPaint?.color = bgOpenColor
                        currentX = rightLimitValue
                    } else {
                        bgPaint?.color = bgCloseColor
                        currentX = leftLimitValue
                    }
                    removeMessages(TO_SHOW)
                }

            }

            invalidate()
        }
    }

    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {

        setBackgroundColor(Color.TRANSPARENT)

        mContext = context

        val ta = context?.obtainStyledAttributes(attrs, R.styleable.MySwitchView)
        bgOpenColor = ta?.getColor(R.styleable.MySwitchView_bgOpenColor, defaultOpenColor)
            ?: defaultOpenColor
        bgCloseColor = ta?.getColor(R.styleable.MySwitchView_bgCloseColor, defaultCloseColor)
            ?: defaultCloseColor
        circleColor = ta?.getColor(R.styleable.MySwitchView_circleColor, defaultCircleColor)
            ?: defaultCircleColor

        ta?.recycle()

        init()

    }


    private fun init() {

        //背景画笔
        bgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        bgPaint?.strokeWidth = 20f
        bgPaint?.style = Paint.Style.FILL
        bgPaint?.color = bgOpenColor

        //小圆画笔
        circlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
        circlePaint?.strokeWidth = 20f
        circlePaint?.style = Paint.Style.FILL
        circlePaint?.color = circleColor

    }

    override fun onLayout(p0: Boolean, p1: Int, p2: Int, p3: Int, p4: Int) {
        super.onLayout(p0, p1, p2, p3, p4)

    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {

        ev ?: return super.onInterceptTouchEvent(ev)

        if (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE) {
            // 将父控件的滚动事件拦截
            requestDisallowInterceptTouchEvent(true)
        } else if (ev.getAction() == MotionEvent.ACTION_UP) {
            // 把滚动事件恢复给父控件
            requestDisallowInterceptTouchEvent(false)
        }

        return super.onInterceptTouchEvent(ev)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)

        viewWidth = w
        viewHeight = h

        offsetValue = viewHeight / 10f

        radius = (viewHeight - offsetValue * 2) / 2f

        leftLimitValue = offsetValue + radius

        rightLimitValue = viewWidth - offsetValue - radius

        currentX = rightLimitValue

        center = viewWidth / 2f

    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)



        if (bgPaint != null && circlePaint != null) {

            canvas?.drawRoundRect(
                0f,
                0f,
                viewWidth.toFloat(),
                viewHeight.toFloat(),
                viewHeight / 2f,
                viewHeight / 2f,
                bgPaint!!
            )

            canvas?.drawCircle(currentX, viewHeight / 2f, radius, circlePaint!!)

        }

    }

    private var downX: Float = 0f
    //isScroll = true 表示发生了滑动
    private var isScroll: Boolean = false

    override fun onTouchEvent(event: MotionEvent?): Boolean {

        if (event == null || isAnimation) {
            return super.onTouchEvent(event)
        }

        if (!isCanTouch) {

            mySvOnClickListener?.onClick(isOpen)

            if (tipString.isEmpty().not()) {
                Toast.makeText(mContext!!, "$tipString", Toast.LENGTH_SHORT).show()
            }

            return super.onTouchEvent(event)
        }

        when (event.action) {

            MotionEvent.ACTION_DOWN -> {

                downX = event.x

            }

            MotionEvent.ACTION_MOVE -> {

                currentX = event.x

                isScroll = Math.abs(currentX - downX) > 5

                if (isScroll) {
                    if (currentX < leftLimitValue || currentX < 0) {
                        currentX = leftLimitValue
                    } else if (currentX > rightLimitValue || currentX > viewWidth) {
                        currentX = rightLimitValue
                    }

                    isOpen = currentX >= center

                    if (isOpen) {
                        bgPaint?.color = bgOpenColor
                    } else {
                        bgPaint?.color = bgCloseColor
                    }

                    invalidate()
                }

            }


            MotionEvent.ACTION_UP -> {

                currentX = event.x

                if (isScroll) {
                    isOpen = currentX >= center

                    if (currentX < leftLimitValue || currentX < 0) {
                        currentX = leftLimitValue
                    } else if (currentX > rightLimitValue || currentX > viewWidth) {
                        currentX = rightLimitValue
                    }

                    if (isOpen) {
                        bgPaint?.color = bgOpenColor
                    } else {
                        bgPaint?.color = bgCloseColor
                    }

                    if (isOpen) {
                        goOpen()
                    } else {
                        goClose()
                    }
                } else {

                    //这里,要做颜色的替换
                    if (isOpen) {
                        currentX = rightLimitValue
                        bgPaint?.color = bgCloseColor
                    } else {
                        currentX = leftLimitValue
                        bgPaint?.color = bgOpenColor
                    }

                    isOpen = !isOpen

                    if (isOpen) {
                        goOpen()
                    } else {
                        goClose()
                    }

                }

                mySvOnClickListener?.onClick(isOpen)

            }

        }

        return true

    }

    //设置是否开启,true表示开启
    fun setIsOpen(b: Boolean) {
        isOpen = b

        mHandler.sendEmptyMessageDelayed(TO_SHOW, 40)


    }

    fun getOpen(): Boolean {

        return isOpen
    }

    /**
     * 设置是否可以点击、滑动
     *
     * b:true 表示可以点击、滑动(即:进行交互)
     * tip:要提示的语句,一般和 b=false 结合使用(即:屏幕交互后,给出提示)
     */
    fun setIsCanTouch(b: Boolean, tip: String = "") {

        isCanTouch = b
        tipString = tip
    }

    private fun goClose() {
        isOpen = false
        mHandler.sendEmptyMessage(TO_CLOSE)
    }

    private fun goOpen() {
        isOpen = true
        mHandler.sendEmptyMessage(TO_OPEN)
    }


    interface MySvOnClickListener {
        fun onClick(isOpen: Boolean)
    }

    private var mySvOnClickListener: MySvOnClickListener? = null

    fun setMySvOnClickListener(listener: MySvOnClickListener) {
        mySvOnClickListener = listener
    }


}

到此,源码就完成了。接下来,讲使用:
1、点击事件,拿到结果值。注意,是结果值。不是switchView的当前值。

studySetChToEnSv.setMySvOnClickListener(object : MySwitchView.MySvOnClickListener {
	override fun onClick(isOpen: Boolean) {

		Toast.makeText(this@MainActivity, "$isOpen", Toast.LENGTH_SHORT).show()

	}

})

2、有4个按钮,只能关闭2个。

private var svList: MutableList<MySwitchView> = mutableListOf()

handleSwitchView(mySv_1,true)
handleSwitchView(mySv_2,true)
handleSwitchView(mySv_3,true)
handleSwitchView(mySv_4,true)

svList = mutableListOf(
	mySv_1,
	mySv_2,
	mySv_3,
	mySv_4
)
//方法
setSwitchViewIsCanTouch()
==============================

private fun handleSwitchView(
        switchView: MySwitchView?,
        value: Boolean
) {

	switchView ?: return

	switchView.setIsOpen(value)

	switchView.setMySvOnClickListener(object : MySwitchView.MySvOnClickListener {

		override fun onClick(isOpen: Boolean) {
			setSwitchViewIsCanTouch()
		}

	})

}


//设置 SwitchView 是否可以被点击
private fun setSwitchViewIsCanTouch() {

	//打开的集合
	var rightList = svList.filter {
		it.getOpen()
	}.toMutableList()

	//关闭的集合
	var leftList = svList.filter {
		!it.getOpen()
	}.toMutableList()

	//遍历被点亮的控件
	rightList.forEach {
		//如果关闭的个数小于2,则被点亮的控件还可以点击
		it.setIsCanTouch(leftList.size < 2, "打开的个数,不能小于2个")
	}

}

========== 前方高能 ==========

1、这个switchView,不是继承自 view,而是 LinearLayout(ViewGroup)。因为要解决滑动冲突,我的思路是,点击的时候,让父控件(如 ScrollView)不拦截事件,擡起手后,把滑动事件交还给 父控件。这就要用到 onInterceptTouchEvent,在其中做 requestDisallowInterceptTouchEvent。而 View,没有 onInterceptTouchEvent 方法。

2、在构造方法中,有这么一句:

setBackgroundColor(Color.TRANSPARENT)

这是因为,如果单纯的继承自 ViewGroup,在onDraw方法里绘制图形,是不会展示的。是一片空白。必须,给 ViewGroup一个背景色。

3、为什么要在 setIsOpen 方法中,

mHandler.sendEmptyMessageDelayed(TO_SHOW, 40)

如果不这样,直接在使用的时候,activity中 setIsOpen(false),会有问题。因为这个时候,控件还没绘制完,拿不到宽高。

有2个解决办法:1、在界面中使用的时候,mySwitchView.post{…},这样就行了,但是,如果控件很多,就会很麻烦。就算用根布局的 post 方法,也不好,因为要在每个使用的界面中写;2、延迟一点,给系统测量、绘制、摆放预留点时间。这里,我选了40毫秒。这样有个好处就是,在界面中使用的时候,不用额外做 post等类似操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章