首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Android自定义控件—一个可以可以改变形状和带点击动效的播放暂停按钮

  • 25-04-18 23:22
  • 4383
  • 7384
juejin.cn

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

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

image.png

image.png

image.png

属性

xml
代码解读
复制代码
"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="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" /> <attr name="topLeftRadius_playOrPause" format="dimension" /> <attr name="bottomLeftRadius_playOrPause" format="dimension" /> <attr name="topRightRadius_playOrPause" format="dimension" /> <attr name="bottomRightRadius_playOrPause" format="dimension" /> <attr name="shape_playOrPause" format="enum"> <enum name="circle" value="0" /> <enum name="square" value="1" /> attr> declare-styleable> resources>

代码

kotlin
代码解读
复制代码
package com.wuleizhenshang.fitness.mod_sport_record_detail.view.custom import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.util.AttributeSet import android.view.View import com.wuleizhenshang.fitness.mod_sport_record_detail.R 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_playOrPause, 0f) if (_topLeftRadius < 0) { _topLeftRadius = 0f } _bottomLeftRadius = typedArray.getDimension( R.styleable.PlayOrPauseButton_bottomLeftRadius_playOrPause, 0f ) if (_bottomLeftRadius < 0) { _bottomLeftRadius = 0f } _topRightRadius = typedArray.getDimension( R.styleable.PlayOrPauseButton_topRightRadius_playOrPause, 0f ) if (_topRightRadius < 0) { _topRightRadius = 0f } _bottomRightRadius = typedArray.getDimension( R.styleable.PlayOrPauseButton_bottomRightRadius_playOrPause, 0f ) if (_bottomRightRadius < 0) { _bottomRightRadius = 0f } _shape = typedArray.getInt(R.styleable.PlayOrPauseButton_shape_playOrPause, 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 + 360f) 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:animationType="rotateType" app:autoChange="true" app:initIsPlaying="false" 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="20dp" app:shape="square" app:startBackgroundColor="@color/blue_FFA0CDE5" app:startIconColor="@color/yellow_FFFFCC00" app:startIconSize="40dp" app:topLeftRadius="60dp" />

你可以设置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) // } // }
注:本文转载自juejin.cn的无泪真伤的文章"https://juejin.cn/post/7450482185435381823"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top