網上搜 滑動控件(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等類似操作