自定義控件——用貝塞爾曲線實現(xiàn)直播點贊效果

最近在逛博客的時候?qū)W到一個新的東西徐钠,正如標(biāo)題那樣豺型,用貝塞爾曲線實現(xiàn)直播點贊的動畫效果势腮,動畫效果看著不錯,而且感覺以后開發(fā)中遇到這種功能的幾率還是很大歹茶,所以學(xué)習(xí)一下,下面是對整個學(xué)習(xí)過程的記錄你弦。

先上實現(xiàn)的效果圖:

點贊效果圖

仔細觀察這個效果圖惊豺,可以將其分為兩個部分來實現(xiàn),首先是在屏幕底部中心生成圖片禽作,伴隨著圖片的生成扮叨,同時還有縮放動畫和透明度動畫,然后這個圖片會沿著一條曲線向上移動领迈,同時伴隨著透明度變化彻磁,這條曲線就是此次學(xué)習(xí)到的新東西——貝塞爾曲線碍沐。總結(jié)下流程:

1衷蜓、每次點擊在屏幕底部中心生成一張圖片累提,伴隨縮放動畫和漸變動畫;
2磁浇、第一部分動畫執(zhí)行完成后斋陪,圖片沿著貝塞爾曲線移動,同時伴隨漸變動畫置吓。

流程梳理清楚无虚,接下來開始編寫代碼,首先是第一部分的實現(xiàn)衍锚,先往數(shù)組中放三張圖友题,每次點擊都隨機取出一張圖片放到容器中指定的位置,下面看看核心部分代碼的實現(xiàn):

    /**
     * 動畫效果
     * @param iv 動畫target
     */
    private fun getAnimatorSet(iv: ImageView): AnimatorSet {
        //透明度
        val alphaAni = ObjectAnimator.ofFloat(iv,"alpha",0.3f,1f)
        //x方向縮放
        val scaleX = ObjectAnimator.ofFloat(iv,"scaleX",0.2f,1f)
        //方向縮放
        val scaleY = ObjectAnimator.ofFloat(iv,"scaleY",0.2f,1f)
        val createAnimatorSet = AnimatorSet() //圖片生成動畫
        createAnimatorSet.playTogether(alphaAni,scaleX,scaleY) //圖片的生成伴隨著三種動畫的同時發(fā)生
        createAnimatorSet.duration = 500
        return createAnimatorSet 
    }

這里主要是實現(xiàn)了屬性動畫和多個屬性動畫一起執(zhí)行的效果戴质。到這里再加上一些資源的配置就能實現(xiàn)圖片出現(xiàn)在底部的效果了度宦,下面是配置資源代碼以及效果:

class LikedEffectLayout @JvmOverloads constructor(context: Context,attr:AttributeSet ?= null,defAttr:Int = 0) : RelativeLayout(context,attr,defAttr){
    private lateinit var mRed : Drawable //紅心心
    private lateinit var mPink : Drawable //粉心心
    private lateinit var mBlue : Drawable //藍心心
    private lateinit var mDrawables : ArrayList<Drawable> //圖片集合 隨機選中一張圖片
    private lateinit var mInterpolators : ArrayList<Interpolator> //插值器集合 隨機選中一個插值器
    private var mDrawableHeight = 0 //圖片高度
    private var mDrawableWidth = 0 //圖片寬度
    private var mHeight = 0 //布局高度
    private var mWidth = 0 //布局寬度
    private var mParams : LayoutParams //圖片參數(shù)
    private var mRandom = Random() //隨機數(shù)

    init {
        initDrawable() //初始化圖片集
        initInterpolator() //初始化插值器集
        mParams = LayoutParams(mDrawableWidth/5,mDrawableHeight/5) //設(shè)置圖片參數(shù) 因為找的圖片尺寸太大了  所以縮小了5倍
        mParams.addRule(CENTER_HORIZONTAL, TRUE) //設(shè)置圖片水平居中
        mParams.addRule(ALIGN_PARENT_BOTTOM, TRUE) //設(shè)置圖片位于容器底部
    }

    /**
     * 初始化插值器集
     */
    private fun initInterpolator() {
        mInterpolators = arrayListOf()
        mInterpolators.add(LinearInterpolator())
        mInterpolators.add(AccelerateDecelerateInterpolator())
        mInterpolators.add(AccelerateInterpolator())
        mInterpolators.add(DecelerateInterpolator())
    }

