Android自定义控件—一个简单的播放暂停播放按钮

Android自定义控件---一个简单的播放暂停播放按钮

这是一个简单的播放暂停按钮,可以在xml设置圆角样式或者直接设置为圆形样式,还提供了两个点击变化的动画效果,具体样式可以看下面

属性

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--开始和暂停按钮的一些样式-->
    <declare-styleable name="PlayOrPauseButton">
        <!-- 开始状态的图标颜色 -->
        <attr name="startIconColor" format="color" />

        <!-- 开始状态的背景底色 -->
        <attr name="startBackgroundColor" format="color" />

        <!-- 开始状态的内部图标大小 -->
        <attr name="startIconSize" format="dimension" />

        <!-- 暂停状态的图标颜色 -->
        <attr name="pauseIconColor" format="color" />

        <!-- 暂停状态的背景底色 -->
        <attr name="pauseBackgroundColor" format="color" />

        <!-- 暂停状态的内部图标大小 -->
        <attr name="pauseIconSize" format="dimension" />

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

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

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

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

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

        <!-- 动画类型:0-无动画, 1-动画类型1, 2-动画类型2 -->
        <attr name="animationType" format="enum">
            <enum name="none" value="0" />
            <enum name="rotateType" value="1" />
            <enum name="fluctuateType" value="2" />
        </attr>

        <!--是否自动变化暂停和开始状态-->
        <attr name="autoChange" format="boolean" />

        <!--初始状态-->
        <attr name="initIsPlaying" format="boolean" />
    </declare-styleable>

</resources>

代码

kotlin 复制代码
package com.wuleizhenshang.fitness.mod_sport_record_detail

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PorterDuff
import android.util.AttributeSet
import android.view.View
import kotlin.math.sqrt

/**
 * @author: wuleizhenshang
 * @date: 2024/12/20 16:51
 * @description: 简单播放暂停按钮
 */
class PlayOrPauseButton(context: Context, attrs: AttributeSet) : View(context, attrs) {
    /**
     * 解析自定义属性
     */
    /**
     * 开始状态的图标颜色
     */
    private var _startIconColor: Int = Color.WHITE

    /**
     * 开始状态的背景颜色
     */
    private var _startBackgroundColor: Int = Color.BLUE

    /**
     * 开始状态的图标大小
     */
    private var _startIconSize: Float = 40f

    /**
     * 暂停状态的图标颜色
     */
    private var _pauseIconColor: Int = Color.BLACK

    /**
     * 暂停状态的背景颜色
     */
    private var _pauseBackgroundColor: Int = Color.LTGRAY

    /**
     * 暂停状态的图标大小
     */
    private var _pauseIconSize: Float = 40f

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

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

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

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

    /**
     * 形状
     */
    private var _shape: Int = 0

    /**
     * 动画类型 0 无动画; 1 动画类型1 ; 2 动画类型2
     */
    private var _animationType: Int = 0

    /**
     * 是否自动变化
     */
    private var _autoChange = false

    /**
     * 是否正在播放
     */
    private var _isPlaying = false

    /**
     * 解析属性
     * 初始化
     */
    init {
        // 从 attrs.xml 中解析自定义属性
        val typedArray =
            context.theme.obtainStyledAttributes(attrs, R.styleable.PlayOrPauseButton, 0, 0)

        try {
            _startIconColor =
                typedArray.getColor(R.styleable.PlayOrPauseButton_startIconColor, Color.BLACK)
            _startBackgroundColor =
                typedArray.getColor(R.styleable.PlayOrPauseButton_startBackgroundColor, Color.WHITE)
            _startIconSize =
                typedArray.getDimension(R.styleable.PlayOrPauseButton_startIconSize, 40f)

            _pauseIconColor =
                typedArray.getColor(R.styleable.PlayOrPauseButton_pauseIconColor, Color.BLACK)
            _pauseBackgroundColor =
                typedArray.getColor(R.styleable.PlayOrPauseButton_pauseBackgroundColor, Color.WHITE)
            _pauseIconSize =
                typedArray.getDimension(R.styleable.PlayOrPauseButton_pauseIconSize, 40f)

            _topLeftRadius =
                typedArray.getDimension(R.styleable.PlayOrPauseButton_topLeftRadius, 0f)
            _bottomLeftRadius =
                typedArray.getDimension(R.styleable.PlayOrPauseButton_bottomLeftRadius, 0f)
            _topRightRadius =
                typedArray.getDimension(R.styleable.PlayOrPauseButton_topRightRadius, 0f)
            _bottomRightRadius =
                typedArray.getDimension(R.styleable.PlayOrPauseButton_bottomRightRadius, 0f)

            _shape = typedArray.getInt(R.styleable.PlayOrPauseButton_shape, 0)

            _animationType = typedArray.getInt(R.styleable.PlayOrPauseButton_animationType, 0)

            _autoChange = typedArray.getBoolean(R.styleable.PlayOrPauseButton_autoChange, true)

            _isPlaying = typedArray.getBoolean(R.styleable.PlayOrPauseButton_initIsPlaying, true)
        } finally {
            typedArray.recycle()
        }

        // 设置点击事件
        setOnClickListener {
            if (_autoChange) {
                // 点击后状态取反
                _isPlaying = !_isPlaying
                // 回调监听
                _onPlayOrPauseChangeListener?.invoke(_isPlaying)
                // 根据动画类型刷新视图
                if (_animationType == 0) {
                    invalidate()
                } else if (_animationType == 1) {
                    rotateButtonAndInvalidate()
                }else{
                    startSizeAnimationAndInvalidate()
                }
            }
        }
    }

