搜尋
首頁微信小程式小程式開發實現欲罷不能的網易雲音樂宇宙塵埃特效

微信小程式開發教學專欄今天教大家實現網易雲音樂宇宙塵埃特效,手把手教。

實現欲罷不能的網易雲音樂宇宙塵埃特效

前言

前段時間,女友用網易雲音樂的時候看到一個宇宙塵埃特效,說很好看,想要讓我給她開VIP用。

笑話,為什麼身為程式設計師不能自己實現!開什麼VIP! !

什麼女朋友?程式設計師有嗎?我只在意特效的實現!

0202年了,Android開發大都應該是老油條了把。如果你自訂View還是掌握得不夠熟練的話,那可就說不過去了哦。自訂View可以說是Android開發中,無論是初級,中級或高級都必須掌握的一個點。

不然的話,UI一不小心設計的太酷,那你豈不是要跟他打起來了?難道你不想成為下圖的男人嗎?

實現欲罷不能的網易雲音樂宇宙塵埃特效

所以,自訂View的重要性已經不用我多說了。本篇是針對有自訂View基礎知識,但苦於沒有好的專案模仿,或者說看到了酷炫效果沒有思路不知道該如何下手的人。恭喜你,我將一步步手把手的帶你分析效果,然後程式碼實現它。

我就知道沒圖是騙不到人的。先放圖,大家看一下最終實現的效果。

ps:為了能更快載入出來,gif是壓縮了又壓縮,大家可以腦部清晰度。

ps2:朋友如果有好的gif壓縮網站可以推薦一波

實現欲罷不能的網易雲音樂宇宙塵埃特效

咳咳,雖然畫質堪比AV畫質,但還是看得出來的效果是非常不錯的。那麼今天我就帶小夥伴們一起從頭到尾的實現一下這個效果吧。

實現欲罷不能的網易雲音樂宇宙塵埃特效

特效分析

先看動圖,我們可以拆成兩個部分完成,一個是裡面不斷旋轉的圓形圖片,一個是外面不斷擴散的粒子動效。

我們由易到難來完成,畢竟柿子要挑軟的捏嘛。

另外由於本篇重點是講自訂View的,所以就不採用ViewGroup的方式來實現圖片和粒子動效的結合了。而是採用分開佈局的方式。這樣做的好處是可以只專注於粒子動效的實現,而不需要去考慮測量,佈局等。

至於自訂ViewGroup,下一篇文章我將會帶領大家達到一個非常非常非常酷炫的效果。

載入圖片

我們先觀察,首先這是一個圓形圖片。其次,它在不停的轉。

實現欲罷不能的網易雲音樂宇宙塵埃特效

咳咳,先別罵,容我說完嘛。

圓形圖片的話我們就用Glide來進行實現把,其實自訂View實作也可以,但我們重點還是粒子特效。

先定義一個ImageView

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <ImageView
            android:id="@+id/music_avatar"        
            android:layout_centerInParent="true"
            android:layout_width="178dp"
            android:layout_height="178dp"/></RelativeLayout>复制代码

現在我們去Activity中,用Glide載入一張圓形圖片。

class DemoActivity : AppCompatActivity() {    private lateinit var demoBinding: ActivityDemoBinding    
    
    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)
        demoBinding = ActivityDemoBinding.inflate(layoutInflater)
        setContentView(demoBinding.root)
        
        lifecycleScope.launch(Dispatchers.Main) {
           loadImage()
        }
    }    private suspend fun loadImage() {
        withContext(Dispatchers.IO) {
            Glide.with(this@DemoActivity)
                    .load(R.drawable.ic_music)
                    .circleCrop()
                    .into(object : ImageViewTarget<Drawable>(demoBinding.musicAvatar) {                        override fun setResource(resource: Drawable?) {     
                            demoBinding.musicAvatar.setImageDrawable(resource)
                        }
                    })
        }
    }
}复制代码

這樣我們利用Glide就載入了一個圓形的圖片。

旋轉圖片

圖片有了,接下來就應該是旋轉了。

那麼我們開始搞旋轉。

旋轉是如何實現的?我想不用我多說,很多小夥伴都知道,是動畫嘛。

沒錯,就是動畫。我們這裡使用屬性動畫來實作。

定義一個屬性動畫並且給圖片設定一個點擊事件,讓它旋轉起來

lateinit var rotateAnimator: ObjectAnimatoroverride fun onCreate(savedInstanceState: Bundle?) {
        ...
        setContentView(demoBinding.root)
        rotateAnimator = ObjectAnimator.ofFloat(demoBinding.musicAvatar, View.ROTATION, 0f, 360f)
        rotateAnimator.duration = 6000
        rotateAnimator.repeatCount = -1
        rotateAnimator.interpolator = LinearInterpolator()
        lifecycleScope.launch(Dispatchers.Main) {
            loadImage()            //添加点击事件,并且启动动画
            demoBinding.musicAvatar.setOnClickListener {
                rotateAnimator.start()
            }
        }
}复制代码

