package com.xw.common.custom import android.content.Context import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable 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.ContextCompat import com.xw.common.R import kotlin.math.max import kotlin.math.min /** * 滑动按钮自定义 View */ class SlideButtonView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { companion object { const val DIRECTION_LEFT = 0 const val DIRECTION_RIGHT = 1 const val SHAPE_CIRCLE = 0 const val SHAPE_SQUARE = 1 const val SHAPE_ROUND_RECT = 2 } // 固定间距 2dp private val fixedMargin = dpToPx(3f) private var slideDirection: Int = DIRECTION_RIGHT private var backgroundColor: Int = Color.parseColor("#2196F3") private var backgroundCornerRadius: Float = 0f private var text: String = "滑动" private var textColor: Int = Color.WHITE private var textSize: Float = 48f private var thumbDrawableRes: Int = -1 private var thumbBitmap: Bitmap? = null private var thumbWidth: Float = 0f private var thumbHeight: Float = 0f private var thumbShape: Int = SHAPE_ROUND_RECT private var thumbCornerRadius: Float = 0f private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG) private var thumbX: Float = 0f private var maxThumbX: Float = 0f private var isDragging = false private var slideThreshold: Float = 0.7f //是否能够使用 var enable: Boolean = true set(value) { field = value invalidate() } private val scroller = Scroller(context, DecelerateInterpolator()) private var onSlideCompleteListener: (() -> Unit)? = null init { context.theme.obtainStyledAttributes( attrs, R.styleable.SlideButtonView, defStyleAttr, 0 ).apply { try { slideDirection = getInt(R.styleable.SlideButtonView_slideDirection, DIRECTION_RIGHT) backgroundColor = getColor(R.styleable.SlideButtonView_slideBackgroundColor, backgroundColor) backgroundCornerRadius = getDimension(R.styleable.SlideButtonView_backgroundCornerRadius, 0f) text = getString(R.styleable.SlideButtonView_slideText) ?: text textColor = getColor(R.styleable.SlideButtonView_slideTextColor, textColor) textSize = getDimension(R.styleable.SlideButtonView_slideTextSize, textSize) thumbDrawableRes = getResourceId(R.styleable.SlideButtonView_thumbDrawable, -1) thumbShape = getInt(R.styleable.SlideButtonView_thumbShape, SHAPE_ROUND_RECT) thumbCornerRadius = getDimension(R.styleable.SlideButtonView_thumbCornerRadius, 0f) slideThreshold = getFloat(R.styleable.SlideButtonView_slideThreshold, 0.7f) enable = getBoolean(R.styleable.SlideButtonView_enable, enable) } finally { recycle() } } initPaints() loadThumbDrawable() } private fun initPaints() { backgroundPaint.color = backgroundColor textPaint.apply { color = textColor textSize = this@SlideButtonView.textSize textAlign = Paint.Align.CENTER isFakeBoldText = true // 粗体效果 // typeface = Typeface.DEFAULT_BOLD } thumbPaint.apply { isFilterBitmap = true isAntiAlias = true } } private fun loadThumbDrawable() { if (thumbDrawableRes != -1) { val drawable = ContextCompat.getDrawable(context, thumbDrawableRes) drawable?.let { thumbBitmap = if (it is BitmapDrawable) { it.bitmap } else { drawableToBitmap(it) } } } } private fun drawableToBitmap(drawable: Drawable): Bitmap { val width = drawable.intrinsicWidth.coerceAtLeast(1) val height = drawable.intrinsicHeight.coerceAtLeast(1) val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return bitmap } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) if (backgroundCornerRadius == 0f) { backgroundCornerRadius = h / 2f } // 滑块高度 = 总高度 - 上下间距(各 5dp) thumbHeight = h - fixedMargin * 2 thumbWidth = if (thumbBitmap != null) { val ratio = thumbBitmap!!.width.toFloat() / thumbBitmap!!.height thumbHeight * ratio } else { thumbHeight } maxThumbX = w - thumbWidth - fixedMargin * 2 resetThumbPosition() } private fun resetThumbPosition() { thumbX = when (slideDirection) { DIRECTION_LEFT -> maxThumbX else -> 0f } } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val width = width.toFloat() val height = height.toFloat() // 绘制背景(全填充,无内边距) val backgroundRect = RectF(0f, 0f, width, height) canvas.drawRoundRect(backgroundRect, backgroundCornerRadius, backgroundCornerRadius, backgroundPaint) // 绘制文字 val textX = width / 2 val textY = height / 2 + textPaint.textSize / 3 canvas.drawText(text, textX, textY, textPaint) // 绘制滑块(距离边缘 5dp) thumbBitmap?.let { bitmap -> val left = fixedMargin + thumbX val top = fixedMargin val right = left + thumbWidth val bottom = top + thumbHeight when (thumbShape) { SHAPE_CIRCLE -> { val path = Path().apply { val centerX = (left + right) / 2 val centerY = (top + bottom) / 2 val radius = min(thumbWidth, thumbHeight) / 2 addCircle(centerX, centerY, radius, Path.Direction.CW) } canvas.save() canvas.clipPath(path) canvas.drawBitmap(bitmap, null, RectF(left, top, right, bottom), thumbPaint) canvas.restore() } SHAPE_ROUND_RECT -> { val radius = if (thumbCornerRadius == 0f) { min(thumbWidth, thumbHeight) / 2 } else { thumbCornerRadius } val path = Path().apply { addRoundRect( RectF(left, top, right, bottom), radius, radius, Path.Direction.CW ) } canvas.save() canvas.clipPath(path) canvas.drawBitmap(bitmap, null, RectF(left, top, right, bottom), thumbPaint) canvas.restore() } else -> { canvas.drawBitmap(bitmap, null, RectF(left, top, right, bottom), thumbPaint) } } } } override fun onTouchEvent(event: MotionEvent): Boolean { if (!enable) { return super.onTouchEvent(event) } when (event.action) { MotionEvent.ACTION_DOWN -> { val touchX = event.x val thumbLeft = fixedMargin + thumbX val thumbRight = thumbLeft + thumbWidth if (touchX in thumbLeft..thumbRight) { isDragging = true parent.requestDisallowInterceptTouchEvent(true) return true } } MotionEvent.ACTION_MOVE -> { if (isDragging) { val rawX = event.x - thumbWidth / 2 - fixedMargin thumbX = min(max(rawX, 0f), maxThumbX) invalidate() return true } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { if (isDragging) { isDragging = false parent.requestDisallowInterceptTouchEvent(false) val progress = if (slideDirection == DIRECTION_LEFT) { 1 - (thumbX / maxThumbX) } else { thumbX / maxThumbX } if (progress >= slideThreshold) { onSlideCompleteListener?.invoke() } else { animateReset() } return true } } } return super.onTouchEvent(event) } fun animateReset() { val startX = thumbX.toInt() val endX = when (slideDirection) { DIRECTION_LEFT -> maxThumbX.toInt() else -> 0 } scroller.startScroll(startX, 0, endX - startX, 0, 300) invalidate() } override fun computeScroll() { if (scroller.computeScrollOffset()) { thumbX = scroller.currX.toFloat() invalidate() if (!scroller.isFinished) { postInvalidateOnAnimation() } } } private fun dpToPx(dp: Float): Float { return dp * resources.displayMetrics.density } // ==================== 公共 API ==================== fun setOnSlideCompleteListener(listener: () -> Unit) { onSlideCompleteListener = listener } fun setSlideDirection(direction: Int) { slideDirection = direction resetThumbPosition() invalidate() } fun getSlideDirection() = slideDirection fun setSlideBackgroundColor(color: Int) { backgroundColor = color backgroundPaint.color = color invalidate() } fun setBackgroundCornerRadius(radius: Float) { backgroundCornerRadius = radius invalidate() } fun setSlideText(text: String) { this.text = text invalidate() } fun setSlideTextColor(color: Int) { textColor = color textPaint.color = color invalidate() } fun setSlideTextSize(size: Float) { textSize = size textPaint.textSize = size invalidate() } fun setThumbDrawable(resId: Int) { thumbDrawableRes = resId loadThumbDrawable() requestLayout() invalidate() } fun setThumbBitmap(bitmap: Bitmap?) { thumbBitmap = bitmap requestLayout() invalidate() } fun setThumbShape(shape: Int) { thumbShape = shape invalidate() } fun setThumbCornerRadius(radius: Float) { thumbCornerRadius = radius invalidate() } fun setSlideThreshold(threshold: Float) { slideThreshold = threshold.coerceIn(0f, 1f) } }