    /**
     * 初始化圖片集
     */
    private fun initDrawable() {
        mRed = resources.getDrawable(R.drawable.love_red,null)
        mPink = resources.getDrawable(R.drawable.love_pink,null)
        mBlue = resources.getDrawable(R.drawable.love_blue,null)

        mDrawables = arrayListOf()
        mDrawables.apply {
            add(mRed)
            add(mPink)
            add(mBlue)
        }

        mDrawableHeight = mRed.intrinsicHeight //獲取圖片高度
        mDrawableWidth = mRed.intrinsicWidth //獲取圖片寬度
    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        mWidth = measuredWidth //獲取布局寬度
        mHeight = measuredHeight //獲取布局高度
    }

    /**
     * 暴露給外部調(diào)用的生成圖片的方法
     * 添加點贊效果的圖片
     * 點擊一次生成一張圖片 然后沿著貝塞爾曲線移動
     */
    fun addLove(){
        val loveIv = ImageView(context)
        loveIv.setImageDrawable(mDrawables[mRandom.nextInt(mDrawables.size)]) //從圖片集隨機取出一張
        loveIv.layoutParams = mParams
        addView(loveIv)

        val finalSet = getAnimatorSet(loveIv)//設(shè)置動畫效果
        finalSet.start() //動畫開始
    }

    /**
     * 動畫效果
     * @param iv 動畫target
     */
    private fun getAnimatorSet(iv: ImageView): AnimatorSet {
        //透明度
        val alphaAni = ObjectAnimator.ofFloat(iv,"alpha",0.3f,1f)
        //x方向縮放
        val scaleX = ObjectAnimator.ofFloat(iv,"scaleX",0.2f,1f)
        //方向縮放
        val scaleY = ObjectAnimator.ofFloat(iv,"scaleY",0.2f,1f)
        val createAnimatorSet = AnimatorSet() //圖片生成動畫
        createAnimatorSet.playTogether(alphaAni,scaleX,scaleY) //圖片的生成伴隨著三種動畫的同時發(fā)生
        createAnimatorSet.duration = 500
        return createAnimatorSet 
    }
}
生成圖片

至此第一部分就算完成了。下面就是怎么讓圖片沿著曲線動起來告匠,首先來看一下貝塞爾曲線以及它的使用:

貝塞爾曲線三次方公式
class BezierEvaluator(private val p1: PointF, private val p2: PointF) : TypeEvaluator<PointF>{

    override fun evaluate(t: Float, p0: PointF?, p3: PointF?): PointF {
        val point = PointF()
        /**
         * kotlin語言中要注意這種分行得寫法
         * 因為kotlin中沒有分號 所以一個表達式要么寫成一行 要么加上一個括號 否則這個估值器不生效
         */
        point.x = (p0!!.x*(1-t)*(1-t)*(1-t)
                +3*p1.x*t*(1-t)*(1-t)
                +3*p2.x*t*t*(1-t)
                +p3!!.x*t*t*t)
        point.y = p0.y*(1-t)*(1-t)*(1-t) +3*p1.y*t*(1-t)*(1-t) +3*p2.y*t*t*(1-t) +p3.y*t*t*t
        return point
    }
}

