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)
// }
// }