帶你走一波Android自定義Animator屬性動畫相關(guān)事項(一)

一新蟆、簡介

image.png

如上圖所示:android動畫分類大致有兩種一種是View動畫一種是轉(zhuǎn)場動畫织鲸。

幀動畫:將圖片一張一張按順序播放千扔,展現(xiàn)出動畫效果砸烦。

補間動畫:實現(xiàn)動畫alpha(淡入淡出)冗尤,translate(位移)听盖,scale(縮放大小)裂七,rotate(旋轉(zhuǎn))等效果皆看,一般采用xml文件形式。

屬性動畫:(重點)它是對于對象屬性的動畫背零。補間動畫的內(nèi)容腰吟,都可以通過屬性動畫實現(xiàn)。

這里我們就不講幀動畫補間動畫,這兩個大家可以自己百度一下用法毛雇。(另外這篇文章中的動畫都是在代碼中實現(xiàn)的嫉称,如果要看xml的使用方法,可以看看Android 動畫使用 scale灵疮、alpha织阅、translate、rotate震捣、set
這篇其他人寫的這篇文章荔棉。

二、屬性動畫

基本使用 (ViewPropertyAnimator)

imag_view.animate().run {
translationX(400f) //設(shè)置左移
duration = 1000//設(shè)置動畫運行的時間
setInterpolator(LinearInterpolator()) //設(shè)置線性插值器
}

gifeditor_20191202_151011.gif

上面是最基本的使用蒿赢,api提供的有移動润樱、旋轉(zhuǎn)縮放诉植、透明祥国,看下面的api:
image.png

上面的api中可以看到 都存在絕對相對(方法后面-by)的方法,其中絕對的方法以上面的代碼為例子晾腔,區(qū)別是:
translationX(400f)代表將translationX變成400
translationXBy(400f)代表將translationX增加400

這么多api就不做逐一展示了舌稀。

ObjectAnimator

1. 基本的使用方式:
  1. 用 ObjectAnimator.ofXXX() 創(chuàng)建 ObjectAnimator 對象;
  2. 添加時長灼擂、差值器等各種參數(shù)
  3. 用 start() 方法執(zhí)行動畫壁查。
ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f).run {
    duration = 1000
    interpolator = LinearInterpolator()
    start()
}

gifeditor_20191202_164532.gif

上面是對一個系統(tǒng)提供的View進(jìn)行動畫展示,主要方法是ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f)
第一個參數(shù):傳入要進(jìn)行屬性動畫的view
第二個參數(shù):要變化的屬性值剔应,這里是傳入View.ROTATION睡腿,也就是"rotation"
第三個參數(shù):屬性起始值
第四個參數(shù):屬性結(jié)束值
后面還可以加入多個參數(shù)值,從第三個參數(shù)開始到最后第n個參數(shù)峻贮,表示屬性開始 ->中間值->中間值.... ->結(jié)束值
注意一點:并非所有的屬性都是可以有set get方法席怪,可以進(jìn)行屬性動畫。

2. 自定義View動畫

自定義View屬性動畫以及使用的步驟:

  1. 為要改變的屬性添加setter/getter方法
  2. setter方法中調(diào)用invalidate()使其重新繪畫
  3. 在onDraw()中根據(jù)改變的屬性繪畫出你要的效果
  4. 使用的時候跟ObjectAnimator基本使用方式一致

大致的模板代碼


class CircleView : View {
  //提供給外面改變的屬性值 這里使用kotlin語法 實際上它默認(rèn)已經(jīng)實現(xiàn)了setter/getter方法
// 但是這里個set方法我們要改寫一下 記得調(diào)用 invalidate()
    var progress: Float = 0f
        set(value) {
            field = value
            invalidate()
        }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //省略......
        canvas!!.drawArc(rectF, 135f, progress * 2.7f, false, paint)
        //省略......
        canvas.drawText("${progress.toInt()}%", centerX, centerY+40, paint)
    }

}

真正的實現(xiàn)與調(diào)用

class CircleView : View {

    val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    val rectF = RectF()