上面就是貝塞爾估值器戈抄,p1、p2兩個拐點需要我們自行計算傳進來后专,起始點p0和終止點p3是設(shè)置屬性動畫的時候設(shè)定划鸽,這里內(nèi)部已經(jīng)幫我們傳過來了,直接用就行戚哎⊙。總的來看這個估值器就是返回一個點,這個點的x坐標(biāo)和y坐標(biāo)根據(jù)貝塞爾曲線得到建瘫。
接下來看看屬性動畫對估值器的使用崭捍,以及生成的圖片是如何沿著貝塞爾曲線移動的:

    /**
     * 貝塞爾曲線動畫
     * @param iv target
     */
    private fun getBezierValueAnimator(iv: ImageView) : ValueAnimator{
        //起始點 此次是放在屏幕底部水平中央的位置
        val p0 = PointF(mWidth/2 - mDrawableWidth/10*1f,mHeight - mDrawableHeight/5*1f)
        //第一個的拐點 x坐標(biāo)在屏幕內(nèi)隨機取 y坐標(biāo)得保證比第二個拐點得要小
        val p1 = PointF(mRandom.nextInt(mWidth)*1f,mRandom.nextInt(mHeight/2)*1f)
        //第二個拐點
        val p2 = PointF(mRandom.nextInt(mWidth)*1f,mRandom.nextInt(mHeight/2)*1f+mHeight/2)
        //終點 屏幕得頂部隨機生成
        val p3 = PointF(mRandom.nextInt(mWidth - mDrawableWidth/5)*1f,0f)
        val evaluator = BezierEvaluator(p1,p2) //傳入兩個拐點生成貝塞爾估值器
        val animator = ValueAnimator.ofObject(evaluator,p0,p3)//生成屬性動畫
        /**
         * 監(jiān)聽動畫執(zhí)行過程不斷改變圖片得坐標(biāo) 達到動畫得效果
         */
        animator.addUpdateListener {
            val point = it.animatedValue as PointF
            iv.x = point.x
            iv.y = point.y
            iv.alpha = (1-it.animatedFraction) //伴隨一個透明度得變化
        }
        /**
         * 動畫結(jié)束從容器中移除target 內(nèi)存優(yōu)化
         */
        animator.doOnEnd {
            removeView(iv)
        }
        animator.setTarget(iv)
        animator.interpolator = mInterpolators[mRandom.nextInt(4)] //隨機生成一個插值器 控制運動速度
        animator.duration = 3000
        return animator
    }
貝塞爾運動軌跡

貝塞爾曲線是一條S型的曲線,所以我們只需要給定四個點的值啰脚,接下來的曲線就交給估值器去處理就行殷蛇,估值器會實時返回曲線上的點,在這里我們需要算出起始點p0橄浓,第一個拐點p1粒梦,第二個拐點p2和終止點p3,下面我們一一剖析這四個點:

1荸实、其中p0點位于屏幕底部的中心匀们,這個點是一個定點,由于渲染機制准给,其實這個點不是指的圖片的中心點p0泄朴,而是圖片左上角那個p點重抖,所以我們在計算的時候要把圖片的尺寸考慮進去,計算p0的坐標(biāo)實際是算p點的坐標(biāo),所以p0的x坐標(biāo)值應(yīng)該是:(布局的一半)-(圖片的一半) => mWidth/2 - mDrawableWidth/10(除以10是因為找的圖尺寸太大祖灰,我縮小了5倍)钟沛,p0的y坐標(biāo)值為:(布局的高度)-(圖片的高度) => mHeight - mDrawableHeight/5
2局扶、p1點是第一個拐點恨统,它和第二個拐點p2結(jié)合起來看,這種S型的曲線三妈,第一個拐點在第二個拐點的下方畜埋,所以我們要對兩個拐點的y坐標(biāo)做出約束,x坐標(biāo)的話只要在屏幕內(nèi)就可以畴蒲,所以兩個拐點的x坐標(biāo)就是在屏幕內(nèi)取隨機數(shù)悠鞍,即:mRandom.nextInt(mWidth),第一個拐點p1的y坐標(biāo)在屏幕下半部分隨機取值饿凛,即:mRandom.nextInt(mHeight/2)狞玛;
3软驰、第二個拐點p2在第一個拐點的上方涧窒,x坐標(biāo)也是在屏幕內(nèi)取值就可以,所以p2的x坐標(biāo)值為:mRandom.nextInt(mWidth)锭亏,y坐標(biāo)要保證是在第一個拐點的上方纠吴,所以結(jié)合第一個拐點的y坐標(biāo)可以得其y坐標(biāo)值為:mRandom.nextInt(mHeight/2)*1f+mHeight/2
4慧瘤、終止點p3的位置位于屏幕的頂部戴已,為了做出像花束那樣的發(fā)散效果,所以終止點的x坐標(biāo)并不固定锅减,在屏幕內(nèi)隨機取值就可以糖儡,即:mRandom.nextInt(mWidth - mDrawableWidth/5)*1f,減去圖片一個寬度是防止圖片在屏幕的兩側(cè)就跑出屏幕了怔匣,因為在屏幕的頂部握联,y坐標(biāo)值為0.

