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)
//            }
//        }
相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui