Android自定义控件-仿keep结束按钮,带结束动效

115 阅读5分钟

Android自定义控件-仿keep结束按钮,带结束动效

image.png

image.png

属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--结束按钮的样式-->
    <declare-styleable name="TerminateButton">
        <!-- 图标颜色 -->
        <attr name="iconColor" format="color" />

        <!--图标大小-->
        <attr name="iconSize" format="dimension" />

        <!-- 背景底色 -->
        <attr name="backgroundColor" format="color" />

        <!--进度背景色-->
        <attr name="progressBackgroundColor" format="color" />

        <!--进度颜色-->
        <attr name="progressColor" format="color" />

        <!--是否执行3s动画-->
        <attr name="isAnimation" format="boolean" />

        <!-- 左上角弧度 -->
        <attr name="topLeftRadius_terminate" format="dimension" />

        <!-- 左下角弧度 -->
        <attr name="bottomLeftRadius_terminate" format="dimension" />

        <!-- 右上角弧度 -->
        <attr name="topRightRadius_terminate" format="dimension" />

        <!-- 右下角弧度 -->
        <attr name="bottomRightRadius_terminate" format="dimension" />

        <!--整体样式,圆形还是方形-->
        <attr name="shape_terminate" format="enum">
            <enum name="circle" value="0" />
            <enum name="square" value="1" />
        </attr>
    </declare-styleable>

</resources>

实现

package com.wuleizhenshang.fitness.mod_sport_record_detail

import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PathMeasure
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.core.animation.addListener

/**
 * @author: wuleizhenshang
 * @date: 2024/12/21 15:01
 * @description: 结束按钮
 */
class TerminateButton(context: Context, attrs: AttributeSet) : View(context, attrs) {

    companion object {
        const val STYLE_CIRCLE = 0
        const val STYLE_ROUND_RECT = 1
    }

    /**
     * 解析属性
     */
    /**
     * 图标颜色
     */
    private var _iconColor = Color.WHITE

    /**
     * 图标大小
     */
    private var _iconSize = 0f

    /**
     * 1/2图标大小
     */
    private var _halfIconSize = 0f

    /**
     * 背景颜色
     */
    private var _backgroundColor = Color.RED

    /**
     * 进度背景色
     */
    private var _progressBackgroundColor = Color.GRAY

    /**
     * 进度颜色
     */
    private var _progressColor = Color.BLUE

    /**
     * 左上角圆角半径
     */
    private var _topLeftRadius = 0f

    /**
     * 右上角圆角半径
     */
    private var _topRightRadius = 0f

    /**
     * 左下角圆角半径
     */
    private var _bottomLeftRadius = 0f

    /**
     * 右下角圆角半径
     */
    private var _bottomRightRadius = 0f

    /**
     * 整体样式
     */
    private var _style = STYLE_CIRCLE

    /**
     * 是否执行3s动画
     */
    private var _isAnimation = true

    /**
     * 控件宽度
     */
    private var _width = 0f

    /**
     * 1/2宽度
     */
    private var _halfWidth = 0f

    /**
     * 控件高度
     */
    private var _height = 0f

    /**
     * 1/2高度
     */
    private var _halfHeight = 0f

    /**
     * 宽高的较小值
     */
    private var _minSize = 0f