這些都是小兒科了,相信面對電視機前的觀眾朋友們,啊不,口誤口誤。

相信小夥伴們都很熟悉了,那我們開始今天的重頭戲,這個粒子動畫。

粒子動畫

其實我很久以前看粒子動畫的時候,也很好奇,這些酷炫的粒子動畫是怎麼實現的,當時的我完全沒有思路。

尤其是看到一些圖片,啪唧一下變成了一堆粒子,掉落,然後又呱唧從粒子變成了圖片,就覺得異常的牛X。

其實啊,一點都不神奇。

首先我們要知道bitmap是什麼。 bitmap是什麼呀?

在數學上,有這麼幾個概念,點,線,面。點很好理解,就是一個點。線是由一堆點組成的,而面又類似一堆線組成的。本質上,面就是由無數的點組成的。

可是這和bitmap以及今天的粒子動畫有什麼關係呢?

一个bitmap,我们可以简单地理解为一张图片。这个图片是不是一个平面呢?而平面又是一堆点组成的,这个点在这里称为像素点。所以bitmap就是由一堆像素点所组成的,有趣的是,这些像素点是有颜色的,当这些像素点足够小,你离得足够远你看起来就像一幅完整的画了。

在现实中也不乏这样的例子,举办一些活动的时候,一个个人穿着不同颜色的衣服有序的站在广场上,如果有一架无人机在空中看,就能看到是一幅画。就像这样

實現欲罷不能的網易雲音樂宇宙塵埃特效

所以当把一幅画拆成一堆粒子的话,其实就是获得bitmap中所有的像素点,然后改变他们的位置就可以了。如果想要用一堆粒子拼凑出一幅画,只需要知道这些粒子的顺序,排放整齐自然就是一幅画了。

扯远了,说这些呢其实和今天的效果没有特别强的联系,只是为了让你能够更好的理解粒子动画的本质。

實現欲罷不能的網易雲音樂宇宙塵埃特效

粒子动画分析

我们先观察这个特效,你会发现有一个圆,这个圆上不断的往外发散粒子,粒子在发散的过程中速度是不相同的。而且,在发散的过程中,透明度也在不断变化,直到最后完全透明。

好,我们归纳一下。

  • 圆形生产粒子
  • 粒子速度不同,也就是随机。
  • 粒子透明度不断降低,直到最后消散。
  • 粒子沿着到圆心的反方向扩散。

写自定义View的时候千万不要一上来就开干,而是要逐渐分析,有的时候我们遇到一个复杂的效果,更是要逐帧的分析。

而且我写自定义View的时候有个习惯,就是一点点的实现效果,不会去一次性实现全部的效果。

所以我们第一步,生产粒子。

生产粒子

首先,我们可以知道,粒子是有颜色的,但是似乎这个效果粒子只有白色,那就指定粒子颜色为白色了。

然后我们可以得出,粒子是有位置的,位置肯定由x,y组成嘛。然后粒子还有个速度,以及透明度和半径。

定义粒子

我们可以定义一个粒子类:

