自定义滑动控件(SwitchView),同时解决和ScrollView的冲突
Posted 夜尽天明89
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义滑动控件(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等类似操作
以上是关于自定义滑动控件(SwitchView),同时解决和ScrollView的冲突的主要内容,如果未能解决你的问题,请参考以下文章
Android自定义LinearLayout实现侧滑布局--SwipeLinearLayout