    /**
     * 一些需要的属性和对象
     */
    /**
     * 控件宽
     */
    private var _width = 0f

    /**
     * 1/2的宽度
     */
    private var _width2 = 0f

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

    /**
     * 1/2的高度
     */
    private var _height2 = 0f

    /**
     * 两者的最小值
     */
    private var _min = 0f

    /**
     * 画笔
     */
    private val _paint = Paint().apply {
        // 设置抗锯齿
        isAntiAlias = true
        // 设置填充样式
        style = Paint.Style.FILL
    }

    /**
     * 画background形状的路径
     */
    private val _pathBackground = Path()

    /**
     * 画暂停图标左边的竖线的路径
     */
    private val _pathPauseLeft = Path()

    /**
     * 画暂停图标右边的竖线的路径
     */
    private val _pathPauseRight = Path()

    /**
     * 画开始图标的路径
     */
    private val _pathStart = Path()

    /**
     * 1/6的暂停图标的宽度
     */
    private var _pauseIconWidth6 = 0f

    /**
     * 1/3的暂停图标的宽度
     */
    private var _pauseIconWidth3 = 0f

    /**
     * 1/2的暂停图标的宽度
     */
    private var _pauseIconWidth2 = 0f

    /**
     * 1/2的开始图标的宽度
     */
    private var _startIconWidth2 = 0f

    /**
     * 布局大小改变时调用
     */
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //记录宽高
        _width = w.toFloat()
        _height = h.toFloat()

        //计算1/2的宽高
        _width2 = _width / 2
        _height2 = _height / 2

        _min = if (_width > _height) _height else _width

        //如果弧度大于宽和高的较小值就取最小值
        _topLeftRadius = if (_topLeftRadius > _min) _min else _topLeftRadius
        _bottomLeftRadius = if (_bottomLeftRadius > _min) _min else _bottomLeftRadius
        _topRightRadius = if (_topRightRadius > _min) _min else _topRightRadius
        _bottomRightRadius = if (_bottomRightRadius > _min) _min else _bottomRightRadius

        //如果图标大小大于宽和高的较小值就取最小值
        _startIconSize = if (_startIconSize > _min) _min else _startIconSize
        _pauseIconSize = if (_pauseIconSize > _min) _min else _pauseIconSize

        //计算暂停图标的宽度
        _pauseIconWidth6 = _pauseIconSize / 6
        _pauseIconWidth3 = _pauseIconSize / 3
        _pauseIconWidth2 = _pauseIconSize / 2

        //计算开始图标的宽度
        _startIconWidth2 = _startIconSize / 2