class Particle(    var x:Float,//X坐标
    var y:Float,//Y坐标
    var radius:Float,//半径
    var speed:Float,//速度
    var alpha: Int//透明度)复制代码

由于我们的这个效果看起来就像是水波一样的涟漪,我给自定义View起名为涟漪,也就是dimple

我们来定义这个自定义View把

定义自定义view

class DimpleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {    //定义一个粒子的集合  
    private var particleList = mutableListOf<Particle>()    //定义画笔
    var paint = Paint()
}复制代码

一开始就直接圆形生产粒子着实有些难度,我先考虑考虑如何实现生产粒子把。

先不断生产粒子,然后再考虑圆形的事情。

而且生产一堆粒子比较麻烦,我先实现从上到下生产一个粒子。

那么如何生产一个粒子呢?前面也说了,粒子就是个很小的点,所以用canvas的drawCircle就可以。

那我们来吧

override fun onDraw(canvas: Canvas) {        super.onDraw(canvas)
        paint.color = Color.WHITE
        paint.isAntiAlias = true
        var particle=Particle(0f,0f,2f,2f,100)
        canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
}复制代码

画画嘛,就要在onDraw方法中进行了。我们先new一个Particle,然后画出来。

实际上这样并没有什么效果。为啥呢?

我们的背景是白色的,粒子默认是白色的,你当然看不到了。所以我们需要先做个测试,为了能看出效果。这里啊,我们把背景换成黑色。同时,为了方便测试,先把Imageview设置成不可见。然后我们看下效果

實現欲罷不能的網易雲音樂宇宙塵埃特效

没错,就是没什么效果。你什么都看不出来。

實現欲罷不能的網易雲音樂宇宙塵埃特效

先不急,慢慢来,且听我吹,啊不,且听我和你慢慢道来。

我们在这里只花了一个圆,而且是在坐标原点画了一个半径为2的点,可以说很小很小了。自然就看不到了。

什么,你不知道原点在哪?

實現欲罷不能的網易雲音樂宇宙塵埃特效

棕色部分就是我们的屏幕,所以原点就是左上角。

现在我们需要做的事情只有两个,要么把点变大,要么改变点的位置。

粒子粒子的,当然不能变大,所以我们把它放到屏幕中心去。

所以我们定义一个屏幕中心的坐标,centerX,centerY。并且在onSizeChanged方法中给它们赋值

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {        super.onSizeChanged(w, h, oldw, oldh)
        centerX= (w/2).toFloat()
        centerY= (h/2).toFloat()
}复制代码

那我们改一下上面的画点的代码:

override fun onDraw(canvas: Canvas) {
        ...        var particle=Particle(centerX,centerY,2f,2f,100)
        canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
}复制代码

如此,可以看到这个点了,虽然很小很小,但是也胜过没有呀

實現欲罷不能的網易雲音樂宇宙塵埃特效

可是这时候有人跳出来了,说你这不对啊,一个点有啥用?还那么小,我本来就近视,你这搞得我更看不清了。你是不是眼睛店派来的叛徒!

添加多个粒子

那好吧,我们多加几个。可是该怎么加?效果图中是圆形的,可是我不会啊,我只能先试试一横排添加。看看这样可不可以呢?我们知道,横排的话就是y值不变,x变。好,但是为了避免我们画出一条线,我们x值随机增加,这样的话看起来也比较不规则一些。

那么代码就应该是这样了

override fun onDraw(canvas: Canvas) {        super.onDraw(canvas)
        paint.color = Color.WHITE
        paint.isAntiAlias = true
        for (i in 0..50){            var random= Random()            var nextX=random.nextInt((centerX*2).toInt())            var particle=Particle(nextX.toFloat(),centerY,2f,2f,100)
            canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
        }

}复制代码

由于centerX是屏幕的中心,所以它的值是屏幕宽度的一半,这里的话X的值就是在屏幕宽度内随机选一个值。那么效果看起来是下面这样

實現欲罷不能的網易雲音樂宇宙塵埃特效

效果看起来不错了。

但是总有爱搞事的小伙伴又跳出来了,说你会不会写代码?onDraw方法一直被调用,不能定义对象你不知道么?很容易引发频繁的GC,造成内存抖动的。而且你这还搞个循环,性能能行不?

这个小伙伴你说的非常对,是我错了!

确实,在ondraw方法中不适合定义对象,尤其是for循环中就更不能了。段时间看,我们50个粒子好像对性能的开销不是很大,但是一旦粒子数量很多,性能开销就会十分的大。而且,为了不掉帧,我们需要在16ms之内完成绘制。这个不明白的话我后续会有性能优化的专题,可以关注一下我~

这里我们测量一下50个粒子的绘制时间和5000个粒子的绘制时间。

override fun onDraw(canvas: Canvas) {        super.onDraw(canvas)
        paint.color = Color.WHITE
        paint.isAntiAlias = true
        var time= measureTimeMillis {            for (i in 0..50){                var random= Random()                var nextX=random.nextInt((centerX*2).toInt())                var particle=Particle(nextX.toFloat(),centerY,2f,2f,100)
                canvas.drawCircle(particle.x, particle.y, particle.radius, paint)
            }
        }
        Log.i("dimple","绘制时间$time ms")
}复制代码

结果如下:50个粒子的绘制时间

實現欲罷不能的網易雲音樂宇宙塵埃特效

5000个粒子的绘制时间:

實現欲罷不能的網易雲音樂宇宙塵埃特效

可以看到,明显超了16ms。所以我们需要优化,怎么优化?很简单,就是不在ondraw方法中创建对象就好了,那我们选择在哪里呢?

构造方法可以吗?好像不可以呢,这个时候还没办法获得屏幕宽高,嘿嘿嘿,onSizeChanged方法就决定是你了!

粒子添加到集合中
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {        super.onSizeChanged(w, h, oldw, oldh)
        centerX= (w/2).toFloat()
        centerY= (h/2).toFloat()        val random= Random()        var nextX=0
        for (i in 0..5000){
            nextX=random.nextInt((centerX*2).toInt())
            particleList.add(Particle(nextX.toFloat(),centerY,2f,2f,100))
        }
}复制代码

我们再来看看onDraw方法中绘制时间是多少:

override fun onDraw(canvas: Canvas) {    super.onDraw(canvas)
    paint.color = Color.WHITE
    paint.isAntiAlias = true
    var time= measureTimeMillis {
        particleList.forEach {
            canvas.drawCircle(it.x,it.y,it.radius,paint)
        }
    }
    Log.i("dimple","绘制时间$time ms")
}复制代码

實現欲罷不能的網易雲音樂宇宙塵埃特效

emmmm,好像是低于16ms了,可是这也太危险了吧,你这分分钟就超过了16ms啊。

确实是这样子,但是实际情况下,我们并不需要5000个这么多的粒子。又有人问,,万一真的需要怎么办?那就得看surfaceView了。这里就不讲了

我们还是回过头来,先把粒子数量变成50个。

现在粒子也有了,该实现动起来的效果了。

动起来,我们想想,应该怎么做呢?效果图是类似圆一样的扩散,我现在做不到,我往下掉这应该不难吧?

说动就动,搞起!至于怎么动,那肯定是属性动画呀。

定义动画

private var animator = ValueAnimator.ofFloat(0f, 1f)init {
        animator.duration = 2000
        animator.repeatCount = -1
        animator.interpolator = LinearInterpolator()
        animator.addUpdateListener {
            updateParticle(it.animatedValue as Float)
            invalidate()//重绘界面
        }
}复制代码

我在这里啊,定义了一个方法updateParticle,每次动画更新的时候啊就去更新粒子的状态。

updateParticle方法应该去做什么事情呢?我们来开动小脑筋想想。

如果说是粒子不断往下掉的话,那应该是y值不断地增加就可以了,嗯,非常有道理。

我们来实现一下这个方法

更新粒子位置

private fun updateParticle(value: Float) {
    particleList.forEach {
        it.y += it.speed
    }
}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        ...
        animator.start()//别忘了启动动画
    }复制代码