    init {
        paint.run {
            textSize = dpToPixel(50f)
            textAlign = Paint.Align.CENTER
        }
    }

    var radius = dpToPixel(120f)

    var progress: Float = 0f
        set(value) {
            field = value
            invalidate()
        }

    constructor(context: Context) : super(context)
    constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet)


    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var centerX = width / 2f
        var centerY = height / 2f
        //畫弧形進(jìn)度條
        paint.run {
            color = context.getColor(R.color.colorAccent)
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
            strokeWidth = dpToPixel(20f)
        }
        rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius)
        canvas!!.drawArc(rectF, 135f, progress * 2.7f, false, paint)

        //畫百分比的數(shù)值
        paint.run {
            color = Color.BLACK
            style = Paint.Style.FILL
        }
        canvas.drawText("${progress.toInt()}%", centerX, centerY+40, paint)
    }

}
//調(diào)用代碼
ObjectAnimator.ofFloat(circle_view,"progress",0f,80f).run {
  duration = 2000
  interpolator = OvershootInterpolator() //插值器 超過結(jié)束值后再回彈
  repeatCount = INFINITE  // 重復(fù)次數(shù)-無限循環(huán)
  repeatMode = RESTART //重復(fù)模式-重新開始
  start()
}
gifeditor_20191202_172002.gif

這里設(shè)置了OvershootInterpolator插值器纤控,使得它可以超過后回彈回來挂捻。這里可以使用PropertyValuesHolders.ofKeyframe()做到類似效果,可以翻到下面對應(yīng)內(nèi)容看一下

ValueAnimator

這個是ObjectAnimator的父類船万,這個我并沒怎么接觸刻撒,后期再補充吧

2. 設(shè)置監(jiān)聽器

  • 設(shè)置ObjectAnimator的監(jiān)聽器
監(jiān)聽器類型.png
//添加停止的監(jiān)聽器
addPauseListener( object : Animator.AnimatorPauseListener{
  override fun onAnimationPause(animation: Animator?) {
    Log.i("MainActivity","addPauseListener -- onAnimationPause ------------------------------------")
  }
  override fun onAnimationResume(animation: Animator?) {
    Log.i("MainActivity","addPauseListener -- onAnimationResume ------------------------------------")
  }
})
//添加更新的監(jiān)聽器
addUpdateListener(object :ValueAnimator.AnimatorUpdateListener{
  override fun onAnimationUpdate(animation: ValueAnimator?) {
//    Log.i("MainActivity","addUpdateListener -- onAnimationUpdate ------------------------------------")
    }
  })
//添加監(jiān)聽器
addListener(object : Animator.AnimatorListener{
  override fun onAnimationRepeat(animation: Animator?) {
    Log.i("MainActivity","AnimatorListener -- onAnimationRepeat------------------------------------")
    }
   override fun onAnimationEnd(animation: Animator?) {
    Log.i("MainActivity","AnimatorListener -- onAnimationEnd------------------------------------")
    }
  override fun onAnimationCancel(animation: Animator?) {
  Log.i("MainActivity","AnimatorListener -- onAnimationCancel------------------------------------")
  }
  override fun onAnimationStart(animation: Animator?) {
      Log.i("MainActivity","AnimatorListener -- onAnimationStart------------------------------------")
     }
  })
}

設(shè)置點擊事件監(jiān)聽

R.id.start_object_animator ->{
                省略...
                mObjectAnim.start()
                Log.i("MainActivity","點擊開始狀態(tài) --${!mObjectAnim.isStarted}")
            }

            R.id.end_object_animator ->{
                mObjectAnim.end()
                Log.i("MainActivity","點擊結(jié)束狀態(tài) --${!mObjectAnim.isRunning}")

            }
            R.id.cancel_object_animator ->{
                mObjectAnim.cancel()
                Log.i("MainActivity","點擊取消狀態(tài) -- ")
            }
            R.id.pause_object_animator->{
                if (mObjectAnim.isRunning) {
                    mObjectAnim.pause()
                    Log.i("MainActivity","點擊暫停狀態(tài) --${mObjectAnim.isPaused}")
                }
            }
            R.id.reverse_object_animator->{
                Log.i("MainActivity","點擊反向狀態(tài) --")
                mObjectAnim.reverse()
            }
            R.id.resume_object_animator->{
                mObjectAnim.resume()
                Log.i("MainActivity","點擊繼續(xù)執(zhí)行 --")
            }

