最近在逛博客的時候?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中分行寫算式時需要加括號淤刃。
最后晒他,附上源碼地址:源碼