波多野结衣 蜜桃视频,国产在线精品露脸ponn,a v麻豆成人,AV在线免费小电影

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

前言

前段時間,女朋友用網易云音樂的時候看到一個宇宙塵埃特效,說很好看,想要讓我給她開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: ObjectAnimator
override 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()//別忘了啟動動畫
    }

那我們現在來看一下效果如何


5f852d94a78b8.gif


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()
    }

這是效果,看起來有點樣子了。不過問題又來了,人家的粒子是一直散發的,你這個粒子怎么沒了就是沒了呢?


5f852de58ceed.gif


有道理,所以我覺得我們需要設置一個粒子移動的最大距離,一旦超出這個最大距離,我們啊就讓它回到初始的位置。

修改粒子的定義

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,速度都是重新生成的,這樣才能看起來效果不錯。

那我們運行起來看看效果把。


5f852e6d01348.gif


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,y
private val tan = FloatArray(2)//擴散圓上某一點切線

這里我們定義了三個變量,首當其沖的就是PathMeasure類,第二個和第三個變量是一個float數組,pos是用來保存圓上某一點的位置信息的,其中pos[0]是X值,pos[1]是Y值。

第二個變量tan是某一點的切線值,你可以暫且理解為是某一點的角度。不過我們這個效果用不到,只是個湊參數的。

PathMeasure有個很重要的方法就是getPosTan方法。
boolean getPosTan (float distance, float[] pos, float[] tan)

方法各個參數釋義:


帶你實現女朋友欲罷不能的網易云音樂宇宙塵埃特效


相信小伙伴還是能看明白的,我這里就不一一解釋了。

所以到了這里,我們已經能夠獲取圓上某一點的位置了。還記得我們之前是怎么設置初始位置的嗎?就是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點為原點,

cos(∠Z)=AG/AB,sin(∠Z)=BG/AB

所以AD

AD=AH?cos(∠Z)=(AH?AG)/AB=((R+offset)?AG)/R

HD

HD=AH?sin(∠Z)=(AH?BG)/AB=((R+offset)?BG)/R

按理說沒問題了,這個時候H的值我們已經得到了。但是,注意此時我們是以A點為原點得出來的值,而我們的手機屏幕中是以左上角為原點的。A點的值我們此時在程序中寫死了是centerX和centerY,所以上面的公式還得改一下

AD=((R+offset)?(B.X?centerX))/R

HD=((R+offset)?(centerY?B.Y))/R

注意哦,此時只是AD和HD的值,只是這兩個線段的長度而不是真正H點的坐標。H點的坐標應該在A點的基礎上增加,即

H.X=AD+centerX=((R+offset)?(B.X?centerX))/R+centerX

H.Y=centerY?HD=centerY?((R+offset)?(centerY?B.Y))/R

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

帶你實現女朋友欲罷不能的網易云音樂宇宙塵埃特效


兄臺,不要急,先聽我說,一會還不懂的話我親自幫你踢板凳。

小伙伴紛紛表示,上邊的公式也太復雜了,每個象限都計算一遍,自定義View就這么復雜嗎?

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

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

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

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

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

Z=(B.X?centerX)/R

假設這個時候∠Z是30°,那么也就是說

cos∠Z=0.5

那么H的X值也就是

H.X=AH?0.5+centerX

可是如果在左半區的話角度Z

cos∠Z=?0.5

那么此時H的X值應該是這么算

H.X=centerX?AH?0.5

其實本質上

H.X=centerX+AH?cos∠Z

我們根本不需要考慮左右的問題,因為如果在右邊cos∠Z是正,在左邊為負數,所以直接加就可以。

而我們需要考慮的是上下問題,也就是Y的問題。畢竟這個正負是基于X的值算出來的。當我們轉換成角度以后需要根據此時H的Y值是否大于centerY來分別計算。

當H的Y值在centerY之上,也就是H.Y<centerY

H.Y=centerY?AH?abs(sin∠Z)

反之

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()
}

其實還有很多可以優化的地方,比如說,粒子的數量抽取為一個常量,中間圓的半徑也可以定為一個屬性值去手動設置等等。。不過這些都是小意思,相信小伙伴們一定可以自己搞定的。我就不班門弄斧了。

最后這是優化過后的效果,接下來的與圖片結合,就希望小伙伴們自己實現一下啦~很簡單的。


帶你實現女朋友欲罷不能的網易云音樂宇宙塵埃特效


作者:Mlx

出自:掘金

原文:帶你實現女朋友欲罷不能的網易云音樂宇宙塵埃特效 - 掘金


分享到:
標簽:網易云音樂 宇宙塵埃 特效
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定