點擊順序 (這里點擊開始按鈕是重新初始化動畫,請大家不要誤解)
開始start -> 暫停pause ->繼續(xù)執(zhí)行resume->結(jié)束end
開始start->取消cancel
開始start->結(jié)束end

效果圖.gif
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態(tài) --false
MainActivity: addPauseListener -- onAnimationPause ------------------------------------
MainActivity: 點擊暫停狀態(tài) --true
MainActivity: addPauseListener -- onAnimationResume ------------------------------------
MainActivity: 點擊繼續(xù)執(zhí)行 --
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊結(jié)束狀態(tài) --true
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態(tài) --false
MainActivity: AnimatorListener -- onAnimationCancel------------------------------------
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊取消狀態(tài) -- 
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態(tài) --false
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊結(jié)束狀態(tài) --true

上面是點擊產(chǎn)生的log日志耿导,其中有個addUpdateListener監(jiān)聽我沒有打印信息声怔,因為動畫運行中就會不停打印出來,所以就沒有打印出來了舱呻。
可以看到所有的狀態(tài)都是可以有回調(diào)方法監(jiān)聽的醋火。

這里有個方法要注意一下 cancel()end()這個兩個方法。

如果動畫是已經(jīng)結(jié)束了end()的時候,就不會有回調(diào)onAnimationCancelonAnimationEnd兩個監(jiān)聽方法了,看一下ValueAnimator源碼中有體現(xiàn)了

    @Override
    public void cancel() {
關(guān)鍵代碼1
        if (mAnimationEndRequested) {
            return;
        }
        if ((mStarted || mRunning) && mListeners != null) {
            if (!mRunning) {
                notifyStartListeners();
            }
            ArrayList<AnimatorListener> tmpListeners =
                    (ArrayList<AnimatorListener>) mListeners.clone();
            for (AnimatorListener listener : tmpListeners) {
關(guān)鍵代碼2
                  listener.onAnimationCancel(this);
            }
        }
關(guān)鍵代碼3
        endAnimation();
    }
    private void endAnimation() {
關(guān)鍵代碼4
        mAnimationEndRequested = true;
  省略......
            for (int i = 0; i < numListeners; ++i) {
關(guān)鍵代碼5
                tmpListeners.get(i).onAnimationEnd(this, mReversing);
            }
        }
省略......
    }

關(guān)鍵代碼1:mAnimationEndRequested == true則不走下面的邏輯胎撇,設(shè)置true是在關(guān)鍵代碼4中設(shè)置的,也就是說當(dāng)動畫結(jié)束調(diào)用了endAnimation()就不會調(diào)用到兩個回調(diào)onAnimationCancelonAnimationEnd介粘。
關(guān)鍵代碼2跟3跟5:就是動畫未結(jié)束,所以調(diào)用了回調(diào)onAnimationCancel()再調(diào)用回調(diào)onAnimationEnd()

小結(jié):當(dāng)調(diào)用cancel()的時候晚树,動畫未結(jié)束時則回調(diào)onAnimationCancel()onAnimationEnd(),當(dāng)動畫結(jié)束時雅采,則不會調(diào)用任何監(jiān)聽回調(diào)方法

end方法有時候會回調(diào)兩個回調(diào) 分別是onAnimationStartonAnimationEnd,看一下ValueAnimator源碼中的邏輯

    public void end() {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        if (!mRunning) {
            // Special case if the animation has not yet started; get it ready for ending
            startAnimation();
            mStarted = true;
        } else if (!mInitialized) {
            initAnimation();
        }
        animateValue(shouldPlayBackward(mRepeatCount, mReversing) ? 0f : 1f);
        endAnimation();
    }

從上面的邏輯可以看到爵憎,當(dāng)調(diào)用到!mRunning == true的時候,會調(diào)用startAnimation()導(dǎo)致回調(diào)多一個onAnimationStart方法婚瓜。