那我们现在来看一下效果如何

實現欲罷不能的網易雲音樂宇宙塵埃特效

emmmm看起来有点雏形了,不过效果图里的粒子速度似乎是随机的,咱们这里是同步的呀。

没关系,我们可以让粒子的速度变成随机的速度。我们修改添加粒子这里的代码

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {        super.onSizeChanged(w, h, oldw, oldh)
        centerX = (w / 2).toFloat()
        centerY = (h / 2).toFloat()        val random = Random()        var nextX = 0
        var speed=0 //定义一个速度
        for (i in 0..50) {
            nextX = random.nextInt((centerX * 2).toInt())
            speed= random.nextInt(10)+5 //速度从5-15不等
            particleList.add(
                Particle(nextX.toFloat(), centerY, 2f, speed.toFloat(), 100)
            )
        }
        animator.start()
    }复制代码

这是效果,看起来有点样子了。不过问题又来了,人家的粒子是一直散发的,你这个粒子怎么没了就是没了呢?實現欲罷不能的網易雲音樂宇宙塵埃特效有道理,所以我觉得我们需要设置一个粒子移动的最大距离,一旦超出这个最大距离,我们啊就让它回到初始的位置。

修改粒子的定义

class Particle(    var x:Float,//X坐标
    var y:Float,//Y坐标
    var radius:Float,//半径
    var speed:Float,//速度
    var alpha: Int, //透明度
    var maxOffset:Float=300f//最大移动距离)复制代码

如上,我们添加了一个最大移动距离。但是有时候我们往往最大移动距离都是固定的,所以我们这里给设置了一个默认值,如果哪个粒子想特立独行也不是不可以。

有了最大的移动距离,我们就得判定,一旦移动的距离超过了这个值,我们就让它回到起点。这个判定在哪里做呢?当然是在更新位置的地方啦

粒子运动距离判定

private fun updateParticle(value: Float) {
        particleList.forEach {            if(it.y - centerY >it.maxOffset){
                it.y=centerY //重新设置Y值
                it.x = random.nextInt((centerX * 2).toInt()).toFloat() //随机设置X值
                it.speed= (random.nextInt(10)+5).toFloat() //随机设置速度
            }
            it.y += it.speed
        }
}复制代码

本来呀,我想慢慢来,先随机Y,在随机X和速度。

但是我觉得可以放在一起讲,因为一个粒子一旦超出这个最大距离,那么它就相当于被回收重新生成一个新的粒子了,而一个新的粒子,必然X,Y,速度都是重新生成的,这样才能看起来效果不错。

那我们运行起来看看效果把。實現欲罷不能的網易雲音樂宇宙塵埃特效

emmm似乎还不错的样子?不过人家的粒子看起来很多呀,没关系,我们这里设置成300个粒子再试试?

實現欲罷不能的網易雲音樂宇宙塵埃特效

看起来已经不错了。那我们接下来该怎么办呢?是不是还有个透明度没搞呀。

透明度的话,我们想想该如何去设置呢?首先,应该是越远越透明,直到最大值,完全透明。这就是了,透明度和移动距离是息息相关的。

粒子移动透明

private fun updateParticle(value: Float) {
        particleList.forEach {
            ...            //设置粒子的透明度
            it.alpha= ((1f - (it.y-centerY) / it.maxOffset)  * 225f).toInt()
            ...
        }
}override fun onDraw(canvas: Canvas) {
        ...        var time = measureTimeMillis {
            particleList.forEach {                //设置画笔的透明度
                paint.alpha=it.alpha
                canvas.drawCircle(it.x, it.y, it.radius, paint)
            }
        }
        ...
}复制代码

再看一下效果。。。

實現欲罷不能的網易雲音樂宇宙塵埃特效

看起来不错了,有点意思了哦~~不过好像不够密集,我们把粒子数量调整到500就会好很多哟。而且,不知道大家有没有发现在动画刚刚加载的时候,那个效果是很不好的。因为所有的例子起始点是一样的,速度也难免会有一样的,所以效果不是很好,只需要在添加粒子的时候,Y值也初始化即可。

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {    super.onSizeChanged(w, h, oldw, oldh)
    ...    var nextY=0f
    for (i in 0..500) {
        ...        //初始化Y值,这里是以起始点作为最低值,最大距离作为最大值
        nextY= random.nextInt(400)+centerY
        speed= random.nextInt(10)+5
        particleList.add(
            Particle(nextX.toFloat(), nextY, 2f, speed.toFloat(), 100)
        )
    }
    animator.start()
}复制代码

这样一来,效果就会很好了,没有一点问题了。现在看来,似乎除了不是圆形以外,没有什么太大的问题了。那我们下一步就该思考如何让它变成圆形那样生成粒子呢?

定义圆形

首先这个圆形是圆,但又不能画出来。

什么意思?

就是说,虽然是圆形生成粒子,但是不能够画出来这个圆,所以这个圆只是个路径而已。

路径是什么?没错,就是Path。

熟悉的小伙伴们就知道,Path可以添加各种各样的路径,由圆,线,曲线等。所以我们这里就需要一个圆的路径。

定义一个Path,添加圆。注意,我们上面讲的性能优化,不要再onDraw中定义哦。

var path = Path()override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    ...
    path.addCircle(centerX, centerY, 280f, Path.Direction.CCW)
    ...
}复制代码

