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

相关推荐
Json_1817901448016 分钟前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
诸神黄昏EX26 分钟前
Android 分区相关介绍
android
风尚云网39 分钟前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子020442 分钟前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing44 分钟前
React核心功能详解(一)
前端·react.js·前端框架
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
深度混淆1 小时前
实用功能,觊觎(Edge)浏览器的内置截(长)图功能
前端·edge
Smartdaili China1 小时前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
秦老师Q1 小时前
「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用
前端·chrome·edge
滴水可藏海1 小时前
Chrome离线安装包下载
前端·chrome