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)
//            }
//        }
相关推荐
Entropless11 分钟前
OkHttp 深度解析(一) : 从一次完整请求看 OkHttp 整体架构
android·okhttp
v***913034 分钟前
Spring+Quartz实现定时任务的配置方法
android·前端·后端
wilsend1 小时前
Android Studio 2024版新建java项目和配置环境下载加速
android
兰琛1 小时前
Android Compose展示PDF文件
android·pdf
走在路上的菜鸟2 小时前
Android学Dart学习笔记第四节 基本类型
android·笔记·学习
百锦再2 小时前
第21章 构建命令行工具
android·java·图像处理·python·计算机视觉·rust·django
skyhh4 小时前
Android Studio 最新版汉化
android·ide·android studio
路人甲ing..4 小时前
Android Studio 快速的制作一个可以在 手机上跑的app
android·java·linux·智能手机·android studio
携欢7 小时前
PortSwigger靶场之Web shell upload via path traversal靶场通关秘籍
android
消失的旧时光-194315 小时前
Android ADB指令大全详解
android·adb