在onSizeChanged中我们添加了一个圆,参数的意思我就不讲了,小伙伴应该都明白。

现在我们已经定义了这个Path,但是我们又不画,那我们该怎么办呢?

我们思考一下,我们如果想要圆形生产粒子的话,是不是得需要这个圆上的任意一点的X,Y值有了这个X,Y值,我们才能够将粒子的初始位置给确定呢?看看有没人有知道怎么确定位置啊,知道的小伙伴举手示意一下

啊,等了十几分钟也没见有小伙伴举手,看来是没人了。

實現欲罷不能的網易雲音樂宇宙塵埃特效

好汉饶命!

我说,我说,其实就是PathMeasure这个类,它可以帮助我们得到在这个路径上任意一点的位置和方向。不会用的小伙伴赶紧谷歌一下用法吧~或者看我代码也很好理解的。

private val pathMeasure = PathMeasure()//路径,用于测量扩散圆某一处的X,Y值private var pos = FloatArray(2) //扩散圆上某一点的x,yprivate val tan = FloatArray(2)//扩散圆上某一点切线复制代码

这里我们定义了三个变量,首当其冲的就是PathMeasure类,第二个和第三个变量是一个float数组,pos是用来保存圆上某一点的位置信息的,其中pos[0]是X值,pos[1]是Y值。

第二个变量tan是某一点的切线值,你可以暂且理解为是某一点的角度。不过我们这个效果用不到,只是个凑参数的。

PathMeasure有个很重要的方法就是getPosTan方法。

boolean getPosTan (float distance, float[] pos, float[] tan)复制代码

方法各个参数释义:

参数 作用 备注
返回值(boolean) 判断获取是否成功 true表示成功,数据会存入 pos 和 tan 中, false 表示失败,pos 和 tan 不会改变
distance 距离 Path 起点的长度 取值范围: 0
pos 该点的坐标值 当前点在画布上的位置,有两个数值,分别为x,y坐标。
tan 该点的正切值 当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。

相信小伙伴还是能看明白的,我这里就不一一解释了。

所以到了这里,我们已经能够获取圆上某一点的位置了。还记得我们之前是怎么设置初始位置的吗?就是Y值固定,X值随机,现在我们已经能够得到一个标准的圆的位置了。但是,很重要啊,但是如果我们按照圆的标准位置去一个个放粒子的话,岂不就是一个圆了?而我们的效果图,位置可看起来不怎么规律。