    /**
     * 画笔
     */
    private val _paint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.FILL
    }

    /**
     * 画进度的画笔
     */
    private val _progressPaint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.STROKE
    }

    /**
     * 圆角样式的背景路径
     */
    private val _backgroundPath = Path()

    /**
     * 圆角样式的进度背景路径
     */
    private val _progressBackgroundPath = Path()

    /**
     * 圆角样式的进度路径
     */
    private val _progressPath = Path()

    /**
     * 进度值动画
     */
    private var _progressAnimator: ValueAnimator? = null

    init {
        //解析属性
        val typedArray =
            context.theme.obtainStyledAttributes(attrs, R.styleable.TerminateButton, 0, 0)

        try {
            _iconSize = typedArray.getDimension(R.styleable.TerminateButton_iconSize, 0f)
            _halfIconSize = _iconSize / 2
            _iconColor = typedArray.getColor(R.styleable.TerminateButton_iconColor, Color.WHITE)
            _backgroundColor =
                typedArray.getColor(R.styleable.TerminateButton_backgroundColor, Color.RED)
            _progressBackgroundColor =
                typedArray.getColor(R.styleable.TerminateButton_progressBackgroundColor, Color.GRAY)
            _progressColor =
                typedArray.getColor(R.styleable.TerminateButton_progressColor, Color.BLUE)
            _topLeftRadius =
                typedArray.getDimension(R.styleable.TerminateButton_topLeftRadius_terminate, 0f)
            if (_topLeftRadius < 0) {
                _topLeftRadius = 0f
            }
            _topRightRadius =
                typedArray.getDimension(R.styleable.TerminateButton_topRightRadius_terminate, 0f)
            if (_topRightRadius < 0) {
                _topRightRadius = 0f
            }
            _bottomLeftRadius =
                typedArray.getDimension(R.styleable.TerminateButton_bottomLeftRadius_terminate, 0f)
            if (_bottomLeftRadius < 0) {
                _bottomLeftRadius = 0f
            }
            _bottomRightRadius =
                typedArray.getDimension(R.styleable.TerminateButton_bottomRightRadius_terminate, 0f)
            if (_bottomRightRadius < 0) {
                _bottomRightRadius = 0f
            }
            _style = typedArray.getInt(R.styleable.TerminateButton_shape_terminate, STYLE_CIRCLE)
            _isAnimation = typedArray.getBoolean(R.styleable.TerminateButton_isAnimation, true)
        } finally {
            typedArray.recycle()
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        _width = w.toFloat()
        _height = h.toFloat()
        _halfWidth = _width / 2
        _halfHeight = _height / 2
        _minSize = if (_width > _height) _height else _width
        if (_topLeftRadius > _minSize) {
            _topLeftRadius = _minSize
        }
        if (_topRightRadius > _minSize) {
            _topRightRadius = _minSize
        }
        if (_bottomLeftRadius > _minSize) {
            _bottomLeftRadius = _minSize
        }
        if (_bottomRightRadius > _minSize) {
            _bottomRightRadius = _minSize
        }
        initBackgroundPath()
        initProgressBackgroundPath()
        initProgressPath()
    }

    /**
     * 初始化圆角样式的背景路径
     */
    private fun initBackgroundPath() {
        if (_style == STYLE_ROUND_RECT) {
            _backgroundPath.reset()
            val balance = _minSize / 2
            _backgroundPath.addRoundRect(
                _halfWidth - balance,
                _halfHeight - balance,
                _halfWidth + balance,
                _halfHeight + balance,
                //四个角的圆弧,每两个表示圆弧的水平和垂直半径
                floatArrayOf(
                    _topLeftRadius,
                    _topLeftRadius,
                    _topRightRadius,
                    _topRightRadius,
                    _bottomRightRadius,
                    _bottomRightRadius,
                    _bottomLeftRadius,
                    _bottomLeftRadius
                ),
                //Path.Direction.CW 顺时针绘制
                Path.Direction.CW
            )
        }
    }

    /**
     * 初始化圆角样式的进度背景路径
     */
    private fun initProgressBackgroundPath() {
        if (_style == STYLE_ROUND_RECT) {
            _progressBackgroundPath.reset()
            val balance = _minSize * 8 / 24
            _progressBackgroundPath.addRoundRect(
                _halfWidth - balance,
                _halfHeight - balance,
                _halfWidth + balance,
                _halfHeight + balance,
                //四个角的圆弧,每两个表示圆弧的水平和垂直半径
                floatArrayOf(
                    _topLeftRadius,
                    _topLeftRadius,
                    _topRightRadius,
                    _topRightRadius,
                    _bottomRightRadius,
                    _bottomRightRadius,
                    _bottomLeftRadius,
                    _bottomLeftRadius
                ),
                //Path.Direction.CW 顺时针绘制
                Path.Direction.CW
            )
        }
    }

    /**
     * 初始化圆角样式的进度路径
     */
    private fun initProgressPath() {
        if (_style == STYLE_ROUND_RECT) {
            _progressPath.reset()

            // 计算背景路径的长度
            val pathMeasure = PathMeasure(_progressBackgroundPath, false)
            val progressPathLength = pathMeasure.length

            // 计算进度路径的长度
            val progressLength = progressPathLength * _progress

            // 获取从 0 到进度的路径段
            pathMeasure.getSegment(0f, progressLength, _progressPath, true)
        }
    }




    /**
     * 绘制
     */
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        //先画背景
        _paint.color = _backgroundColor
        //画圆形
        if (_style == STYLE_CIRCLE) {
            //传入圆心、半径、画笔
            canvas.drawCircle(_halfWidth, _halfHeight, _minSize / 2, _paint)
        }

        //画圆角矩形
        else if (_style == STYLE_ROUND_RECT) {
            canvas.drawPath(_backgroundPath, _paint)
        }

        //绘制图标
        _paint.color = _iconColor
        //画方形终止图标
        canvas.drawRect(
            _halfWidth - _halfIconSize,
            _halfHeight - _halfIconSize,
            _halfWidth + _halfIconSize,
            _halfHeight + _halfIconSize,
            _paint
        )

        //绘制进度
        if (_isProgressAnimating) {

            val per = _minSize / 24
            val balance = _minSize / 3

            if (_style == STYLE_CIRCLE) {
                //画圆形进度
                //画进度背景
                _progressPaint.apply {
                    color = _progressBackgroundColor
                    strokeWidth = per
                }

                canvas.drawCircle(_halfWidth, _halfHeight, balance, _progressPaint)
                //画进度
                _progressPaint.apply {
                    color = _progressColor
                    strokeWidth = per
                }
                canvas.drawArc(
                    _halfWidth - balance,
                    _halfHeight - balance,
                    _halfWidth + balance,
                    _halfHeight + balance,
                    //起始角度,从顶部开始
                    -90f,
                    //扫过的角度,360度为一圈
                    360 * _progress,
                    false,
                    _progressPaint
                )
            } else if (_style == STYLE_ROUND_RECT) {
                //画圆角矩形进度
                //画进度背景
                _progressPaint.apply {
                    color = _progressBackgroundColor
                    strokeWidth = per
                }
                canvas.drawPath(_progressBackgroundPath, _progressPaint)

                // 画进度
                _progressPaint.apply {
                    color = _progressColor
                    strokeWidth = per
                }
                canvas.drawPath(_progressPath, _progressPaint)
            }
        }
    }

    /**
     * 初始比例因子
     */
    private var _scaleFactor = 1f

    /**
     * 进度值
     */
    private var _progress = 0f

    /**
     * 是否在执行进度动画
     */
    private var _isProgressAnimating = false

    /**
     * 触摸事件
     */
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        //不执行动画就直接回调
        if (!_isAnimation) {
            _onTerminateListener?.invoke(true)
            return super.onTouchEvent(event)
        }

        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                //执行放大动画
                startAmplifyAnimation()
                //不用等结束就执行
                _progressAnimator = startProgressAnimation {
                    _onTerminateListener?.invoke(it)
                    //缩小动画
                    startReduceAnimation(onEnd = {
                        //结束重新绘制
                        invalidate()
                    })
                }
            }

            MotionEvent.ACTION_MOVE -> {
                // 如果手指移动,不需要处理
            }

            MotionEvent.ACTION_UP -> {
                if (_isProgressAnimating) {
                    _progressAnimator?.cancel()
                }
            }

            MotionEvent.ACTION_CANCEL -> {
                if (_isProgressAnimating) {
                    _progressAnimator?.cancel()
                }
            }
        }
        return true
    }

    /**
     * 开始放大动画
     */
    private fun startAmplifyAnimation(onEnd: (() -> Unit)? = null) {
        val scaleX = ObjectAnimator.ofFloat(this, "scaleX", _scaleFactor, 1.2f)
        val scaleY = ObjectAnimator.ofFloat(this, "scaleY", _scaleFactor, 1.2f)
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(scaleX, scaleY)
        animatorSet.duration = 300

        // 监听动画结束时记录最终尺寸
        animatorSet.addListener(onEnd = {
            // 记录最终比例因子
            _scaleFactor = 1.2f
            //记录宽高
            _width = width.toFloat()
            _height = height.toFloat()
            _halfWidth = _width / 2
            _halfHeight = _height / 2
            _minSize = if (_width > _height) _height else _width
            initBackgroundPath()
            initProgressBackgroundPath()
            initProgressPath()
            Log.d("TerminateButton放大", "width: $_width, height: $_height")
            onEnd?.invoke()
        })
        // 启动动画
        animatorSet.start()
    }

    /**
     * 开始缩小动画
     */
    private fun startReduceAnimation(onEnd: (() -> Unit)? = null) {
        val scaleX = ObjectAnimator.ofFloat(this, "scaleX", _scaleFactor, 1.0f)
        val scaleY = ObjectAnimator.ofFloat(this, "scaleY", _scaleFactor, 1.0f)
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(scaleX, scaleY)
        animatorSet.duration = 300

        // 监听动画结束时记录最终尺寸
        animatorSet.addListener(onEnd = {
            // 记录最终比例因子
            _scaleFactor = 1.0f
            //记录宽高
            _width = width.toFloat()
            _height = height.toFloat()
            _halfWidth = _width / 2
            _halfHeight = _height / 2
            _minSize = if (_width > _height) _height else _width
            initBackgroundPath()
            initProgressBackgroundPath()
            initProgressPath()
            Log.d("TerminateButton缩小", "width: $_width, height: $_height")
            onEnd?.invoke()
        })
        // 启动动画
        animatorSet.start()
    }

    /**
     * 开始进度动画
     */
    private fun startProgressAnimation(onCompleted: ((Boolean) -> Unit)? = null): ValueAnimator {
        // 从 0 到 1 的进度动画
        val progressAnimator = ValueAnimator.ofFloat(0f, 1f)
        // 2秒动画时长
        progressAnimator.duration = 1000
        //是否回调过,只会回调一次
        var isCallback = false

        progressAnimator.addUpdateListener { animation ->
            _progress = animation.animatedValue as Float
            initProgressPath()
            // 触发视图重绘
            invalidate()
        }

        progressAnimator.addListener(onStart = {
            _isProgressAnimating = true
        }, onEnd = {
            _isProgressAnimating = false
            if (!isCallback) {
                if (_progress == 1f) {
                    onCompleted?.invoke(true)
                } else {
                    onCompleted?.invoke(false)
                }
                isCallback = true
            }
            _progress = 0f
            progressAnimator.removeAllListeners()
        }, onCancel = {
            _isProgressAnimating = false
            if (!isCallback) {
                if (_progress == 1f) {
                    onCompleted?.invoke(true)
                } else {
                    onCompleted?.invoke(false)
                }
                isCallback = true
            }
            _progress = 0f
            progressAnimator.removeAllListeners()
        })

        progressAnimator.start()

        return progressAnimator
    }

    /**
     * 回调
     */
    private var _onTerminateListener: ((Boolean) -> Unit)? = null

    /**
     * 设置回调
     */
    fun setOnTerminateListener(listener: ((Boolean) -> Unit)) {
        _onTerminateListener = listener
    }
}

简单使用

<com.wuleizhenshang.fitness.mod_sport_record_detail.TerminateButton
    android:id="@+id/terminateButton"
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:bottomRightRadius_terminate="50dp"
    app:iconSize="40dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:shape_terminate="square"
    app:topLeftRadius_terminate="20dp"
    app:topRightRadius_terminate="30dp" />
binding.terminateButton.setOnTerminateListener { bool ->
    Toast.makeText(this, "bool = $bool", Toast.LENGTH_SHORT).show()
}