自定義滑動控件(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等類似操作

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