所以我们在得到一个标准的位置之后,需要对它进行一个随机的偏移,偏移的也不能太大,否则成不了一个圆形。

圆形添加粒子

所以我们要修改添加粒子的代码了。

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {        super.onSizeChanged(w, h, oldw, oldh)
        centerX = (w / 2).toFloat()
        centerY = (h / 2).toFloat()
        path.addCircle(centerX, centerY, 280f, Path.Direction.CCW)
        pathMeasure.setPath(path, false) //添加path
        var nextX = 0f
        var speed=0
        var nextY=0f
        for (i in 0..500) {            //按比例测量路径上每一点的值
            pathMeasure.getPosTan(i / 500f * pathMeasure.length, pos, tan)
            nextX = pos[0]+random.nextInt(6) - 3f //X值随机偏移
            nextY=  pos[1]+random.nextInt(6) - 3f//Y值随机偏移
            speed= random.nextInt(10)+5
            particleList.add(
                Particle(nextX, nextY, 2f, speed.toFloat(), 100)
            )
        }
        animator.start()
    }复制代码

现在运行起来就是这样子了

實現欲罷不能的網易雲音樂宇宙塵埃特效

咦,效果和我想象的不一样啊。最初好像是个圆,可是不是应该像涟漪一样扩散吗,可你这还是往下落呀。

还记得我们之前定义的动画的效果吗,就是X值不变,Y值不断扩大,那可不就是一直往下落吗?所以这里我们需要修改动画规则。

修改动画

问题是怎么修改动画呢?

思考一下,效果图中的动画应该是往外扩散,扩散是什么意思?就是沿着它到圆心的方向反向运动,对不对?

上一张图来理解一下

實現欲罷不能的網易雲音樂宇宙塵埃特效

此时内心圆是我们现在粒子所处的圆,假设有一个粒子此时在B点,那么如果要扩散的话,它应该到H点位置。

这个H点的位置应该如何获取呢?

如果以A点为原点的话,此时B点的位置我们是知道的,它分别是X和Y。X=AG,Y=BG。我们也应该能发现,由AB延申至AH的过程中,∠Z是始终不变的。

同时我们应该能发现,扩散这个过程实际上是圆变大了,所以B变挪到了H点上。而这个扩大的值的意思就是圆的半径变大了,即半径R = AB,现在半径R=AH。

AB的值我们是知道的,就是我们一开始画的圆的半径嘛。可是AH是多少呢?

不妨令移动距离offset=AH-AB,那么这个运动距离offset是多少呢?我们想一下,在之前的下落中,距离是不是等于速度乘以时间呢?而我们这里没有时间这个变量,有的只是一次次循环,循环中粒子的Y值不断加速度。所以我们需要一个变量offset值来记录移动的距离,

所以这个offset  += speed

那我们现在offset知道了,也就是说AH-AB的值知道了,AB我们也知道,我们就能求出AH的值

AH=AB +offset

AH知道了,∠Z也知道了,利用三角函数我们可以得到H点的坐标了。设初始半径为R=AB

##A點為原點,

co##s(Z)=A##G/ ##AB, s#in##( Z)=B##G/ A##B#cos(∠Z)=AG/AB、sin(∠Z)=BG/AB #co

A#D##=##AH *cos#(#∠Z)=(A#H##*AG)/AB=((R offs##et )*AG)/R AD = AH * cos(∠Z) = (AH * AG)/AB=( (R 偏移) * AG) / R ##AAG

)

###/# #### #R####################HD#####

H#D##=##A Hsin(Z )=(A#HB G)/AB=##(( R of#fset)BG)/#RHD = AH * sin(∠Z) = (AH*BG)/AB = ( (R offset) * BG) / R 按理說沒問題了,這個時候H的值我們已經得到了。但是,注意此時我們是以A點為原點得出來的值,而我們的手機螢幕中是以左上角為原點的。 A點的值我們此時在程式中寫死了是centerX和centerY,所以上面的公式還要改一下

