10分钟带你实现一个网抑云的音乐播放动效~

老规矩:先上效果图

还原度直接99.98%

实现流程介绍

  1. 首先在View的中心绘制一个圆形封面图(这里用的CircleImageView,不必多说)
  2. 添加动画,让View转动起来
  3. 在转动的基础上,添加外层的边框线,边框线边旋转边扩大,直至边缘消失
  4. 在动画暂停时,中心封面图立即停止旋转,但外层的边框线动画继续,直至所有边框线消失,动画才是真正的停止
  5. 优化:让边框线的扩大速度及产生间隔可动态配置(歌曲高潮时边框线会更加密集,快歌可适当增大边框线的扩大速度)

按流程实现效果

0.准备工作

创建文件继承自FrameLayout

这里使用了JvmOverloads注解,它的作用是自动生成当前方法的重载,这里就等于以前java代码里写的3个构造方法了,减少一些无用的模板代码。

kotlin 复制代码
class MusicAnimationView @JvmOverloads constructor(
    context : Context,
    attrs : AttributeSet? = null,
    defStyle : Int = 0
) : FrameLayout(context, attrs, defStyle){}

定义需要用到的变量

我在代码里面添加了很多注释来帮助大家理解,需要注意的点是:mAnimationFlag,如果在使用者调用stopAnimation的时候,直接真正意义上停止动画的话,现有的边框线将停止扩大无法移除,所以先在stop时将标志位设为false,禁止继续添加新的边框线,直到所有边框线移除才真的关闭动画。

scala 复制代码
/** 中心图片占整个View的比例 */
private val mImageSizeRatio = .5F
/** 中心图片的实际尺寸 */
private var mImageSize = 0
/** 中心图片边框线占图片的比例 */
private val mImageBorderWidthRatio = .03F
/** 中心图片边框线颜色 */
private val mImageBorderColor = Color.parseColor("#80FFFFFF")
/** 中心图片旋转角度 */
private var mImageRotation = 0F

/**
 * 动画标志位(关键!!!)
 * 当它为true时,开始动画,中心图片旋转,外圈边框线旋转、扩大,且可添加;
 * 当它从true转false时,中心图片停止旋转,但是外圈的边框线不会停止,而是继续扩大至消失;
 * 当它为false时,不会再添加新的边框线,直到边框线全部消失后,停止动画
 */
private var mAnimationFlag = true

/** 边框线list */
private val mBorderLines = mutableListOf<BorderLine>()
/** 边框线粗细 */
private var mBorderLineWidth = 2.0
/** 边框线扩大速度 */
@BorderLineSheep
var mBorderLineGrowSheep = SLOW
/** 边框线间隔 */
@BorderLineInterval
var mBorderLineIntervalDistance = COMPACT
/** 边框线画笔 */
private val mBorderLinePaint by lazy {
    Paint().apply {
        color = Color.parseColor("#80FF4777")
        strokeWidth = UIUtil.dip2px(context, mBorderLineWidth).toFloat()
    }
}

1.首先在View的中心绘制一个圆形封面图(这里用的CircleImageView,不必多说)

在代码中使用了lazy关键字初始化CircleImageView,其实就是用到的时候才初始化这个常量,但是实际上我在init的时候就用到它了,所以也等于创建整个View的时候就会创建这个image,然后设置它居中显示和边框属性,在init里面添加进布局里。

scss 复制代码
/** 中心圆形图片 唯一的子View */
val mImage : CircleImageView by lazy {
    CircleImageView(context, attrs, defStyle).apply {
        layoutParams = LayoutParams(0, 0, Gravity.CENTER)
        setImageResource(R.drawable.ic_launcher_background)
        borderColor = mImageBorderColor
    }
}

init {
    addView(mImage)
}

添加动画,让View转动起来

这里使用ValueAnimator .ofFloat来创建一个动画,我的传参是用的360F,看起来好像是360度,但是实际上View的旋转角度并不是靠它的回调值,而是靠回调值与mLastAnimatorVal来计算出旋转的角度,所以这里的值你想填一个亿也可以,但是要同时调整下面的计算规则,这里是有优化空间的,但是我写不动了......计算出旋转的角度差值之后将数据传输给图片和外圈的边框线进行处理。
processImageTransition 的处理非常简单, 就是给中心image设置一个角度就完了。
processBorderLines放到下面说.

ini 复制代码
/** 记录最后的动画值 */
private var mLastAnimatorVal = 0F
/** 中心圆形图片旋转一周的时间 */
private var mAnimatorDuration = 20000L
/** 动画 */
private val mAnimator : ValueAnimator =
    ValueAnimator.ofFloat(360F).apply {
        duration = mAnimatorDuration
        interpolator = null
        repeatMode = ValueAnimator.RESTART
        repeatCount = -1
        addUpdateListener {
            val currentVal = it.animatedValue as Float
            //计算出需要旋转的度数
            val diffVal = if (mLastAnimatorVal <= currentVal) currentVal - mLastAnimatorVal
                          else currentVal + 360F - mLastAnimatorVal
            mLastAnimatorVal = currentVal

            processImageTransition(diffVal)
            processBorderLines(diffVal)

            invalidate()
        }
    }
    
