Android_koglin_自定义右滑拖动确认发送控件

52 阅读4分钟
package global.xfinite.coinbox

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat
import global.xfinite.library.R
import kotlin.math.max
import kotlin.math.min

class SwipeToSendView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // 画笔
    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#F5F6F8")
        style = Paint.Style.FILL
    }

    private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#44D564")
        style = Paint.Style.FILL
    }

    private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#44D564")
        style = Paint.Style.FILL
    }

    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#222222")
        textSize = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            16f,
            resources.displayMetrics
        )
        textAlign = Paint.Align.CENTER
        typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) // 使用系统默认粗体
    }
    private lateinit var arrowBitmap: Bitmap

    // 尺寸
    private var cornerRadius = 0f
    private var thumbRadius = 0f
    private var thumbPosition = 0f
    private var verticalPadding = 0f // 2dp 的垂直间距

    // 状态
    private var isDragging = false
    private var isVerified = false
    private var lastX = 0f

    // 文字
    private val text = "Swipe to send"
    private val textBounds = Rect().also {
        textPaint.getTextBounds(text, 0, text.length, it)
    }

    // 轨迹区域
    private val trackRect = RectF()
    private val clipPath = Path()

    init {
        verticalPadding = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            2f,
            resources.displayMetrics
        )
        setLayerType(LAYER_TYPE_HARDWARE, null) // 启用硬件加速

        arrowBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_arrow_cps_go_night)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        cornerRadius = (height - verticalPadding * 2) / 2f
        thumbRadius = (height - verticalPadding * 2) / 2f
        thumbPosition = thumbRadius

        // 初始化裁剪路径
        clipPath.reset()
        clipPath.addRoundRect(
            0f, verticalPadding,
            width.toFloat(), height - verticalPadding,
            cornerRadius, cornerRadius,
            Path.Direction.CW
        )

        // 缩放 arrowBitmap(如果单独使用)
        val arrowSize = (thumbRadius * 0.8f).toInt()
        arrowBitmap = Bitmap.createScaledBitmap(
            arrowBitmap,
            TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                9f,
                resources.displayMetrics
            ).toInt(),
            TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                18f,
                resources.displayMetrics
            ).toInt(),
            true
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // 1. 绘制背景(带垂直间距)
        canvas.drawRoundRect(
            0f, verticalPadding,
            width.toFloat(), height - verticalPadding,
            cornerRadius, cornerRadius, bgPaint
        )

        // 4. 绘制文字(居中)
        val textY = height / 2f - (textBounds.top + textBounds.bottom) / 2f
        canvas.drawText(text, width / 2f, textY, textPaint)

        // 2. 绘制轨迹(使用矩形而不是路径提高性能)
        if (thumbPosition > thumbRadius) {
            trackRect.set(
                0f, verticalPadding,
                thumbPosition + thumbRadius, height - verticalPadding
            )
            canvas.drawRoundRect(
                trackRect,
                cornerRadius, cornerRadius,
                trackPaint
            )
        }

        // 3. 绘制滑块
        canvas.drawCircle(
            thumbPosition,
            height / 2f,
            thumbRadius,
            thumbPaint
        )
        // 4. 绘制箭头(如果单独使用)
        canvas.drawBitmap(
            arrowBitmap,
            thumbPosition - arrowBitmap.width / 2f,  // x 居中
            height / 2f - arrowBitmap.height / 2f,   // y 居中
            null
        )


    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 检查是否点击了滑块
                val touchX = event.x
                val touchY = event.y
                val distance = Math.sqrt(
                    Math.pow((touchX - thumbPosition).toDouble(), 2.0) +
                            Math.pow((touchY - height / 2f).toDouble(), 2.0)
                )

                if (distance <= thumbRadius) {
                    isDragging = true
                    lastX = event.x
                    return true
                }
            }

            MotionEvent.ACTION_MOVE -> {
                if (isDragging && !isVerified) {
                    val deltaX = event.x - lastX
                    thumbPosition += deltaX

                    // 限制滑块位置
                    thumbPosition = max(thumbRadius, min(thumbPosition, width - thumbRadius))
                    lastX = event.x

                    // 只调用invalidate()而不是postInvalidate()以提高性能
                    invalidate()
                    return true
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                if (isDragging) {
                    isDragging = false

                    // 检查是否滑动到最右边
                    if (thumbPosition >= width - thumbRadius - 10) {
                        isVerified = true
                        listener?.onVerified()
                    } else {
                        // 未到最右边,回弹动画
                        startReboundAnimation()
                    }
                    return true
                }
            }
        }
        return super.onTouchEvent(event)
    }

    private fun startReboundAnimation() {
        val startTime = System.currentTimeMillis()
        val startPosition = thumbPosition
        val duration = 300L // 动画时长300ms

        post(object : Runnable {
            override fun run() {
                val elapsed = System.currentTimeMillis() - startTime
                if (elapsed < duration) {
                    // 使用插值器实现平滑回弹
                    val interpolation = Math.sin((elapsed / duration.toFloat()) * Math.PI / 2).toFloat()
                    thumbPosition = thumbRadius + (startPosition - thumbRadius) * (1 - interpolation)
                    invalidate()
                    postDelayed(this, 16)
                } else {
                    // 确保最终位置正确
                    thumbPosition = thumbRadius
                    invalidate()
                }
            }
        })
    }

    // 验证回调接口
    interface OnVerifiedListener {
        fun onVerified()
    }

    private var listener: OnVerifiedListener? = null

    fun setOnVerifiedListener(listener: OnVerifiedListener) {
        this.listener = listener
    }

    // 重置验证状态
    fun reset() {
        isVerified = false
        thumbPosition = thumbRadius
        invalidate()
    }
}