以上就是四個點的計算邏輯,當(dāng)然只適合我這種情況每瞒,不過無論怎么實現(xiàn)這條曲線金闽,都是有跡可循的。四個點找到后剿骨,將第一個拐點和第二個拐點傳入估值器內(nèi)生成一個估值器代芜,再根據(jù)這個估值器、起始點和終止點
生成圖片的屬性動畫浓利,至此這個動畫就完成了挤庇,也就是開篇那個效果圖的樣子钞速。

至此,直播間點贊的那種效果就完成了罚随,最后給動畫加了一個插值器玉工,從插值器集中隨機取一個出來,實現(xiàn)動畫的速率不一致淘菩,這樣看著就更加華麗(花里胡哨)遵班。還給動畫加了一個結(jié)束監(jiān)聽,因為這個效果是每次點擊就會生成一張圖片潮改,所以大量點擊后對內(nèi)存會有很大的消耗狭郑,所以在動畫結(jié)束后,對資源進行回收汇在,即:

/**
  * 動畫結(jié)束從容器中移除target 內(nèi)存優(yōu)化
  */
 animator.doOnEnd {
      removeView(iv)
 }

好了本次對貝塞爾曲線的學(xué)習(xí)就結(jié)束了翰萨,下面總結(jié)記錄下這次學(xué)習(xí)中的收獲的東西和存在的疑惑點:
收獲一:插值器和估值器是實現(xiàn)非勻速動畫的重要手段,插值器(TimeInterpolator)是根據(jù)時間流逝的百分比計算出屬性動畫的百分比糕殉;估值器(TypeEvaluator)是根據(jù)當(dāng)前屬性改變的百分比計算出的屬性值亩鬼。
插值器之前用的多一點,系統(tǒng)給定的線性插值器(LinearInterpolator)阿蝶、減速插值器(DecelerateInterpolator)和加速減速插值器(AccelerateDecelerateInterpolator)等雳锋,都跟加速度有關(guān),加速度和時間關(guān)聯(lián)羡洁,所以插值器的作用也就好理解了玷过;估值器這是第一次接觸,這次使用中筑煮,泛型返回的是一個點辛蚊,對于動畫過程來說其實就是一個點的移動過程,所以估值器的作用也就好理解了真仲。

收獲二:多個屬性動畫的同步執(zhí)行和順序執(zhí)行袋马。
AnimatorSet.playTogether(Animator... items)方法實現(xiàn)多個動畫的同步執(zhí)行,從方法名也能看出它的作用秸应;
AnimatorSet.playSequentially(Animator... items)這個方法看名字看不出來是干啥虑凛,點開源碼看就知道這個方法是按動畫傳入的順序分步執(zhí)行動畫的,放在前面的動畫最先執(zhí)行灸眼。下面貼出源碼:

    /**
     * Sets up this AnimatorSet to play each of the supplied animations when the
     * previous animation ends.
     *
     * @param items The animations that will be started one after another.
     */
    public void playSequentially(Animator... items) {
        if (items != null) {
            if (items.length == 1) {
                play(items[0]);
            } else {
                for (int i = 0; i < items.length - 1; ++i) {
                    play(items[i]).before(items[i + 1]);
                }
            }
        }
    }

關(guān)于屬性動畫卧檐,我之前也專門寫過一篇文章記錄我的理解,有興趣可以去瞅瞅焰宣,幫我漲漲閱讀量霉囚。文章地址

疑惑一:第一個疑惑就是四個點的坐標(biāo)確定那里,第一個拐點和第二個拐點的y坐標(biāo)確定上匕积,看別人的文章(具體實現(xiàn)的效果也沒錯)寫的是p1的y坐標(biāo)范圍為mRandom.nextInt(mHeight/2)盈罐,即屏幕的一半榜跌,p2的y坐標(biāo)范圍為mRandom.nextInt(mHeight/2)*1f+mHeight/2,即p2的y坐標(biāo)得比p1得大盅粪,但安卓得坐標(biāo)原點位于左上角钓葫,如果p2位于p1的上方,那p2的y坐標(biāo)應(yīng)該比p1小啊票顾,難道自定義控件里面計算的坐標(biāo)原點是在左下角础浮?