A (R offset)*(B.##Xcent#er##X))/R##AD = ((R 偏移) * (B.X - centerX)) / R ##A#D

H#D##=##((R offs#e t)(ce##n terYB.#Y))/R#HD = ( (R offset) * (centerY-B.Y) ) / R注意哦,此時只是AD和HD的值,只是這兩個線段的長度而不是真正H點的座標。 H點的座標應該在A點的基礎上增加,即

H.X##=AD centerX=((R offse#t)##*(B.Xcen terX))##/ R cen#e##r# #X#H.X=AD centerX=( (R 偏移) * (B.X - centerX)) / R centerX ##H

H.##Y=center##Y##HD=c#en##t erY((R## ####抵消######)##### #terYB.Y))/R#H.Y = centerY - HD = centerY -( (R 偏移) * (centerY-B.Y) ) / R H. Y=#c##e nter

而且這只是在右上半區也就是第一象限是這樣計算的,左半區和右下半區的計算規則也不一樣。

實現欲罷不能的網易雲音樂宇宙塵埃特效

兄台,不要急,先聽我說,一會兒還不明白的話我親自幫你踢板凳。

小夥伴紛紛表示,上邊的公式也太複雜了,每個象限都計算一遍,自訂View就這麼複雜嗎?

哈哈哈哈,其實不是。我晃點你的。

規則也確實是上面所說的,但是我們是什麼人?程式設計師啊,最不該怕的就是計算了。反正CPU算不是我算。

我們在分析一遍,這次一定很簡單,你不要跑!

首先有個角度Z,我們需要記下每個粒子的角度,可是這個角度的計算就有的說道了。

我們先以左右兩個半區計算,在右半區的時候角度

# Z=(B.X#c.X#c enterX##)/ RR

假設這個時候∠Z是30°,那麼也就是說 cosZ=0.5cos∠Z = 0.5

###################################### ################0######.#######5########################################### ##那麼H的X值也就是###

H.X##=AH0.5cen terX#H.X= AH * 0.5 centerX

#c

e

nter X可是如果在左半區的話角度Zco#=

###########−######0######.#######5############### ######那麼此時H的X值應該是這麼算###

H.X##=centerX AH0.5#H.X= centerX - AH * 0.50

##.

5

#其實本質上H.X=cen terX AH##∗cos#Z

Z

####我們根本不需要考慮左右的問題,因為如果在右邊cos∠Z是正,在左邊為負數,所以直接加就可以。 ######而我們需要考慮的是上下問題,也就是Y的問題。畢竟這個正負是基於X的值算出來的。當我們轉換成角度以後需要根據此時H的Y值是否大於centerY來分別計算。 ######當H的Y值在centerY之上,也就是H.YH.##Y=center##Y##AH*a#bs##( sinZ)##H.Y = centerY - AH * abs( sin∠Z)s(

s

##i###### n# ####∠######Z######)######################反之###

H.Y=centerY+AHabs(sinZ)H.Y = centerY + AH * abs( sin∠Z)

这样的话随着offset的增长,H点的坐标也能够随时的计算出来了。

话说再多也没用,还是代码更为直观。

根据上面的描述,我们需要给粒子添加两个属性,一个是移动距离,一个是粒子的角度

class Particle(
    ...    var offset:Int,//当前移动距离
    var angle:Double,//粒子角度
    ...
)复制代码

在添加粒子的地方修改:

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    ...    var angle=0.0
    for (i in 0..500) {
        ...        //反余弦函数可以得到角度值,是弧度
        angle=acos(((pos[0] - centerX) / 280f).toDouble())
        speed= random.nextInt(10)+5
        particleList.add(
            Particle(nextX, nextY, 2f, speed.toFloat(), 100,0,angle)
        )
    }
    animator.start()
}复制代码

在更新粒子动画的地方修改:

private fun updateParticle(value: Float) {
    particleList.forEach {particle->        if(particle.offset >particle.maxOffset){
            particle.offset=0
            particle.speed= (random.nextInt(10)+5).toFloat()
        }
        particle.alpha= ((1f - particle.offset / particle.maxOffset)  * 225f).toInt()
        particle.x = (centerX+ cos(particle.angle) * (280f + particle.offset)).toFloat()        if (particle.y > centerY) {
            particle.y = (sin(particle.angle) * (280f + particle.offset) + centerY).toFloat()
        } else {
            particle.y = (centerY - sin(particle.angle) * (280f + particle.offset)).toFloat()
        }
        particle.offset += particle.speed.toInt()
    }
}复制代码

添加粒子的地方angle是角度值,用了Kotlin的acos反余弦函数,这个函数返回的是0-PI的弧度制,0-PI的取值范围也就意味着sin∠Z始终是正值,上面公式中的绝对值就不需要了。

更新的代码很简单,对照公式一看便知。此时我们运行一下,效果就已经很好了,很接近了。

實現欲罷不能的網易雲音樂宇宙塵埃特效