优化版本可以设置颜色

package global.xfinite.conso.view

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat
import global.xfinite.conso.library.R
import kotlin.math.max
import kotlin.math.min

class SwipeToSendView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // 画笔
    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
    }

    private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
    }

    private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
    }

    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#222222")
        textSize = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            16f,
            resources.displayMetrics
        )
        textAlign = Paint.Align.CENTER
        typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) // 使用系统默认粗体
    }

    private var arrowBitmap: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_arrow_cps_go_night)

    // 尺寸
    private var cornerRadius = 0f
    private var thumbRadius = 0f
    private var thumbPosition = 0f
    private var verticalPadding = 0f // 2dp 的垂直间距

    // 状态
    private var isDragging = false
    private var isVerified = false
    private var lastX = 0f

    // 文字
    private val text = "Swipe to send"
    private val textBounds = Rect().also {
        textPaint.getTextBounds(text, 0, text.length, it)
    }

    // 轨迹区域
    private val trackRect = RectF()
    private val clipPath = Path()

    // 可配置的颜色
    var backColor: Int = Color.parseColor("#F5F6F8")
        set(value) {
            field = value
            bgPaint.color = value
            invalidate() // 触发重绘
        }

    var trackColor: Int = Color.parseColor("#44D564")
        set(value) {
            field = value
            trackPaint.color = value
            invalidate() // 触发重绘
        }

    var thumbColor: Int = Color.parseColor("#44D564")
        set(value) {
            field = value
            thumbPaint.color = value
            invalidate() // 触发重绘
        }

    var textColor: Int = Color.parseColor("#222222")
        set(value) {
            field = value
            textPaint.color = value
            invalidate() // 触发重绘
        }

    init {
        verticalPadding = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            2f,
            resources.displayMetrics
        )
        setLayerType(LAYER_TYPE_HARDWARE, null) // 启用硬件加速

        // 初始化画笔颜色
        bgPaint.color = backColor
        trackPaint.color = trackColor
        thumbPaint.color = thumbColor
        textPaint.color = textColor

        arrowBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_arrow_cps_go_night)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        cornerRadius = (height - verticalPadding * 2) / 2f
        thumbRadius = (height - verticalPadding * 2) / 2f
        thumbPosition = thumbRadius

        // 初始化裁剪路径
        clipPath.reset()
        clipPath.addRoundRect(
            0f, verticalPadding,
            width.toFloat(), height - verticalPadding,
            cornerRadius, cornerRadius,
            Path.Direction.CW
        )

        // 缩放 arrowBitmap(如果单独使用)
        val arrowSize = (thumbRadius * 0.8f).toInt()
        arrowBitmap = Bitmap.createScaledBitmap(
            arrowBitmap,
            TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                9f,
                resources.displayMetrics
            ).toInt(),
            TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                18f,
                resources.displayMetrics
            ).toInt(),
            true
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // 1. 绘制背景(带垂直间距)
        canvas.drawRoundRect(
            0f, verticalPadding,
            width.toFloat(), height - verticalPadding,
            cornerRadius, cornerRadius, bgPaint
        )

        // 4. 绘制文字(居中)
        val textY = height / 2f - (textBounds.top + textBounds.bottom) / 2f
        canvas.drawText(text, width / 2f, textY, textPaint)

        // 2. 绘制轨迹(使用矩形而不是路径提高性能)
        if (thumbPosition > thumbRadius) {
            trackRect.set(
                0f, verticalPadding,
                thumbPosition + thumbRadius, height - verticalPadding
            )
            canvas.drawRoundRect(
                trackRect,
                cornerRadius, cornerRadius,
                trackPaint
            )
        }

        // 3. 绘制滑块
        canvas.drawCircle(
            thumbPosition,
            height / 2f,
            thumbRadius,
            thumbPaint
        )
        // 4. 绘制箭头(如果单独使用)
        canvas.drawBitmap(
            arrowBitmap,
            thumbPosition - arrowBitmap.width / 2f,  // x 居中
            height / 2f - arrowBitmap.height / 2f,   // y 居中
            null
        )
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 检查是否点击了滑块
                val touchX = event.x
                val touchY = event.y
                val distance = Math.sqrt(
                    Math.pow((touchX - thumbPosition).toDouble(), 2.0) +
                            Math.pow((touchY - height / 2f).toDouble(), 2.0)
                )

                if (distance <= thumbRadius) {
                    isDragging = true
                    lastX = event.x
                    return true
                }
            }

            MotionEvent.ACTION_MOVE -> {
                if (isDragging && !isVerified) {
                    val deltaX = event.x - lastX
                    thumbPosition += deltaX

                    // 限制滑块位置
                    thumbPosition = max(thumbRadius, min(thumbPosition, width - thumbRadius))
                    lastX = event.x

                    // 只调用invalidate()而不是postInvalidate()以提高性能
                    invalidate()
                    return true
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                if (isDragging) {
                    isDragging = false

                    // 检查是否滑动到最右边
                    if (thumbPosition >= width - thumbRadius - 10) {
                        isVerified = true
                        listener?.onVerified()
                    } else {
                        // 未到最右边,回弹动画
                        startReboundAnimation()
                    }
                    return true
                }
            }
        }
        return super.onTouchEvent(event)
    }

    private fun startReboundAnimation() {
        val startTime = System.currentTimeMillis()
        val startPosition = thumbPosition
        val duration = 300L // 动画时长300ms

        post(object : Runnable {
            override fun run() {
                val elapsed = System.currentTimeMillis() - startTime
                if (elapsed < duration) {
                    // 使用插值器实现平滑回弹
                    val interpolation = Math.sin((elapsed / duration.toFloat()) * Math.PI / 2).toFloat()
                    thumbPosition = thumbRadius + (startPosition - thumbRadius) * (1 - interpolation)
                    invalidate()
                    postDelayed(this, 16)
                } else {
                    // 确保最终位置正确
                    thumbPosition = thumbRadius
                    invalidate()
                }
            }
        })
    }

    // 验证回调接口
    interface OnVerifiedListener {
        fun onVerified()
    }

    private var listener: OnVerifiedListener? = null

    fun setOnVerifiedListener(listener: OnVerifiedListener) {
        this.listener = listener
    }

    // 重置验证状态
    fun reset() {
        isVerified = false
        thumbPosition = thumbRadius
        invalidate()
    }

//    // 设置背景颜色
//    fun setBackgroundColor1(color: Int) {
//        backgroundColor = color
//    }
//
//    // 设置轨迹颜色
//    fun setTrackColor(color: Int) {
//        trackColor = color
//    }
//
//    // 设置滑块颜色
//    fun setThumbColor(color: Int) {
//        thumbColor = color
//    }
//
//    // 设置文字颜色
//    fun setTextColor(color: Int) {
//        textColor = color
//    }

    // 设置箭头位图
    fun setArrowBitmap(bitmap: Bitmap) {
        arrowBitmap = bitmap
        invalidate() // 触发重绘
    }

    // 设置箭头资源ID
    fun setArrowResource(resId: Int) {
        arrowBitmap = BitmapFactory.decodeResource(resources, resId)
        invalidate() // 触发重绘
    }
}