


Realize the irresistible special effects of NetEase Cloud Music's cosmic dust
Oct 13, 2020 am 11:03 AMWeChat Mini Program Development Tutorial column today teaches you how to implement the special effects of NetEase Cloud Music universe dust, step by step.
Preface
Some time ago, my girlfriend saw a cosmic dust special effect while using NetEase Cloud Music. She said it looked very nice and wanted me to give it to her. Open for VIP use.
Joke, as a programmer, why can’t I implement it myself! What kind of VIP is open? !
What girlfriend? Are there any programmers? I only care about the realization of special effects!
It’s 2020, and most Android developers should be experienced. If you are not proficient enough in customizing View, it will not make sense. Customizing View can be said to be a point that must be mastered in Android development, whether beginner, intermediate or advanced.
Otherwise, if the UI is accidentally designed to be too cool, wouldn’t you have to fight with him? Don’t you want to be the man in the picture below?
So, I don’t need to say more about the importance of customizing View. This article is for people who have basic knowledge of custom View, but are struggling with no good project imitation, or who have seen cool effects but have no ideas and don’t know how to start. Congratulations, I will take you step by step to analyze the effect, and then implement it with code.
I know you can’t deceive people without pictures. Let’s show the picture first, and let’s take a look at the final effect.
ps: In order to load faster, the gif is compressed and compressed so that everyone can have better brain clarity.
ps2: If you have a good gif compression website, can you recommend it?
Ahem, although the picture quality is comparable to AV picture quality, it is still It can be seen that the effect is very good. So today I will take my friends to realize this effect from beginning to end.
Special effects analysis
First look at the animation, we can split it into two parts to complete, one is a circular picture that is constantly rotating inside, and the other is a constantly rotating picture outside Diffusing particle animation.
We will complete it from easy to difficult, after all, you have to pick the soft persimmon.
In addition, since the focus of this article is on customizing View, the ViewGroup method is not used to achieve the combination of pictures and particle animation. Instead, separate layouts are used. The advantage of this is that you can only focus on the implementation of particle animation without having to consider measurement, layout, etc.
As for the custom ViewGroup, in the next article I will lead you to achieve a very, very, very cool effect.
Load image
Let’s observe first, first of all, this is a circular image. Secondly, it keeps spinning.
Ahem, don’t scold me yet, let me finish.
For circular pictures, we will use Glide to implement it. In fact, custom View implementation is also possible, but our focus is on particle effects.
First define an 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>復(fù)制代碼
Now we go to the Activity and use Glide to load a circular picture.
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) } }) } } }復(fù)制代碼
In this way, we use Glide to load a circular image.
Rotate the picture
Now that the picture is there, the next step is to rotate it.
Then let’s start spinning.
How is rotation achieved? I don’t think I need to say more, many friends know that it is animation.
Yes, it’s animation. We use attribute animation to achieve this.
Define an attribute animation and set a click event for the picture to make it rotate
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() //添加點(diǎn)擊事件,并且啟動動畫 demoBinding.musicAvatar.setOnClickListener { rotateAnimator.start() } } }復(fù)制代碼
These are all pediatrics. I believe that the audience in front of the TV, ah no, a slip of the tongue Slip of the tongue.
I believe all of you are familiar with it, so let’s start with today’s highlight, this particle animation.
Particle Animation
Actually, when I watched particle animation a long time ago, I was also very curious about how these cool particle animations were achieved. At that time, I had no idea at all.
Especially when I see some pictures that suddenly turned into a bunch of particles, fell down, and then changed from particles to pictures again, I felt weird.
Actually, it’s not magical at all.
First we need to know what bitmap is. What is bitmap?
In mathematics, there are several concepts: point, line, and surface. Point is easy to understand, it is just a point. A line is made up of a bunch of points, and a surface is made up of a bunch of lines. Essentially, a surface is composed of countless points.
But what does this have to do with bitmap and today’s particle animation?
一個bitmap,我們可以簡單地理解為一張圖片。這個圖片是不是一個平面呢?而平面又是一堆點(diǎn)組成的,這個點(diǎn)在這里稱為像素點(diǎn)。所以bitmap就是由一堆像素點(diǎn)所組成的,有趣的是,這些像素點(diǎn)是有顏色的,當(dāng)這些像素點(diǎn)足夠小,你離得足夠遠(yuǎn)你看起來就像一幅完整的畫了。
在現(xiàn)實(shí)中也不乏這樣的例子,舉辦一些活動的時候,一個個人穿著不同顏色的衣服有序的站在廣場上,如果有一架無人機(jī)在空中看,就能看到是一幅畫。就像這樣
所以當(dāng)把一幅畫拆成一堆粒子的話,其實(shí)就是獲得bitmap中所有的像素點(diǎn),然后改變他們的位置就可以了。如果想要用一堆粒子拼湊出一幅畫,只需要知道這些粒子的順序,排放整齊自然就是一幅畫了。
扯遠(yuǎn)了,說這些呢其實(shí)和今天的效果沒有特別強(qiáng)的聯(lián)系,只是為了讓你能夠更好的理解粒子動畫的本質(zhì)。
粒子動畫分析
我們先觀察這個特效,你會發(fā)現(xiàn)有一個圓,這個圓上不斷的往外發(fā)散粒子,粒子在發(fā)散的過程中速度是不相同的。而且,在發(fā)散的過程中,透明度也在不斷變化,直到最后完全透明。
好,我們歸納一下。
- 圓形生產(chǎn)粒子
- 粒子速度不同,也就是隨機(jī)。
- 粒子透明度不斷降低,直到最后消散。
- 粒子沿著到圓心的反方向擴(kuò)散。
寫自定義View的時候千萬不要一上來就開干,而是要逐漸分析,有的時候我們遇到一個復(fù)雜的效果,更是要逐幀的分析。
而且我寫自定義View的時候有個習(xí)慣,就是一點(diǎn)點(diǎn)的實(shí)現(xiàn)效果,不會去一次性實(shí)現(xiàn)全部的效果。
所以我們第一步,生產(chǎn)粒子。
生產(chǎn)粒子
首先,我們可以知道,粒子是有顏色的,但是似乎這個效果粒子只有白色,那就指定粒子顏色為白色了。
然后我們可以得出,粒子是有位置的,位置肯定由x,y組成嘛。然后粒子還有個速度,以及透明度和半徑。
定義粒子
我們可以定義一個粒子類:
class Particle( var x:Float,//X坐標(biāo) var y:Float,//Y坐標(biāo) var radius:Float,//半徑 var speed:Float,//速度 var alpha: Int//透明度)復(fù)制代碼
由于我們的這個效果看起來就像是水波一樣的漣漪,我給自定義View起名為漣漪,也就是dimple
我們來定義這個自定義View把
定義自定義view
class DimpleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { //定義一個粒子的集合 private var particleList = mutableListOf<Particle>() //定義畫筆 var paint = Paint() }復(fù)制代碼
一開始就直接圓形生產(chǎn)粒子著實(shí)有些難度,我先考慮考慮如何實(shí)現(xiàn)生產(chǎn)粒子把。
先不斷生產(chǎn)粒子,然后再考慮圓形的事情。
而且生產(chǎn)一堆粒子比較麻煩,我先實(shí)現(xiàn)從上到下生產(chǎn)一個粒子。
那么如何生產(chǎn)一個粒子呢?前面也說了,粒子就是個很小的點(diǎn),所以用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) }復(fù)制代碼
畫畫嘛,就要在onDraw方法中進(jìn)行了。我們先new一個Particle,然后畫出來。
實(shí)際上這樣并沒有什么效果。為啥呢?
我們的背景是白色的,粒子默認(rèn)是白色的,你當(dāng)然看不到了。所以我們需要先做個測試,為了能看出效果。這里啊,我們把背景換成黑色。同時,為了方便測試,先把Imageview設(shè)置成不可見。然后我們看下效果
沒錯,就是沒什么效果。你什么都看不出來。
先不急,慢慢來,且聽我吹,啊不,且聽我和你慢慢道來。
我們在這里只花了一個圓,而且是在坐標(biāo)原點(diǎn)畫了一個半徑為2的點(diǎn),可以說很小很小了。自然就看不到了。
什么,你不知道原點(diǎn)在哪?
棕色部分就是我們的屏幕,所以原點(diǎn)就是左上角。
現(xiàn)在我們需要做的事情只有兩個,要么把點(diǎn)變大,要么改變點(diǎn)的位置。
粒子粒子的,當(dāng)然不能變大,所以我們把它放到屏幕中心去。
所以我們定義一個屏幕中心的坐標(biāo),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() }復(fù)制代碼
那我們改一下上面的畫點(diǎn)的代碼:
override fun onDraw(canvas: Canvas) { ... var particle=Particle(centerX,centerY,2f,2f,100) canvas.drawCircle(particle.x, particle.y, particle.radius, paint) }復(fù)制代碼
如此,可以看到這個點(diǎn)了,雖然很小很小,但是也勝過沒有呀
可是這時候有人跳出來了,說你這不對啊,一個點(diǎn)有啥用?還那么小,我本來就近視,你這搞得我更看不清了。你是不是眼睛店派來的叛徒!
添加多個粒子
那好吧,我們多加幾個??墒窃撛趺醇??效果圖中是圓形的,可是我不會啊,我只能先試試一橫排添加??纯催@樣可不可以呢?我們知道,橫排的話就是y值不變,x變。好,但是為了避免我們畫出一條線,我們x值隨機(jī)增加,這樣的話看起來也比較不規(guī)則一些。
那么代碼就應(yīng)該是這樣了
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) } }復(fù)制代碼
由于centerX是屏幕的中心,所以它的值是屏幕寬度的一半,這里的話X的值就是在屏幕寬度內(nèi)隨機(jī)選一個值。那么效果看起來是下面這樣
效果看起來不錯了。
但是總有愛搞事的小伙伴又跳出來了,說你會不會寫代碼?onDraw方法一直被調(diào)用,不能定義對象你不知道么?很容易引發(fā)頻繁的GC,造成內(nèi)存抖動的。而且你這還搞個循環(huán),性能能行不?
這個小伙伴你說的非常對,是我錯了!
確實(shí),在ondraw方法中不適合定義對象,尤其是for循環(huán)中就更不能了。段時間看,我們50個粒子好像對性能的開銷不是很大,但是一旦粒子數(shù)量很多,性能開銷就會十分的大。而且,為了不掉幀,我們需要在16ms之內(nèi)完成繪制。這個不明白的話我后續(xù)會有性能優(yōu)化的專題,可以關(guān)注一下我~
這里我們測量一下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") }復(fù)制代碼
結(jié)果如下:50個粒子的繪制時間
5000個粒子的繪制時間:
可以看到,明顯超了16ms。所以我們需要優(yōu)化,怎么優(yōu)化?很簡單,就是不在ondraw方法中創(chuàng)建對象就好了,那我們選擇在哪里呢?
構(gòu)造方法可以嗎?好像不可以呢,這個時候還沒辦法獲得屏幕寬高,嘿嘿嘿,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)) } }復(fù)制代碼
我們再來看看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") }復(fù)制代碼
emmmm,好像是低于16ms了,可是這也太危險了吧,你這分分鐘就超過了16ms啊。
確實(shí)是這樣子,但是實(shí)際情況下,我們并不需要5000個這么多的粒子。又有人問,,萬一真的需要怎么辦?那就得看surfaceView了。這里就不講了
我們還是回過頭來,先把粒子數(shù)量變成50個。
現(xiàn)在粒子也有了,該實(shí)現(xiàn)動起來的效果了。
動起來,我們想想,應(yīng)該怎么做呢?效果圖是類似圓一樣的擴(kuò)散,我現(xiàn)在做不到,我往下掉這應(yīng)該不難吧?
說動就動,搞起!至于怎么動,那肯定是屬性動畫呀。
定義動畫
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()//重繪界面 } }復(fù)制代碼
我在這里啊,定義了一個方法updateParticle,每次動畫更新的時候啊就去更新粒子的狀態(tài)。
updateParticle方法應(yīng)該去做什么事情呢?我們來開動小腦筋想想。
如果說是粒子不斷往下掉的話,那應(yīng)該是y值不斷地增加就可以了,嗯,非常有道理。
我們來實(shí)現(xiàn)一下這個方法
更新粒子位置
private fun updateParticle(value: Float) { particleList.forEach { it.y += it.speed } }override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { ... animator.start()//別忘了啟動動畫 }復(fù)制代碼
那我們現(xiàn)在來看一下效果如何
emmmm看起來有點(diǎn)雛形了,不過效果圖里的粒子速度似乎是隨機(jī)的,咱們這里是同步的呀。
沒關(guān)系,我們可以讓粒子的速度變成隨機(jī)的速度。我們修改添加粒子這里的代碼
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() }復(fù)制代碼
這是效果,看起來有點(diǎn)樣子了。不過問題又來了,人家的粒子是一直散發(fā)的,你這個粒子怎么沒了就是沒了呢?有道理,所以我覺得我們需要設(shè)置一個粒子移動的最大距離,一旦超出這個最大距離,我們啊就讓它回到初始的位置。
修改粒子的定義
class Particle( var x:Float,//X坐標(biāo) var y:Float,//Y坐標(biāo) var radius:Float,//半徑 var speed:Float,//速度 var alpha: Int, //透明度 var maxOffset:Float=300f//最大移動距離)復(fù)制代碼
如上,我們添加了一個最大移動距離。但是有時候我們往往最大移動距離都是固定的,所以我們這里給設(shè)置了一個默認(rèn)值,如果哪個粒子想特立獨(dú)行也不是不可以。
有了最大的移動距離,我們就得判定,一旦移動的距離超過了這個值,我們就讓它回到起點(diǎn)。這個判定在哪里做呢?當(dāng)然是在更新位置的地方啦
粒子運(yùn)動距離判定
private fun updateParticle(value: Float) { particleList.forEach { if(it.y - centerY >it.maxOffset){ it.y=centerY //重新設(shè)置Y值 it.x = random.nextInt((centerX * 2).toInt()).toFloat() //隨機(jī)設(shè)置X值 it.speed= (random.nextInt(10)+5).toFloat() //隨機(jī)設(shè)置速度 } it.y += it.speed } }復(fù)制代碼
本來呀,我想慢慢來,先隨機(jī)Y,在隨機(jī)X和速度。
但是我覺得可以放在一起講,因?yàn)橐粋€粒子一旦超出這個最大距離,那么它就相當(dāng)于被回收重新生成一個新的粒子了,而一個新的粒子,必然X,Y,速度都是重新生成的,這樣才能看起來效果不錯。
那我們運(yùn)行起來看看效果把。
emmm似乎還不錯的樣子?不過人家的粒子看起來很多呀,沒關(guān)系,我們這里設(shè)置成300個粒子再試試?
看起來已經(jīng)不錯了。那我們接下來該怎么辦呢?是不是還有個透明度沒搞呀。
透明度的話,我們想想該如何去設(shè)置呢?首先,應(yīng)該是越遠(yuǎn)越透明,直到最大值,完全透明。這就是了,透明度和移動距離是息息相關(guān)的。
粒子移動透明
private fun updateParticle(value: Float) { particleList.forEach { ... //設(shè)置粒子的透明度 it.alpha= ((1f - (it.y-centerY) / it.maxOffset) * 225f).toInt() ... } }override fun onDraw(canvas: Canvas) { ... var time = measureTimeMillis { particleList.forEach { //設(shè)置畫筆的透明度 paint.alpha=it.alpha canvas.drawCircle(it.x, it.y, it.radius, paint) } } ... }復(fù)制代碼
再看一下效果。。。
看起來不錯了,有點(diǎn)意思了哦~~不過好像不夠密集,我們把粒子數(shù)量調(diào)整到500就會好很多喲。而且,不知道大家有沒有發(fā)現(xiàn)在動畫剛剛加載的時候,那個效果是很不好的。因?yàn)樗械睦悠鹗键c(diǎn)是一樣的,速度也難免會有一樣的,所以效果不是很好,只需要在添加粒子的時候,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值,這里是以起始點(diǎn)作為最低值,最大距離作為最大值 nextY= random.nextInt(400)+centerY speed= random.nextInt(10)+5 particleList.add( Particle(nextX.toFloat(), nextY, 2f, speed.toFloat(), 100) ) } animator.start() }復(fù)制代碼
這樣一來,效果就會很好了,沒有一點(diǎn)問題了?,F(xiàn)在看來,似乎除了不是圓形以外,沒有什么太大的問題了。那我們下一步就該思考如何讓它變成圓形那樣生成粒子呢?
定義圓形
首先這個圓形是圓,但又不能畫出來。
什么意思?
就是說,雖然是圓形生成粒子,但是不能夠畫出來這個圓,所以這個圓只是個路徑而已。
路徑是什么?沒錯,就是Path。
熟悉的小伙伴們就知道,Path可以添加各種各樣的路徑,由圓,線,曲線等。所以我們這里就需要一個圓的路徑。
定義一個Path,添加圓。注意,我們上面講的性能優(yōu)化,不要再onDraw中定義哦。
var path = Path()override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { ... path.addCircle(centerX, centerY, 280f, Path.Direction.CCW) ... }復(fù)制代碼
在onSizeChanged中我們添加了一個圓,參數(shù)的意思我就不講了,小伙伴應(yīng)該都明白。
現(xiàn)在我們已經(jīng)定義了這個Path,但是我們又不畫,那我們該怎么辦呢?
我們思考一下,我們?nèi)绻胍獔A形生產(chǎn)粒子的話,是不是得需要這個圓上的任意一點(diǎn)的X,Y值有了這個X,Y值,我們才能夠?qū)⒘W拥某跏嘉恢媒o確定呢?看看有沒人有知道怎么確定位置啊,知道的小伙伴舉手示意一下
啊,等了十幾分鐘也沒見有小伙伴舉手,看來是沒人了。
好漢饒命!
我說,我說,其實(shí)就是PathMeasure這個類,它可以幫助我們得到在這個路徑上任意一點(diǎn)的位置和方向。不會用的小伙伴趕緊谷歌一下用法吧~或者看我代碼也很好理解的。
private val pathMeasure = PathMeasure()//路徑,用于測量擴(kuò)散圓某一處的X,Y值private var pos = FloatArray(2) //擴(kuò)散圓上某一點(diǎn)的x,yprivate val tan = FloatArray(2)//擴(kuò)散圓上某一點(diǎn)切線復(fù)制代碼
這里我們定義了三個變量,首當(dāng)其沖的就是PathMeasure類,第二個和第三個變量是一個float數(shù)組,pos是用來保存圓上某一點(diǎn)的位置信息的,其中pos[0]是X值,pos[1]是Y值。
第二個變量tan是某一點(diǎn)的切線值,你可以暫且理解為是某一點(diǎn)的角度。不過我們這個效果用不到,只是個湊參數(shù)的。
PathMeasure有個很重要的方法就是getPosTan方法。
boolean getPosTan (float distance, float[] pos, float[] tan)復(fù)制代碼
方法各個參數(shù)釋義:
參數(shù) | 作用 | 備注 |
---|---|---|
返回值(boolean) | 判斷獲取是否成功 | true表示成功,數(shù)據(jù)會存入 pos 和 tan 中, false 表示失敗,pos 和 tan 不會改變 |
distance | 距離 Path 起點(diǎn)的長度 | 取值范圍: 0 <= distance <= getLength |
pos | 該點(diǎn)的坐標(biāo)值 | 當(dāng)前點(diǎn)在畫布上的位置,有兩個數(shù)值,分別為x,y坐標(biāo)。 |
tan | 該點(diǎn)的正切值 | 當(dāng)前點(diǎn)在曲線上的方向,使用 Math.atan2(tan[1], tan[0]) 獲取到正切角的弧度值。 |
相信小伙伴還是能看明白的,我這里就不一一解釋了。
所以到了這里,我們已經(jīng)能夠獲取圓上某一點(diǎn)的位置了。還記得我們之前是怎么設(shè)置初始位置的嗎?就是Y值固定,X值隨機(jī),現(xiàn)在我們已經(jīng)能夠得到一個標(biāo)準(zhǔn)的圓的位置了。但是,很重要啊,但是如果我們按照圓的標(biāo)準(zhǔn)位置去一個個放粒子的話,豈不就是一個圓了?而我們的效果圖,位置可看起來不怎么規(guī)律。
所以我們在得到一個標(biāo)準(zhǔn)的位置之后,需要對它進(jìn)行一個隨機(jī)的偏移,偏移的也不能太大,否則成不了一個圓形。
圓形添加粒子
所以我們要修改添加粒子的代碼了。
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) { //按比例測量路徑上每一點(diǎn)的值 pathMeasure.getPosTan(i / 500f * pathMeasure.length, pos, tan) nextX = pos[0]+random.nextInt(6) - 3f //X值隨機(jī)偏移 nextY= pos[1]+random.nextInt(6) - 3f//Y值隨機(jī)偏移 speed= random.nextInt(10)+5 particleList.add( Particle(nextX, nextY, 2f, speed.toFloat(), 100) ) } animator.start() }復(fù)制代碼
現(xiàn)在運(yùn)行起來就是這樣子了
咦,效果和我想象的不一樣啊。最初好像是個圓,可是不是應(yīng)該像漣漪一樣擴(kuò)散嗎,可你這還是往下落呀。
還記得我們之前定義的動畫的效果嗎,就是X值不變,Y值不斷擴(kuò)大,那可不就是一直往下落嗎?所以這里我們需要修改動畫規(guī)則。
修改動畫
問題是怎么修改動畫呢?
思考一下,效果圖中的動畫應(yīng)該是往外擴(kuò)散,擴(kuò)散是什么意思?就是沿著它到圓心的方向反向運(yùn)動,對不對?
上一張圖來理解一下
此時內(nèi)心圓是我們現(xiàn)在粒子所處的圓,假設(shè)有一個粒子此時在B點(diǎn),那么如果要擴(kuò)散的話,它應(yīng)該到H點(diǎn)位置。
這個H點(diǎn)的位置應(yīng)該如何獲取呢?
如果以A點(diǎn)為原點(diǎn)的話,此時B點(diǎn)的位置我們是知道的,它分別是X和Y。X=AG,Y=BG。我們也應(yīng)該能發(fā)現(xiàn),由AB延申至AH的過程中,∠Z是始終不變的。
同時我們應(yīng)該能發(fā)現(xiàn),擴(kuò)散這個過程實(shí)際上是圓變大了,所以B變挪到了H點(diǎn)上。而這個擴(kuò)大的值的意思就是圓的半徑變大了,即半徑R = AB,現(xiàn)在半徑R=AH。
AB的值我們是知道的,就是我們一開始畫的圓的半徑嘛??墒茿H是多少呢?
不妨令移動距離offset=AH-AB,那么這個運(yùn)動距離offset是多少呢?我們想一下,在之前的下落中,距離是不是等于速度乘以時間呢?而我們這里沒有時間這個變量,有的只是一次次循環(huán),循環(huán)中粒子的Y值不斷加速度。所以我們需要一個變量offset值來記錄移動的距離,
所以這個offset += speed
那我們現(xiàn)在offset知道了,也就是說AH-AB的值知道了,AB我們也知道,我們就能求出AH的值
AH=AB +offset
AH知道了,∠Z也知道了,利用三角函數(shù)我們可以得到H點(diǎn)的坐標(biāo)了。設(shè)初始半徑為R=AB
A point 為 origin,BecauseAD
HD

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Xianyu's official WeChat mini program has quietly been launched. In the mini program, you can post private messages to communicate with buyers/sellers, view personal information and orders, search for items, etc. If you are curious about what the Xianyu WeChat mini program is called, take a look now. What is the name of the Xianyu WeChat applet? Answer: Xianyu, idle transactions, second-hand sales, valuations and recycling. 1. In the mini program, you can post idle messages, communicate with buyers/sellers via private messages, view personal information and orders, search for specified items, etc.; 2. On the mini program page, there are homepage, nearby, post idle, messages, and mine. 5 functions; 3. If you want to use it, you must activate WeChat payment before you can purchase it;

Implementing picture filter effects in WeChat mini programs With the popularity of social media applications, people are increasingly fond of applying filter effects to photos to enhance the artistic effect and attractiveness of the photos. Picture filter effects can also be implemented in WeChat mini programs, providing users with more interesting and creative photo editing functions. This article will introduce how to implement image filter effects in WeChat mini programs and provide specific code examples. First, we need to use the canvas component in the WeChat applet to load and edit images. The canvas component can be used on the page

To implement the drop-down menu effect in WeChat Mini Programs, specific code examples are required. With the popularity of mobile Internet, WeChat Mini Programs have become an important part of Internet development, and more and more people have begun to pay attention to and use WeChat Mini Programs. The development of WeChat mini programs is simpler and faster than traditional APP development, but it also requires mastering certain development skills. In the development of WeChat mini programs, drop-down menus are a common UI component, achieving a better user experience. This article will introduce in detail how to implement the drop-down menu effect in the WeChat applet and provide practical

The official WeChat mini program of Xianyu has been quietly launched. It provides users with a convenient platform that allows you to easily publish and trade idle items. In the mini program, you can communicate with buyers or sellers via private messages, view personal information and orders, and search for the items you want. So what exactly is Xianyu called in the WeChat mini program? This tutorial guide will introduce it to you in detail. Users who want to know, please follow this article and continue reading! What is the name of the Xianyu WeChat applet? Answer: Xianyu, idle transactions, second-hand sales, valuations and recycling. 1. In the mini program, you can post idle messages, communicate with buyers/sellers via private messages, view personal information and orders, search for specified items, etc.; 2. On the mini program page, there are homepage, nearby, post idle, messages, and mine. 5 functions; 3.

WeChat applet implements picture upload function With the development of mobile Internet, WeChat applet has become an indispensable part of people's lives. WeChat mini programs not only provide a wealth of application scenarios, but also support developer-defined functions, including image upload functions. This article will introduce how to implement the image upload function in the WeChat applet and provide specific code examples. 1. Preparatory work Before starting to write code, we need to download and install the WeChat developer tools and register as a WeChat developer. At the same time, you also need to understand WeChat

To implement the picture rotation effect in WeChat Mini Program, specific code examples are required. WeChat Mini Program is a lightweight application that provides users with rich functions and a good user experience. In mini programs, developers can use various components and APIs to achieve various effects. Among them, the picture rotation effect is a common animation effect that can add interest and visual effects to the mini program. To achieve image rotation effects in WeChat mini programs, you need to use the animation API provided by the mini program. The following is a specific code example that shows how to

Use the WeChat applet to achieve the carousel switching effect. The WeChat applet is a lightweight application that is simple and efficient to develop and use. In WeChat mini programs, it is a common requirement to achieve carousel switching effects. This article will introduce how to use the WeChat applet to achieve the carousel switching effect, and give specific code examples. First, add a carousel component to the page file of the WeChat applet. For example, you can use the <swiper> tag to achieve the switching effect of the carousel. In this component, you can pass b

Implementing the sliding delete function in WeChat mini programs requires specific code examples. With the popularity of WeChat mini programs, developers often encounter problems in implementing some common functions during the development process. Among them, the sliding delete function is a common and commonly used functional requirement. This article will introduce in detail how to implement the sliding delete function in the WeChat applet and give specific code examples. 1. Requirements analysis In the WeChat mini program, the implementation of the sliding deletion function involves the following points: List display: To display a list that can be slid and deleted, each list item needs to include
