package com.xw.common.custom import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.RectF import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.animation.DecelerateInterpolator import android.widget.Scroller import androidx.core.content.withStyledAttributes import com.xw.common.R import kotlin.math.abs /** * 自定义滑动开关 View * 支持左右滑动切换,带文字提示,支持滑块与背景间距 */ class SlideSwitchView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { // 状态监听 interface OnStateChangeListener { fun onStateChanged(isChecked: Boolean) } //是否能手动点击 var switchEnable = true set(value) { field = value invalidate() } // 尺寸配置 private var switchWidth = 0f private var switchHeight = 0f private var thumbWidth = 0f private var thumbHeight = 0f private var cornerRadius = 0f private var thumbCornerRadius = 0f // 颜色配置 private var backgroundColorUnchecked = Color.parseColor("#F5F5F5") private var backgroundColorChecked = Color.parseColor("#F5F5F5") private var thumbColorUnchecked = Color.parseColor("#9E9E9E") private var thumbColorChecked = Color.parseColor("#4CAF50") private var textColorUnchecked = Color.WHITE private var textColorChecked = Color.WHITE // 文字配置 private var textUnchecked = "待检测" private var textChecked = "检测完成" private var textSize = 40f // 间距配置(新增) private var thumbMargin = 0f // 滑块与背景的间距 // 状态 private var isChecked = false private var thumbPosition = 0f // 0 ~ maxThumbPosition private var maxThumbPosition = 0f private var listener: OnStateChangeListener? = null // 动画 private val scroller = Scroller(context, DecelerateInterpolator(1.5f)) private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) // 触摸处理 private var lastX = 0f private var isDragging = false private val touchSlop = 10 init { context.withStyledAttributes(attrs, R.styleable.SlideSwitchView, defStyleAttr, 0) { switchEnable = getBoolean(R.styleable.SlideSwitchView_switchEnable, switchEnable) // 背景颜色 backgroundColorUnchecked = getColor( R.styleable.SlideSwitchView_switchBackgroundColorUnchecked, backgroundColorUnchecked ) backgroundColorChecked = getColor( R.styleable.SlideSwitchView_switchBackgroundColorChecked, backgroundColorChecked ) // 滑块颜色 thumbColorUnchecked = getColor( R.styleable.SlideSwitchView_switchThumbColorUnchecked, thumbColorUnchecked ) thumbColorChecked = getColor( R.styleable.SlideSwitchView_switchThumbColorChecked, thumbColorChecked ) // 文字颜色 textColorUnchecked = getColor( R.styleable.SlideSwitchView_switchTextColorUnchecked, textColorUnchecked ) textColorChecked = getColor( R.styleable.SlideSwitchView_switchTextColorChecked, textColorChecked ) // 文字内容 textUnchecked = getString(R.styleable.SlideSwitchView_switchTextUnchecked) ?: textUnchecked textChecked = getString(R.styleable.SlideSwitchView_switchTextChecked) ?: textChecked // 文字大小 textSize = getDimension(R.styleable.SlideSwitchView_switchTextSize, textSize) // 圆角 cornerRadius = getDimension(R.styleable.SlideSwitchView_switchCornerRadius, 0f) thumbCornerRadius = getDimension(R.styleable.SlideSwitchView_switchThumbCornerRadius, 0f) // 滑块间距(新增) thumbMargin = getDimension(R.styleable.SlideSwitchView_switchThumbMargin, 0f) // 初始状态 isChecked = getBoolean(R.styleable.SlideSwitchView_switchChecked, false) } // 设置点击可聚焦 isClickable = true isFocusable = true } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val desiredWidth = (160 * resources.displayMetrics.density).toInt() val desiredHeight = (56 * resources.displayMetrics.density).toInt() val widthMode = MeasureSpec.getMode(widthMeasureSpec) val widthSize = MeasureSpec.getSize(widthMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) val width = when (widthMode) { MeasureSpec.EXACTLY -> widthSize MeasureSpec.AT_MOST -> widthSize.coerceAtMost(desiredWidth) else -> desiredWidth } val height = when (heightMode) { MeasureSpec.EXACTLY -> heightSize MeasureSpec.AT_MOST -> heightSize.coerceAtMost(desiredHeight) else -> desiredHeight } setMeasuredDimension(width, height) } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) // 背景区域(考虑整体内边距) val padding = 4 * resources.displayMetrics.density switchWidth = w - padding * 2 switchHeight = h - padding * 2 // 滑块高度 = 背景高度 - 2 * 间距 thumbHeight = switchHeight - thumbMargin * 2 // 滑块宽度占可用宽度的比例(考虑间距) // 可用滑动距离 = switchWidth - thumbWidth - 2 * thumbMargin // 为了视觉效果,滑块宽度设为背景宽度的 60% thumbWidth = (switchWidth - thumbMargin * 2) * 0.6f // 最大滑动位置(相对于背景左边缘 + 间距) maxThumbPosition = switchWidth - thumbWidth - thumbMargin * 2 // 默认圆角 if (cornerRadius == 0f) { cornerRadius = switchHeight / 2 } if (thumbCornerRadius == 0f) { thumbCornerRadius = thumbHeight / 2 } // 设置初始位置 thumbPosition = if (isChecked) maxThumbPosition else 0f } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val padding = 4 * resources.displayMetrics.density val bgLeft = padding val bgTop = padding val bgRight = bgLeft + switchWidth val bgBottom = bgTop + switchHeight // 绘制背景 val bgRect = RectF(bgLeft, bgTop, bgRight, bgBottom) paint.color = if (isChecked) backgroundColorChecked else backgroundColorUnchecked canvas.drawRoundRect(bgRect, cornerRadius, cornerRadius, paint) // 计算滑块位置 // 滑块左边缘 = 背景左边缘 + 间距 + 滑动位置 val thumbLeft = bgLeft + thumbMargin + thumbPosition val thumbTop = bgTop + thumbMargin val thumbRight = thumbLeft + thumbWidth val thumbBottom = thumbTop + thumbHeight // 绘制滑块 val thumbRect = RectF(thumbLeft, thumbTop, thumbRight, thumbBottom) paint.color = if (isChecked) thumbColorChecked else thumbColorUnchecked canvas.drawRoundRect(thumbRect, thumbCornerRadius, thumbCornerRadius, paint) // 绘制文字 textPaint.textSize = textSize textPaint.textAlign = Paint.Align.CENTER // 计算文字基线(垂直居中于滑块) val fontMetrics = textPaint.fontMetrics val baseline = thumbTop + thumbHeight / 2 - (fontMetrics.ascent + fontMetrics.descent) / 2 // 绘制滑块上的文字 textPaint.color = if (isChecked) textColorChecked else textColorUnchecked val currentText = if (isChecked) textChecked else textUnchecked canvas.drawText( currentText, thumbLeft + thumbWidth / 2, baseline, textPaint ) } override fun onTouchEvent(event: MotionEvent): Boolean { if (switchEnable) { when (event.action) { MotionEvent.ACTION_DOWN -> { lastX = event.x isDragging = false parent?.requestDisallowInterceptTouchEvent(true) return true } MotionEvent.ACTION_MOVE -> { val deltaX = event.x - lastX if (!isDragging && abs(deltaX) > touchSlop) { isDragging = true } if (isDragging) { thumbPosition += deltaX // 限制滑动范围 thumbPosition = thumbPosition.coerceIn(0f, maxThumbPosition) invalidate() } lastX = event.x return true } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { parent?.requestDisallowInterceptTouchEvent(false) if (!isDragging) { // 点击切换 toggle() } else { // 滑动释放,根据位置决定状态 val threshold = maxThumbPosition / 2 val targetChecked = thumbPosition > threshold animateToState(targetChecked) } isDragging = false return true } } } return super.onTouchEvent(event) } private fun animateToState(targetChecked: Boolean) { val targetPosition = if (targetChecked) maxThumbPosition else 0f val distance = targetPosition - thumbPosition scroller.startScroll( thumbPosition.toInt(), 0, distance.toInt(), 0, 300 ) if (targetChecked != isChecked) { isChecked = targetChecked listener?.onStateChanged(isChecked) } invalidate() } override fun computeScroll() { if (scroller.computeScrollOffset()) { thumbPosition = scroller.currX.toFloat() invalidate() postInvalidateOnAnimation() } } // 公共方法 fun setChecked(checked: Boolean, animate: Boolean = true) { if (checked == isChecked) return if (animate) { animateToState(checked) } else { isChecked = checked thumbPosition = if (checked) maxThumbPosition else 0f invalidate() } } fun isChecked() = isChecked fun toggle(animate: Boolean = true) { setChecked(!isChecked, animate) } fun setOnStateChangeListener(listener: OnStateChangeListener?) { this.listener = listener } // 代码设置属性 fun setText(unchecked: String, checked: String) { textUnchecked = unchecked textChecked = checked invalidate() } fun setColors( thumbUnchecked: Int, thumbChecked: Int, bgUnchecked: Int = backgroundColorUnchecked, bgChecked: Int = backgroundColorChecked ) { thumbColorUnchecked = thumbUnchecked thumbColorChecked = thumbChecked backgroundColorUnchecked = bgUnchecked backgroundColorChecked = bgChecked invalidate() } // 代码设置间距 fun setThumbMargin(margin: Float) { thumbMargin = margin requestLayout() } }