        //初始化背景路径
        initBackgroundPath()
        //初始化暂停图标左边的竖线路径
        initPauseLeftPath()
        //初始化暂停图标右边的竖线路径
        initPauseRightPath()
        //初始化开始图标路径
        initStartPath()
    }

    /**
     * 初始化背景路径
     */
    private fun initBackgroundPath() {
        //清除之前的路径
        _pathBackground.reset()
        //重新绘制
        //移动到其实点,左上角弧度的开始位置
        _pathBackground.moveTo(_topLeftRadius, 0f)
        //先画左上角弧度到右上角弧度的第一个点的直线
        _pathBackground.lineTo(_width - _topRightRadius, 0f)
        //画右上角弧度(传入一个借助点,就是画布的右上角,凭借这个点和终点画一个弧度)
        _pathBackground.quadTo(_width, 0f, _width, _topRightRadius)
        //画右边的直线
        _pathBackground.lineTo(_width, _height - _bottomRightRadius)
        //画右下角弧度
        _pathBackground.quadTo(_width, _height, _width - _bottomRightRadius, _height)
        //画底边的直线
        _pathBackground.lineTo(_bottomLeftRadius, _height)
        //画左下角弧度
        _pathBackground.quadTo(0f, _height, 0f, _height - _bottomLeftRadius)
        //画左边的直线
        _pathBackground.lineTo(0f, _topLeftRadius)
        //画左上角弧度
        _pathBackground.quadTo(0f, 0f, _topLeftRadius, 0f)
        //闭合路径
        _pathBackground.close()
    }

    /**
     * 初始化暂停图标左边的竖线路径
     */
    private fun initPauseLeftPath() {
        //清除之前的路径
        _pathPauseLeft.reset()
        //重新绘制
        //移动到开始点
        _pathPauseLeft.moveTo(
            _width2 - _pauseIconWidth6 - _pauseIconWidth3,
            _height2 - _pauseIconWidth2
        )
        //画上面直线
        _pathPauseLeft.lineTo(_width2 - _pauseIconWidth6, _height2 - _pauseIconWidth2)
        //画右边直线
        _pathPauseLeft.lineTo(_width2 - _pauseIconWidth6, _height2 + _pauseIconWidth2)
        //画下面直线
        _pathPauseLeft.lineTo(
            _width2 - _pauseIconWidth6 - _pauseIconWidth3,
            _height2 + _pauseIconWidth2
        )
        //闭合路径(终点和起点连接直线)
        _pathPauseLeft.close()
    }

    /**
     * 初始化暂停图标右边的竖线路径
     */
    private fun initPauseRightPath() {
        //清除之前的路径
        _pathPauseRight.reset()
        //重新绘制
        //移动到开始点
        _pathPauseRight.moveTo(_width2 + _pauseIconWidth6, _height2 - _pauseIconWidth2)
        //画上面直线
        _pathPauseRight.lineTo(
            _width2 + _pauseIconWidth6 + _pauseIconWidth3,
            _height2 - _pauseIconWidth2
        )
        //画右边直线
        _pathPauseRight.lineTo(
            _width2 + _pauseIconWidth6 + _pauseIconWidth3,
            _height2 + _pauseIconWidth2
        )
        //画下面直线
        _pathPauseRight.lineTo(_width2 + _pauseIconWidth6, _height2 + _pauseIconWidth2)
        //闭合路径(终点和起点连接直线)
        _pathPauseRight.close()
    }

    /**
     * 初始化开始图标路径
     */
    private fun initStartPath() {
        //清除之前的路径
        _pathStart.reset()
        //重新绘制
        //勾股定理计算横向线的长度,这里斜边为_startIconSize,一直角边为_startIconWidth2
        val otherWidth = (sqrt(3.0) / 2 * _startIconSize).toFloat()
        //一半
        val otherWidth2 = (otherWidth / 2).toFloat()
        //移动到开始点
        _pathStart.moveTo(_width2 - otherWidth2 + otherWidth2 / 4, _height2 - _startIconWidth2)
        //移动到右边的点,画斜向右下的线
        _pathStart.lineTo(_width2 + otherWidth2 + otherWidth2 / 4, _height2)
        //移动到左边的点,画斜向左下的线
        _pathStart.lineTo(_width2 - otherWidth2 + otherWidth2 / 4, _height2 + _startIconWidth2)
        //闭合路径(终点和起点连接直线)
        _pathStart.close()
    }

    /**
     * onDraw和onLayout默认就好,这里onDraw绘制
     */
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //不需要手动清空画布,系统会自动清空
        //开始中,绘制暂停状态
        if (_isPlaying) {
            _paint.color = _pauseBackgroundColor
            //绘制圆形背景
            if (_shape == 0) {
                canvas.drawCircle(_width2, _height2, _min / 2, _paint)
            }
            //绘制path背景
            else {
                canvas.drawPath(_pathBackground, _paint)
            }
            //绘制暂停图标
            //绘制暂停图标
            _paint.color = _pauseIconColor
            canvas.drawPath(_pathPauseLeft, _paint)
            canvas.drawPath(_pathPauseRight, _paint)
        }
        //暂停中,绘制开始状态
        else {
            _paint.color = _startBackgroundColor
            //绘制圆形背景
            if (_shape == 0) {
                canvas.drawCircle(_width2, _height2, _min / 2, _paint)
            }
            //绘制path背景
            else {
                canvas.drawPath(_pathBackground, _paint)
            }
            //绘制开始图标
            _paint.color = _startIconColor
            canvas.drawPath(_pathStart, _paint)
        }
    }

    /**
     * 设置是否正在播放,让外部监听改变吧,内部就不监听改变状态了,可能外部需要根据一些状态决定点击是否改变状态
     */
    fun setIsPlaying(isPlaying: Boolean) {
        _isPlaying = isPlaying
        _onPlayOrPauseChangeListener?.invoke(_isPlaying)
        // 根据动画类型刷新视图
        if (_animationType == 0) {
            invalidate()
        } else if (_animationType == 1) {
            rotateButtonAndInvalidate()
        }else{
            startSizeAnimationAndInvalidate()
        }
    }

    /**
     * 获取是否正在播放
     */
    fun getIsPlaying(): Boolean {
        return _isPlaying
    }

    /**
     * 播放暂停状态改变监听
     */
    private var _onPlayOrPauseChangeListener: ((Boolean) -> Unit)? = null

    fun setOnPlayOrPauseChangeListener(listener: ((Boolean) -> Unit)) {
        _onPlayOrPauseChangeListener = listener
    }


    /**
     * 旋转按钮动画并刷新视图
     * 每次点击时旋转180度
     */
    private fun rotateButtonAndInvalidate() {
        // 使用 ObjectAnimator 执行旋转动画
        val animator = ObjectAnimator.ofFloat(this, "rotation", rotation, rotation + 180f)
        animator.duration = 300


        // 设置动画完成监听器
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                super.onAnimationEnd(animation)
                // 旋转完成后,刷新视图
                invalidate()
            }
        })

        animator.start()
    }

    /**
     * 启动大小波动动画并刷新视图
     */
    private fun startSizeAnimationAndInvalidate() {
        // 缩放动画:先做缩小后放大的波动效果
        val scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 1f, 1.2f, 1f)
        val scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 1f, 1.2f, 1f)

        // 设置动画持续时间
        scaleXAnimator.duration = 300
        scaleYAnimator.duration = 300

        // 动画开始时执行缩放,结束时恢复
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(scaleXAnimator, scaleYAnimator)

        // 设置动画结束后切换状态
        animatorSet.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                super.onAnimationEnd(animation)
                // 刷新视图
                invalidate()
            }
        })

        // 开始动画
        animatorSet.start()
    }

}