/**
 * 如果标志位为true,则旋转中心圆形图片
 */
private fun processImageTransition(diffVal: Float) {
    if (mAnimationFlag) {
        mImageRotation = (mImageRotation + diffVal) % 360F
        mImage.rotation = mImageRotation
    }
}

3、在转动的基础上,添加外层的边框线,边框线边旋转边扩大,直至边缘消失

4、在动画暂停时,中心封面图立即停止旋转,但外层的边框线动画继续,直至所有边框线消失,动画才是真正的停止

第3点跟第4点合并在一起写了,因为我的代码已经成了最终版,不想回过头去再写一遍半成品了。

定义一个叫BorderLine的类,用于存储边框线的参数信息。
processBorderLines 是整个View最复杂的逻辑了,首先对边框线的参数进行处理,如果发现边框线超出屏幕则利用迭代器删除该对象,别想着用for循环做这操作 [狗头] 。然后就是边框线的添加逻辑,当没有边框线且mAnimationFlag为false的时候,停止动画回调

这里其实可以考虑做一个对象池,因为BorderLine对象的生命周期很短,且创建频繁,如果加了对象池可以对内存使用进行优化。

scss 复制代码
/**
 * 处理边框线的变化
 */
private fun processBorderLines(diffVal: Float) {
    if (mImageSize == 0) return
    //迭代器处理每条边框线  对边框线进行扩大、旋转  超出屏幕的移除
    val iterator = mBorderLines.iterator()
    while (iterator.hasNext()) {
        val line = iterator.next()
        line.radius += mBorderLineGrowSheep
        line.degree = (line.degree - diffVal) % 360
        //因为是逆时针旋转  可能为负角度  负角度情况加360变为正的
        if (line.degree < 0) line.degree += 360
        //超出屏幕的移除
        if (line.radius * 2 > width) iterator.remove()
    }
    //如果没有边框线
    if (mBorderLines.isEmpty()) {
        //标志位为true  直接添加一条边框线
        if (mAnimationFlag) {
            mBorderLines.add(BorderLine(mImageSize / 2F,
                Random.nextInt(5, 15).toFloat(), Random.nextInt(360).toFloat()))
        } else {
            //真正停止动画
            mAnimator.cancel()
        }
    } else {
        //最后一条边框线与中心圆形图片的距离大于设定好的间隔,添加一条边框线
        if (mBorderLines.last().radius * 2 - mImageSize >= mBorderLineIntervalDistance) {
            if (mAnimationFlag) {
                mBorderLines.add(BorderLine(mImageSize / 2F,
                    Random.nextInt(5, 15).toFloat(), Random.nextInt(360).toFloat()))
            }
        }
    }
}

data class BorderLine(var radius : Float, val pointRadius : Float, var degree : Float)

优化:让边框线的扩大速度及产生间隔可动态配置(歌曲高潮时边框线会更加密集,快歌可适当增大边框线的扩大速度)

定义一个叫BorderLineInterval的注解,用来控制边框线创建的间隔距离。

定义一个叫BorderLineSheep的注解,用来控制边框线的扩大速度。

这两个注解的作用其实就是两个枚举,就把它当成枚举的另一种写法就好了,使用起来是类似的。

然后用它们来修饰变量,这样我们在外层设置的时候,就可以通过下面的预设值来传入,如果预设值里没有想要的,也可以直接传入自己想要的Int值,反正你喜欢怎么操作就怎么来。

less 复制代码
/** 边框线扩大速度 */
@BorderLineSheep
var mBorderLineGrowSheep = SLOW
/** 边框线间隔 */
@BorderLineInterval
var mBorderLineIntervalDistance = COMPACT

@Retention(AnnotationRetention.SOURCE)
@IntDef(COMPACT, MODERATE, SPARSE)
annotation class BorderLineInterval {
    companion object {
        const val COMPACT = 100
        const val MODERATE = 200
        const val SPARSE = 300
    }
}

@Retention(AnnotationRetention.SOURCE)
@IntDef(SLOW, NORMAL, FAST)
annotation class BorderLineSheep {
    companion object {
        const val SLOW = 1
        const val NORMAL = 2
        const val FAST = 3
    }
}

总结

10分钟过去了,这个简单的MusicAnimationView你拿下没有?

觉得不错的话,就不要吝啬你的点赞!直接一键三连!

需要整份代码的话,下面链接自提。

代码链接 : github.MyCustomView

相关推荐
J不A秃V头A33 分钟前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客1 小时前
pinia在vue3中的使用
前端·javascript·vue.js
消失的旧时光-19432 小时前
kotlin的密封类
android·开发语言·kotlin
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
服装学院的IT男3 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
天下无贼!3 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr3 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林3 小时前
npm发布插件超级简单版
前端·npm·node.js