现在感觉是不是好多了?就是速度有点快,粒子有点少~没关系,我们做一些优化工作,比如说,粒子的初始移动距离也随机取一个值,粒子的最大距离也随机。这样下来,我们的效果就十分的好了

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {    super.onSizeChanged(w, h, oldw, oldh)
    centerX = (w / 2).toFloat()
    centerY = (h / 2).toFloat()
    path.addCircle(centerX, centerY, 280f, Path.Direction.CCW)
    pathMeasure.setPath(path, false)    var nextX = 0f
    var speed=0f
    var nextY=0f
    var angle=0.0
    var offSet=0
    var maxOffset=0
    for (i in 0..2000) {
        pathMeasure.getPosTan(i / 2000f * pathMeasure.length, pos, tan)
        nextX = pos[0]+random.nextInt(6) - 3f
        nextY=  pos[1]+random.nextInt(6) - 3f
        angle=acos(((pos[0] - centerX) / 280f).toDouble())
        speed= random.nextInt(2) + 2f
        offSet = random.nextInt(200)
        maxOffset = random.nextInt(200)
        particleList.add(
            Particle(nextX, nextY, 2f, speed, 100,offSet.toFloat(),angle, maxOffset.toFloat())
        )
    }
    animator.start()
}复制代码

其实还有很多可以优化的地方,比如说,粒子的数量抽取为一个常量,中间圆的半径也可以定为一个属性值去手动设置等等。。不过这些都是小意思,相信小伙伴们一定可以自己搞定的。我就不班门弄斧了。

最后这是优化过后的效果,接下来的与图片结合,就希望小伙伴们自己实现一下啦~很简单的。可以在评论区交作业哦~

實現欲罷不能的網易雲音樂宇宙塵埃特效

这是我的成品地址,做了一些额外的优化,比如说x和y的随机偏移等。小伙伴们可以手动去实现一下或者看我代码,当然我代码只是为了实现效果,并没有做更多的优化,不要喷我哦:DimpleView

预告

这一次是自定义View。 下一次我将要带大家实现自定义ViewGroup,效果是这样的,是不是更加炫酷?欢迎关注我,一个喜欢自定义View和NDK开发的小喵喵~

實現欲罷不能的網易雲音樂宇宙塵埃特效

相關免費學習推薦:#微信小程式開發教學

以上是實現欲罷不能的網易雲音樂宇宙塵埃特效的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:juejin。如有侵權,請聯絡admin@php.cn刪除
微信小程序架构原理基础详解微信小程序架构原理基础详解Oct 11, 2022 pm 02:13 PM

本篇文章给大家带来了关于微信小程序的相关问题,其中主要介绍了关于基础架构原理的相关内容,其中包括了宿主环境、执行环境、小程序整体架构、运行机制、更新机制、数据通信机制等等内容,下面一起来看一下,希望对大家有帮助。

微信小程序常用API(总结分享)微信小程序常用API(总结分享)Dec 01, 2022 pm 04:08 PM

本篇文章给大家带来了关于微信小程序的相关知识,其中主要总结了一些常用的API,下面一起来看一下,希望对大家有帮助。

微信小程序云服务配置详解微信小程序云服务配置详解May 27, 2022 am 11:53 AM

本篇文章给大家带来了关于微信小程序的相关知识,其中主要介绍了关于云服务的配置详解,包括了创建使用云开发项目、搭建云环境、测试云服务等等内容,下面一起来看一下,希望对大家有帮助。

浅析微信小程序中自定义组件的方法浅析微信小程序中自定义组件的方法Mar 25, 2022 am 11:33 AM

微信小程序中怎么自定义组件?下面本篇文章给大家介绍一下微信小程序中自定义组件的方法,希望对大家有所帮助!

微信小程序实战项目之富文本编辑器实现微信小程序实战项目之富文本编辑器实现Oct 08, 2022 pm 05:51 PM

本篇文章给大家带来了关于微信小程序的相关知识,其中主要介绍了关于富文本编辑器的实战示例,包括了创建发布页面、实现基本布局、实现编辑区操作栏的功能等内容,下面一起来看一下,希望对大家有帮助。

西安坐地铁用什么小程序西安坐地铁用什么小程序Nov 17, 2022 am 11:37 AM

西安坐地铁用的小程序为“乘车码”。使用方法:1、打开手机微信客户端,点击“发现”中的“小程序”;2、在搜索栏中输入“乘车码”进行搜索;3、直接定位城市西安,或者搜索西安,点击“西安地铁乘车码”选项的“去乘车”按钮;4、根据腾讯官方提示进行授权,开通“乘车码”业务即可利用该小程序提供的二维码来支付乘车了。

简单介绍:实现小程序授权登录功能简单介绍:实现小程序授权登录功能Nov 07, 2022 pm 05:32 PM

本篇文章给大家带来了关于微信小程序的相关知识,其中主要介绍了怎么实现小程序授权登录功能的相关内容,下面一起来看一下,希望对大家有帮助。

微信小程序开发工具介绍微信小程序开发工具介绍Oct 08, 2022 pm 04:47 PM

本篇文章给大家带来了关于微信小程序的相关问题,其中主要介绍了关于开发工具介绍的相关内容,包括了下载开发工具以及编辑器总结等内容,下面一起来看一下,希望对大家有帮助。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具