简单使用

xml 复制代码
<com.wuleizhenshang.fitness.mod_sport_record_detail.PlayOrPauseButton
    android:id="@+id/playOrPauseButton"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:pauseBackgroundColor="@color/black_FF222222"
    app:pauseIconColor="@color/white_FFFFFFFF"
    app:pauseIconSize="40dp"
    app:initIsPlaying="false"
    app:autoChange="true"
    app:shape="circle"
    app:animationType="rotateType"
    app:startBackgroundColor="@color/blue_FFA0CDE5"
    app:startIconColor="@color/yellow_FFFFCC00"
    app:startIconSize="40dp" />

你可以设置autoChange为false,内部不处理点击变为暂停还是开始,你可以监听自己设置

kotlin 复制代码
        binding.playOrPauseButton.setOnPlayOrPauseChangeListener { bool ->
            Toast.makeText(this, "bool = $bool", Toast.LENGTH_SHORT).show()
        }

//        binding.playOrPauseButton.setOnClickListener {
//            if (binding.playOrPauseButton.getIsPlaying()) {
//                binding.playOrPauseButton.setIsPlaying(false)
//            }else{
//                binding.playOrPauseButton.setIsPlaying(true)
//            }
//        }
相关推荐
玖疯子21 分钟前
如何按照详细的步骤进行DedeCMS的安装过程?
android·dedecms·织梦cms
王家视频教程图书馆42 分钟前
请求go web后端接口 java安卓端播放视频
android·java·前端
mmsx2 小时前
android opencv导入进行编译
android·人工智能·opencv
truemi.732 小时前
flutter --no-color pub get 超时解决方法
android·flutter
wwwsmef14 小时前
android源码下载
android
droidHZ4 小时前
Compose Multiplatform 之旅—声明式UI
android·kotlin
Henry_He4 小时前
修改壁纸后进桌面一直黑屏问题分析
android
花生糖@4 小时前
Android XR 是什么?解释它的功能、设备、开发工具等
android·xr
zhangphil5 小时前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
android·kotlin