小結(jié): 當(dāng)調(diào)用end()的時候宝鼓,如果動畫處于運行中,則回調(diào)onAnimationEnd巴刻,如果不處于運行中愚铡,則回調(diào)onAnimationStartonAnimationEnd

  • 設(shè)置ViewPropertyAnimator的監(jiān)聽器

image.png

相比ObjectAnimator的監(jiān)聽器胡陪,這里的ViewPropertyAnimator多了兩個回調(diào)方法

withStartActionwithEndAction分別在開始跟結(jié)束調(diào)用沥寥,但是只會被調(diào)用一次。
setListener中的onAnimationRepeat回調(diào)不會被調(diào)用柠座,因為ViewPropertyAnimator不支持重復(fù)
其它的回調(diào)跟ObjectAnimator一致

三. 屬性動畫組合

image.png
  • ViewPropertyAnimator
    改變尺寸同時改變透明度邑雅,下面這種寫法是幾種動畫一塊運行的
R.id.btn_viewProperty_mulity->{
  if (isSelect) {
    imag_view.animate().scaleX(1.5f).scaleY(1.5f).alpha(0f).duration =2000
  }else{
    imag_view.animate().scaleX(1.0f).scaleY(1.0f).alpha(1f).duration =2000
  }
   isSelect = !isSelect
}
gifeditor_20191203_163831.gif
  • PropertyValuesHolder
    ObjectAnimator使用時則是要通過PropertyValuesHolder來實現(xiàn)
    上面的代碼可以用
R.id.btn_viewProperty_mulity->{
  if (isSelect) {
    var propertyValueHolder1 = PropertyValuesHolder.ofFloat("scaleX",1f,1.5f)
    var propertyValueHolder2 = PropertyValuesHolder.ofFloat("scaleY",1f,1.5f)
    var propertyValueHolder3 = PropertyValuesHolder.ofFloat("alpha",1f,0f)
    ObjectAnimator.ofPropertyValuesHolder(imag_view,propertyValueHolder1,propertyValueHolder2,propertyValueHolder3)
    .setDuration(2000).start()
  }else{
    var propertyValueHolder1 = PropertyValuesHolder.ofFloat("scaleX",1.5f,1f)
    var propertyValueHolder2 = PropertyValuesHolder.ofFloat("scaleY",1.5f,1f)
    var propertyValueHolder3 = PropertyValuesHolder.ofFloat("alpha",0f,1f)
    ObjectAnimator.ofPropertyValuesHolder(imag_view,propertyValueHolder1,propertyValueHolder2,propertyValueHolder3)
    .setDuration(2000).start()
    }
    isSelect = !isSelect
}
  • PropertyValuesHolders.ofKeyframe()同一個屬性拆分
    ofKeyframe (關(guān)鍵幀),可以把同一個動畫屬性拆分成多個階段
var keyframe = Keyframe.ofFloat(0f,0f)//關(guān)鍵幀 0剛才開時的時候
var keyframe1 = Keyframe.ofFloat(0.5f,95f)//關(guān)鍵幀 0.5進(jìn)行到一半的時候
var keyFrame2 = Keyframe.ofFloat(1f,80f)//關(guān)鍵幀1最后的時候
var holder = PropertyValuesHolder.ofKeyframe("progress", keyframe, keyframe1, keyFrame2)
ObjectAnimator.ofPropertyValuesHolder(circle_view,holder).setDuration(3000).start()

gifeditor_20191203_175839.gif

實現(xiàn)類似interpolator = OvershootInterpolator()插值器超過結(jié)束值后再回彈的效果

  • AnimatorSet
    也是組合動畫的妈经,可以讓多個動畫配合執(zhí)行淮野,不過它可以讓動畫先后有序執(zhí)行~~
    playTogether(同時執(zhí)行)、playSequentially(順序執(zhí)行)
    精確配置順序with(),before(),after()