疑惑二:這個問題跟kotlin語言特性有關(guān),分行寫一個算式時有問題奠骄,因為kotlin中沒有分號豆同,在編譯的時候在每行末尾都會加一個分號,這樣就導(dǎo)致一個分行寫的算式被分成了很多個世子含鳞。
估值器里分行寫方程式的時候影锈,一開始我是分行寫的,然后一運行始終達不到效果蝉绷,很是納悶鸭廷,找了好久才找到原因,是一個括號引發(fā)的血案熔吗,最后將kotlin轉(zhuǎn)為java代碼才肯定了問題的根源辆床,下面來看看:
這是kotlin代碼:

        point.x = p0!!.x*(1-t)*(1-t)*(1-t)
                +3*p1.x*t*(1-t)*(1-t)
                +3*p2.x*t*t*(1-t)
                +p3!!.x*t*t*t
        point.y = p0.y*(1-t)*(1-t)*(1-t) +3*p1.y*t*(1-t)*(1-t) +3*p2.y*t*t*(1-t) +p3.y*t*t*t

轉(zhuǎn)換為java:

      point.x = p0.x * ((float)1 - t) * ((float)1 - t) * ((float)1 - t);
      float var10000 = (float)3 * this.p1.x * t * ((float)1 - t) * ((float)1 - t);
      var10000 = (float)3 * this.p2.x * t * t * ((float)1 - t);

      var10000 = p3.x * t * t * t;
      point.y = p0.y * ((float)1 - t) * ((float)1 - t) * ((float)1 - t) + (float)3 * this.p1.y * t * ((float)1 - t) * ((float)1 - t) + (float)3 * this.p2.y * t * t * ((float)1 - t) + p3.y * t * t * t;

以上就是在kotlin中分行寫沒加括號時轉(zhuǎn)換成java后的代碼,可以看到x的值只有第一行的值磁滚,后面的都沒有算上佛吓,y坐標(biāo)值在kotlin中沒分行寫宵晚,就沒有問題垂攘,下面再來看看加上括號的樣子:

     float var10001 = p0.x * ((float)1 - t) * ((float)1 - t) * ((float)1 - t) + (float)3 * this.p1.x * t * ((float)1 - t) * ((float)1 - t) + (float)3 * this.p2.x * t * t * ((float)1 - t);

      point.x = var10001 + p3.x * t * t * t;
      point.y = p0.y * ((float)1 - t) * ((float)1 - t) * ((float)1 - t) + (float)3 * this.p1.y * t * ((float)1 - t) * ((float)1 - t) + (float)3 * this.p2.y * t * t * ((float)1 - t) + p3.y * t * t * t;

可以看到此時的x坐標(biāo)是沒有問題的,所以在kotlin中分行寫算式時需要加括號淤刃。

最后晒他,附上源碼地址:源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市逸贾,隨后出現(xiàn)的幾起案子陨仅,更是在濱河造成了極大的恐慌,老刑警劉巖铝侵,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灼伤,死亡現(xiàn)場離奇詭異,居然都是意外死亡咪鲜,警方通過查閱死者的電腦和手機狐赡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疟丙,“玉大人颖侄,你說我怎么就攤上這事鸟雏。” “怎么了览祖?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵孝鹊,是天一觀的道長。 經(jīng)常有香客問我展蒂,道長又活,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任锰悼,我火速辦了婚禮皇钞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘松捉。我一直安慰自己夹界,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布隘世。 她就那樣靜靜地躺著可柿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪丙者。 梳的紋絲不亂的頭發(fā)上复斥,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音械媒,去河邊找鬼目锭。 笑死,一個胖子當(dāng)著我的面吹牛纷捞,可吹牛的內(nèi)容都是我干的痢虹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼主儡,長吁一口氣:“原來是場噩夢啊……” “哼奖唯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起糜值,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤丰捷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后寂汇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體病往,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年骄瓣,在試婚紗的時候發(fā)現(xiàn)自己被綠了停巷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖叠穆,靈堂內(nèi)的尸體忽然破棺而出少漆,到底是詐尸還是另有隱情,我是刑警寧澤硼被,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布示损,位于F島的核電站,受9級特大地震影響嚷硫,放射性物質(zhì)發(fā)生泄漏检访。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一仔掸、第九天 我趴在偏房一處隱蔽的房頂上張望脆贵。 院中可真熱鬧,春花似錦起暮、人聲如沸卖氨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筒捺。三九已至,卻和暖如春纸厉,著一層夾襖步出監(jiān)牢的瞬間系吭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工颗品, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肯尺,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓躯枢,卻偏偏與公主長得像则吟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子闺金,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359