puzhibing
2026-08-21 5fa94b3718b4a78a4fcea4c97652b0ef20b7de3a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
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)
    }
}