一、簡介
如上圖所示:android動畫分類大致有兩種一種是
View動畫
一種是轉場動畫
。幀動畫:將圖片一張一張按順序播放,展現出動畫效果。
補間動畫:實現動畫alpha(淡入淡出),translate(位移),scale(縮放大小),rotate(旋轉)等效果,一般採用xml文件形式。
屬性動畫:(重點)它是對於對象屬性的動畫。補間動畫的內容,都可以通過屬性動畫實現。
這裏我們就不講幀動畫
跟補間動畫
,這兩個大家可以自己百度一下用法。(另外這篇文章中的動畫都是在代碼中實現的,如果要看xml的使用方法,可以看看Android 動畫使用 scale、alpha、translate、rotate、set
這篇其他人寫的這篇文章。
二、屬性動畫
基本使用 (ViewPropertyAnimator)
imag_view.animate().run {
translationX(400f) //設置左移
duration = 1000//設置動畫運行的時間
setInterpolator(LinearInterpolator()) //設置線性插值器
}
上面是最基本的使用,api提供的有移動
、旋轉
、縮放
、透明
,看下面的api:
上面的api中可以看到 都存在
絕對
跟相對
(方法後面-by
)的方法,其中絕對的方法以上面的代碼爲例子,區別是:
translationX(400f)
代表將translationX
變成400
translationXBy(400f)
代表將translationX
增加400
這麼多api就不做逐一展示了。
ObjectAnimator
1. 基本的使用方式:
- 用 ObjectAnimator.ofXXX() 創建 ObjectAnimator 對象;
- 添加時長、差值器等各種參數
- 用 start() 方法執行動畫。
ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f).run {
duration = 1000
interpolator = LinearInterpolator()
start()
}
上面是對一個系統提供的View
進行動畫展示,主要方法是ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f)
第一個參數:傳入要進行屬性動畫的view
第二個參數:要變化的屬性值,這裏是傳入View.ROTATION
,也就是"rotation"
第三個參數:屬性起始值
第四個參數:屬性結束值
後面還可以加入多個參數值,從第三個參數開始到最後第n個參數,表示屬性開始 ->中間值->中間值… ->結束值
注意一點:並非所有的屬性都是可以有set
get
方法,可以進行屬性動畫。
2. 自定義View動畫
自定義View屬性動畫以及使用的步驟:
- 爲要改變的屬性添加setter/getter方法
- setter方法中調用
invalidate()
使其重新繪畫- 在onDraw()中根據改變的屬性繪畫出你要的效果
- 使用的時候跟ObjectAnimator基本使用方式一致
大致的模板代碼
class CircleView : View {
//提供給外面改變的屬性值 這裏使用kotlin語法 實際上它默認已經實現了setter/getter方法
// 但是這裏個set方法我們要改寫一下 記得調用 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)
}
}
真正的實現與調用
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
//畫弧形進度條
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)
//畫百分比的數值
paint.run {
color = Color.BLACK
style = Paint.Style.FILL
}
canvas.drawText("${progress.toInt()}%", centerX, centerY+40, paint)
}
}
//調用代碼
ObjectAnimator.ofFloat(circle_view,"progress",0f,80f).run {
duration = 2000
interpolator = OvershootInterpolator() //插值器 超過結束值後再回彈
repeatCount = INFINITE // 重複次數-無限循環
repeatMode = RESTART //重複模式-重新開始
start()
}
這裏設置了
OvershootInterpolator
插值器,使得它可以超過後回彈回來。這裏可以使用PropertyValuesHolders.ofKeyframe()
做到類似效果,可以翻到下面對應內容看一下
#####ValueAnimator
這個是ObjectAnimator
的父類,這個我並沒怎麼接觸,後期再補充吧
2. 設置監聽器
//添加停止的監聽器
addPauseListener( object : Animator.AnimatorPauseListener{
override fun onAnimationPause(animation: Animator?) {
Log.i("MainActivity","addPauseListener -- onAnimationPause ------------------------------------")
}
override fun onAnimationResume(animation: Animator?) {
Log.i("MainActivity","addPauseListener -- onAnimationResume ------------------------------------")
}
})
//添加更新的監聽器
addUpdateListener(object :ValueAnimator.AnimatorUpdateListener{
override fun onAnimationUpdate(animation: ValueAnimator?) {
// Log.i("MainActivity","addUpdateListener -- onAnimationUpdate ------------------------------------")
}
})
//添加監聽器
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------------------------------------")
}
})
}
設置點擊事件監聽
R.id.start_object_animator ->{
省略...
mObjectAnim.start()
Log.i("MainActivity","點擊開始狀態 --${!mObjectAnim.isStarted}")
}
R.id.end_object_animator ->{
mObjectAnim.end()
Log.i("MainActivity","點擊結束狀態 --${!mObjectAnim.isRunning}")
}
R.id.cancel_object_animator ->{
mObjectAnim.cancel()
Log.i("MainActivity","點擊取消狀態 -- ")
}
R.id.pause_object_animator->{
if (mObjectAnim.isRunning) {
mObjectAnim.pause()
Log.i("MainActivity","點擊暫停狀態 --${mObjectAnim.isPaused}")
}
}
R.id.reverse_object_animator->{
Log.i("MainActivity","點擊反向狀態 --")
mObjectAnim.reverse()
}
R.id.resume_object_animator->{
mObjectAnim.resume()
Log.i("MainActivity","點擊繼續執行 --")
}
點擊順序 (這裏點擊開始按鈕是重新初始化動畫,請大家不要誤解)
開始start -> 暫停pause ->繼續執行resume->結束end
開始start->取消cancel
開始start->結束end
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1LU1soK0-1583756850159)(https://user-gold-cdn.xitu.io/2019/12/6/16ed8de489e5844e?w=411&h=726&f=gif&s=7632472)]
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態 --false
MainActivity: addPauseListener -- onAnimationPause ------------------------------------
MainActivity: 點擊暫停狀態 --true
MainActivity: addPauseListener -- onAnimationResume ------------------------------------
MainActivity: 點擊繼續執行 --
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊結束狀態 --true
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態 --false
MainActivity: AnimatorListener -- onAnimationCancel------------------------------------
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊取消狀態 --
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 點擊開始狀態 --false
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 點擊結束狀態 --true
上面是點擊產生的log
日誌,其中有個addUpdateListener
監聽我沒有打印信息,因爲動畫運行中就會不停打印出來,所以就沒有打印出來了。
可以看到所有的狀態都是可以有回調方法監聽的。
這裏有個方法要注意一下 cancel()
跟end(
)這個兩個方法。
如果動畫是已經結束了
end()
的時候,就不會有回調onAnimationCancel
跟onAnimationEnd
兩個監聽方法了,看一下ValueAnimator
源碼中有體現了
@Override
public void cancel() {
關鍵代碼1
if (mAnimationEndRequested) {
return;
}
if ((mStarted || mRunning) && mListeners != null) {
if (!mRunning) {
notifyStartListeners();
}
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
關鍵代碼2
listener.onAnimationCancel(this);
}
}
關鍵代碼3
endAnimation();
}
private void endAnimation() {
關鍵代碼4
mAnimationEndRequested = true;
省略......
for (int i = 0; i < numListeners; ++i) {
關鍵代碼5
tmpListeners.get(i).onAnimationEnd(this, mReversing);
}
}
省略......
}
關鍵代碼1:mAnimationEndRequested == true
則不走下面的邏輯,設置true
是在關鍵代碼4
中設置的,也就是說當動畫結束調用了endAnimation()
就不會調用到兩個回調onAnimationCancel
跟onAnimationEnd
。
關鍵代碼2跟3跟5:就是動畫未結束,所以調用了回調onAnimationCancel()
再調用回調onAnimationEnd()
小結:當調用cancel()的時候,動畫未結束時則回調
onAnimationCancel()
跟onAnimationEnd()
,當動畫結束時,則不會調用任何監聽回調方法
end
方法有時候會回調兩個回調 分別是onAnimationStart
跟onAnimationEnd
,看一下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();
}
從上面的邏輯可以看到,當調用到!mRunning == true
的時候,會調用startAnimation()
導致回調多一個onAnimationStart
方法。
小結: 當調用
end()
的時候,如果動畫處於運行中,則回調onAnimationEnd
,如果不處於運行中,則回調onAnimationStart
跟onAnimationEnd
。
相比ObjectAnimator
的監聽器,這裏的ViewPropertyAnimator
多了兩個回調方法
withStartAction
跟withEndAction
分別在開始跟結束調用,但是隻會被調用一次。
setListener
中的onAnimationRepeat
回調不會被調用,因爲ViewPropertyAnimator
不支持重複
其它的回調跟ObjectAnimator
一致
三. 屬性動畫組合
改變尺寸同時改變透明度,下面這種寫法是幾種動畫一塊運行的
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
}
ObjectAnimator
使用時則是要通過PropertyValuesHolder
來實現
上面的代碼可以用
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
}
ofKeyframe
(關鍵幀),可以把同一個動畫屬性拆分成多個階段
var keyframe = Keyframe.ofFloat(0f,0f)//關鍵幀 0剛纔開時的時候
var keyframe1 = Keyframe.ofFloat(0.5f,95f)//關鍵幀 0.5進行到一半的時候
var keyFrame2 = Keyframe.ofFloat(1f,80f)//關鍵幀1最後的時候
var holder = PropertyValuesHolder.ofKeyframe("progress", keyframe, keyframe1, keyFrame2)
ObjectAnimator.ofPropertyValuesHolder(circle_view,holder).setDuration(3000).start()
實現類似interpolator = OvershootInterpolator()
插值器超過結束值後再回彈的效果
也是組合動畫的,可以讓多個動畫配合執行,不過它可以讓動畫先後有序執行~~
playTogether
(同時執行)、playSequentially
(順序執行)
精確配置順序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()
}
解釋一下上面的代碼
上面使用 AnimatorSet
將多個ObjectAnimator
放在一塊運行,並且playTogether(animator2,animator3)
代表一起運行
playSequentially(animator1,animator4)
代表先後順序運行
所以就有了下面的效果圖 (x軸、y軸方向放大跟透明度由0-1)是一起的,然後在x軸方向移動。
上面的播放邏輯也可以使用精確配置順序with(),before(),after()來實現
AnimatorSet().apply {
play(animator1).with(animator2).with(animator3).before(animator4)
duration = 2000
start()
}
注意:每個傳入給
AnimatorSet
的animator
可以自己定義自己的動畫運行時間、差值器等,但是如果在AnimatorSet
設置了運行時間的話則以在AnimatorSet
設置的爲準。