示例代碼:
var animator1 = ObjectAnimator.ofFloat(imag_view,"alpha",0f,1f)
animator1.interpolator = AccelerateDecelerateInterpolator()
var animator3 = ObjectAnimator.ofFloat(imag_view,"scaleX",0f,1f)
var animator2 = ObjectAnimator.ofFloat(imag_view,"scaleY",0f,1f)
var animator4 = ObjectAnimator.ofFloat(imag_view,"translationX",0f,200f)
animator4.interpolator = LinearInterpolator()
AnimatorSet().apply {
  playTogether(animator2,animator3)
  playSequentially(animator1,animator4)
  duration = 2000
  start()
}

gifeditor_20191203_172107.gif

解釋一下上面的代碼
上面使用 AnimatorSet將多個ObjectAnimator放在一塊運行吹泡,并且playTogether(animator2,animator3) 代表一起運行
playSequentially(animator1,animator4) 代表先后順序運行
所以就有了下面的效果圖 (x軸骤星、y軸方向放大跟透明度由0-1)是一起的,然后在x軸方向移動爆哑。

上面的播放邏輯也可以使用精確配置順序with(),before(),after()來實現(xiàn)

AnimatorSet().apply {
  play(animator1).with(animator2).with(animator3).before(animator4)
  duration = 2000
  start()
 }

注意:每個傳入給AnimatorSetanimator可以自己定義自己的動畫運行時間洞难、差值器等,但是如果在AnimatorSet設(shè)置了運行時間的話則以在AnimatorSet設(shè)置的為準(zhǔn)泪漂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末廊营,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子萝勤,更是在濱河造成了極大的恐慌露筒,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敌卓,死亡現(xiàn)場離奇詭異慎式,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門瘪吏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來癣防,“玉大人,你說我怎么就攤上這事掌眠±俣ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵蓝丙,是天一觀的道長级遭。 經(jīng)常有香客問我,道長渺尘,這世上最難降的妖魔是什么挫鸽? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮鸥跟,結(jié)果婚禮上丢郊,老公的妹妹穿的比我還像新娘。我一直安慰自己医咨,他們只是感情好枫匾,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布洲炊。 她就那樣靜靜地躺著姐直,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谐檀。 梳的紋絲不亂的頭發(fā)上惩歉,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天等脂,我揣著相機與錄音,去河邊找鬼撑蚌。 笑死上遥,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的争涌。 我是一名探鬼主播粉楚,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼亮垫!你這毒婦竟也來了模软?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤饮潦,失蹤者是張志新(化名)和其女友劉穎燃异,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體继蜡,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡回俐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年逛腿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仅颇。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡单默,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出忘瓦,到底是詐尸還是另有隱情搁廓,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布政冻,位于F島的核電站枚抵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏明场。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一李丰、第九天 我趴在偏房一處隱蔽的房頂上張望苦锨。 院中可真熱鬧,春花似錦趴泌、人聲如沸舟舒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秃励。三九已至,卻和暖如春吉捶,著一層夾襖步出監(jiān)牢的瞬間夺鲜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工呐舔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留币励,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓珊拼,卻偏偏與公主長得像食呻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子澎现,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 【Android 動畫】 動畫分類補間動畫(Tween動畫)幀動畫(Frame 動畫)屬性動畫(Property ...
    Rtia閱讀 6,164評論 1 38
  • 屬性動畫(Animator)是在API11(Android3.0)新加入的仅胞。主要是針對一個對象的屬性變化加入動畫效...
    zackyG閱讀 1,910評論 0 6
  • 動畫基礎(chǔ)概念 動畫分類 Android 中動畫分為兩種,一種是 Tween 動畫剑辫、還有一種是 Frame 動畫干旧。 ...
    Rtia閱讀 1,231評論 0 6
  • 前面學(xué)習(xí)的內(nèi)容:Android自定義View(一) -- 初識Android自定義View(二) -- Paint...
    T9的第三個三角閱讀 4,334評論 1 19
  • 不同的季節(jié)莱革,我們可以欣賞到不同的花峻堰,例如,一月的迎春花盅视,二月的杏花捐名,三月的桃花,四月的牡丹闹击,五月的石榴花镶蹋,...
    花開半夏_1d0c閱讀 891評論 0 0