我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達(dá)達(dá)達(dá)達(dá)sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.謝謝關(guān)注_,說不定什么時候會有福利哈.
項(xiàng)目地址:ViewAnimator豫缨,本文分析版本: dfa45e0
1.簡介
在項(xiàng)目開發(fā)中我們應(yīng)該都接觸過動畫效果的開發(fā).我們知道在
Andorid
中實(shí)現(xiàn)動畫大致分為兩類,一種是Tween/Frame
動畫,另一種是Property Animation
也就是屬性動畫.關(guān)于這兩種動畫的使用方法我們這篇文章就不多做討論了。可以從這篇文章了解更多寂屏。我們這篇文章只涉及屬性動畫相關(guān)知識洽故。我們今天要介紹的ViewAnimator
就是用來簡化我們寫屬性動畫的的代碼量的,它可以通過非常簡潔的代碼通過建造者模式調(diào)用來組合各種動畫.讓我們的代碼簡潔易讀咒循。如果你的APP
里需要各種動畫組合,ViewAnimator
一定是你的最佳選擇隅茎。
2.使用方法
想必大家都使用過屬性動畫了欣孤。我們來做一個最簡單的位移動畫:
ObjectAnimator animator = ObjectAnimator.ofFloat(textView, "translationX",0, 500);
animator.setDuration(2000);
animator.setRepeatCount(1);
animator.setInterpolator(new BounceInterpolator());
animator.start();
上面的代碼執(zhí)行之后就可以使textView
從當(dāng)前位置水平移動500px
,整個動畫過程是2s
,并且添加了一個彈性插值器,而且使動畫再重復(fù)執(zhí)行一遍。這樣看起來整個代碼還是很清晰的,使用起來也很方便,但是如果我們要多個View
相互組合再加上各種動畫,可想而知代碼量會有多少了呀潭。下面我們就用屬性動畫來寫一個下面這張圖里的動畫:
這張圖里包含了:1.文字顏色的漸變以及背景的漸變,然后同時又textView
放大動畫,和兩張圖片的下落動畫,第一組動畫結(jié)束后,圓形的圖片開始旋轉(zhuǎn),然后textView
不斷的顯示進(jìn)度.這就是所有動畫,下面是我們實(shí)現(xiàn)的代碼:
ObjectAnimator mountainTransY = ObjectAnimator.ofFloat(mountain, "translationY", - dip2px(500), 0);
ObjectAnimator mountainAlpha = ObjectAnimator.ofFloat(mountain, "alpha", 0, 1);
ObjectAnimator imageTransY = ObjectAnimator.ofFloat(image, "translationY", - dip2px(500), 0);
ObjectAnimator imageAlpha = ObjectAnimator.ofFloat(image, "alpha", 0, 1);
ObjectAnimator percentScaleX = ObjectAnimator.ofFloat(percent, "scaleX", 0, 1);
ObjectAnimator percentScaleY = ObjectAnimator.ofFloat(percent, "scaleY", 0, 1);
ObjectAnimator textColorAnimator = ObjectAnimator.ofInt(text, "textColor", Color.BLACK, Color.WHITE, Color.RED);
textColorAnimator.setEvaluator(new ArgbEvaluator());
ObjectAnimator textBackgroundAnimator = ObjectAnimator.ofInt(text, "backgroundColor", Color.WHITE, Color.BLACK, Color.YELLOW);
textBackgroundAnimator.setEvaluator(new ArgbEvaluator());
ObjectAnimator imageRotation = ObjectAnimator.ofFloat(image, "rotation", 0, 360);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
percent.setText(String.format(Locale.US, "%.02f%%", animation.getAnimatedValue()));
}
});
AnimatorSet firstSet = new AnimatorSet();
firstSet.playTogether(mountainTransY, mountainAlpha, imageTransY, imageAlpha, percentScaleX,
percentScaleY, textColorAnimator, textBackgroundAnimator);
firstSet.setInterpolator(new AccelerateDecelerateInterpolator());
firstSet.setDuration(5000);
final AnimatorSet secondSet = new AnimatorSet();
secondSet.playTogether(imageRotation, valueAnimator);
secondSet.setDuration(5000);
firstSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
secondSet.start();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
firstSet.start();
上面就是實(shí)現(xiàn)這個效果的所有代碼.借用岳云鵬的一句話就是:"我的天哪"(請腦補(bǔ)配音)钉迷。這么一大坨代碼。钠署。我整整寫了十幾分鐘糠聪。。而且從這么多代碼來看上去,如果以后需要調(diào)整動畫的話,無論如何也得先整個看一遍才能找到怎么調(diào)節(jié)谐鼎。這樣維護(hù)成本就增加了舰蟆。那么如何解決這個問題呢?這就要用到我們今天要介紹的主角ViewAnimator
,下面是用ViewAnimator
來實(shí)現(xiàn)相同動畫的代碼:
ViewAnimator.animate(mountain, image)
.dp().translationY(-500, 0)
.alpha(0, 1)
.andAnimate(percent)
.scale(0, 1)
.andAnimate(text)
.textColor(Color.BLACK, Color.WHITE)
.backgroundColor(Color.WHITE, Color.BLACK)
.waitForHeight()
.interpolator(new AccelerateDecelerateInterpolator())
.duration(2000)
.thenAnimate(percent)
.custom(new AnimationListener.Update<TextView>() {
@Override
public void update(TextView view, float value) {
view.setText(String.format(Locale.US, "%.02f%%", value));
}
}, 0, 1)
.andAnimate(image)
.rotation(0, 360)
.duration(2000)
.start();
真是又簡潔又易讀。簡直"完美"(請?jiān)倌X補(bǔ)配音)狸棍∩砗Γ可以看到從上到下,我們需要先通過animate(View... view)
方法將我們要進(jìn)行動畫的View
傳入,然后通過建造者模式調(diào)用我們需要做的動畫,方法名代表我們需要動畫的屬性,方法參數(shù)里直接傳入數(shù)值即可。andAnimate(View... view)
表示同時做該view
的動畫但是具體的動畫可以不一樣草戈。然后通過thenAnimate(View... view)
方法就可以表示前面的動畫執(zhí)行完畢后再執(zhí)行的動畫.具體每個方法代表的意思也很清楚就是我們需要操作的屬性的意思塌鸯。所以整體來看代碼簡潔易讀又好維護(hù)。
此外ViewAnimator
還封裝了不少動畫組合讓我們拿來即用,例如:standUp()
,wave()
,shake()
等等動畫唐片。此外還支持Path
以及SVG Path
動畫.更多的使用方法可以參照ViewAnimator
的README.md丙猬。下面我們就具體來看看如此好用的ViewAnimator
是如何實(shí)現(xiàn)的。
3.類關(guān)系圖
從類圖上來看
ViewAnimator
的結(jié)構(gòu)也很簡單明了,ViewAnimator
和AnimationBuilder
雙向關(guān)聯(lián).AnimationListener.Start
和AnimationListener.Stop
兩個接口是單獨(dú)定義出來,分別用來在動畫開始和結(jié)束時的回調(diào)牵触。下面我們就來看看具體是如何實(shí)現(xiàn)的:
4.源碼分析
ViewAnimator
的實(shí)現(xiàn)并不復(fù)雜,我相信大家都應(yīng)該能看懂,但是作者的實(shí)現(xiàn)思路非常值得我們學(xué)習(xí),所以我們還是按照我們一直以來的方式來看,根據(jù)我們的使用方法,來分析ViewAnimator
的調(diào)用流程來看具體的實(shí)現(xiàn)淮悼。
由于ViewAnimator
類和AnimationBuilder
是相互調(diào)用,所以我這里為了防止理解錯誤,在我們看到的執(zhí)行的方法都寫在了對應(yīng)的類里,并省略了其他方法。
1.ViewAnimator.animate(mountain, image);的實(shí)現(xiàn):
public class ViewAnimator {
...
private List<AnimationBuilder> animationList = new ArrayList<>();
public static AnimationBuilder animate(View... view) {
//創(chuàng)建一個ViewAnimator對象.
ViewAnimator viewAnimator = new ViewAnimator();
//通過addAnimationBuilder方法返回一個AnimationBuilder對象
return viewAnimator.addAnimationBuilder(view);
}
public AnimationBuilder addAnimationBuilder(View... views) {
//創(chuàng)建一個animationBuilder對象并添加到animationList中去
AnimationBuilder animationBuilder = new AnimationBuilder(this, views);
animationList.add(animationBuilder);
return animationBuilder;
}
...
}
public class AnimationBuilder {
...
private final ViewAnimator viewAnimator;
private final View[] views;
public AnimationBuilder(ViewAnimator viewAnimator, View... views) {
//分別賦值viewAnimator和views
this.viewAnimator = viewAnimator;
this.views = views;
}
...
}
可以看到這一步初始化了一個ViewAnimator
對象和一個AnimationBuilder
,并將AnimationBuilder
對象保存在了ViewAnimator
的animationList
數(shù)組里,Views
則保存在了AnimationBuilder
對象里.
2.dp().translationY(-500, 0).alpha(0, 1);的實(shí)現(xiàn):
由于返回了一個AnimationBuilder
對象,所以dp()
方法肯定在AnimationBuilder
里實(shí)現(xiàn):
public class AnimationBuilder {
...
public AnimationBuilder dp() {
//標(biāo)記nextValueWillBeDp
nextValueWillBeDp = true;
return this;
}
public AnimationBuilder translationY(float... y) {
return property("translationY", y);
}
public AnimationBuilder alpha(float... alpha) {
return property("alpha", alpha);
}
public AnimationBuilder property(String propertyName, float... values) {
//遍歷views中的所有view,依次實(shí)例化ObjectAnimator對象
//并添加到AnimationBuilder的animatorList對象中.
for (View view : views) {
this.animatorList.add(ObjectAnimator.ofFloat(view, propertyName, getValues(values)));
}
return this;
}
...
}
先是標(biāo)記了nextValueWillBeDp
,然后translationY(float... y)
和alpha(float... alpha)
方法都是調(diào)用了property(String propertyName, float... values)
方法,然后在這個方法里去實(shí)例化對應(yīng)的ObjectAnimator
對象,這樣就避免了我們重復(fù)寫很多創(chuàng)建ObjectAnimator
對象的代碼了,所以我們類似的操作下面這些屬性時都會調(diào)用這個方法:
- translationY
- translationX
- alpha
- scaleX
- scaleY
- rotationX
- rotationY
- rotation
3.andAnimate(text).textColor(Color.BLACK, Color.WHITE).backgroundColor(Color.WHITE, Color.BLACK)的實(shí)現(xiàn):
public class AnimationBuilder {
...
public AnimationBuilder andAnimate(View... views) {
return viewAnimator.addAnimationBuilder(views);
}
...
}
public class ViewAnimator {
...
public AnimationBuilder addAnimationBuilder(View... views) {
//創(chuàng)建一個animationBuilder對象并添加到animationList中去
AnimationBuilder animationBuilder = new AnimationBuilder(this, views);
animationList.add(animationBuilder);
return animationBuilder;
}
...
}
注意這里是先在AnimationBuilder
拿viewAnimator
初始化了一個新的AnimationBuilder
對象并返回了,當(dāng)然也同樣添加進(jìn)了animationList
,所以下面的textColor(Color.BLACK, Color.WHITE).backgroundColor(Color.WHITE, Color.BLACK)
就會實(shí)例化text
對應(yīng)的ObjectAnimator
對象了,代碼這里我們就不貼了,我們繼續(xù)往下看揽思。
4.waitForHeight().interpolator(new Interpolator()).duration(2000);方法的實(shí)現(xiàn)
public class AnimationBuilder {
...
public AnimationBuilder waitForHeight() {
//waitForHeight表示當(dāng)View開始繪制的時候再開始動畫.
waitForHeight = true;
return this;
}
public AnimationBuilder interpolator(Interpolator interpolator) {
//賦值插值器,直接賦值給了viewAnimator中的interpolator對象
viewAnimator.interpolator(interpolator);
return this;
}
public AnimationBuilder duration(long duration) {
//設(shè)定動畫持續(xù)時間,也是直接賦值給了viewAnimator的duration對象
viewAnimator.duration(duration);
return this;
}
...
}
這些都很簡單,我們繼續(xù)往下看thenAnimate(percent).custom(...);
方法袜腥。
5.thenAnimate(percent).custom(...);方法的實(shí)現(xiàn)
public class AnimationBuilder {
...
public AnimationBuilder thenAnimate(View... views) {
//直接調(diào)用viewAnimator的thenAnimate()方法
return viewAnimator.thenAnimate(views);
}
...
}
public class ViewAnimator {
...
public AnimationBuilder thenAnimate(View... views) {
//再創(chuàng)建一個nextViewAnimator對象
ViewAnimator nextViewAnimator = new ViewAnimator();
//將nextViewAnimator賦值給當(dāng)前ViewAnimator對象的next.
this.next = nextViewAnimator;
nextViewAnimator.prev = this;
//return一個nextViewAnimator創(chuàng)建的AnimationBuilder對象
return nextViewAnimator.addAnimationBuilder(views);
}
...
}
可以看到這里是又創(chuàng)建了一個新的ViewAnimator
對象和一個新的AnimationBuilder
對象,注意這里的next
和prev
賦值,其實(shí)就是數(shù)據(jù)結(jié)構(gòu)中雙向鏈表的思想,這里我們在動畫開始的時候就可以根據(jù)prev
來找到最早的ViewAnimator
對象,然后再用next
就可以將動畫順序執(zhí)行了。
5.custom(...);方法的實(shí)現(xiàn)
在使用方法里我們是這樣調(diào)用的:
.custom(new AnimationListener.Update<TextView>() {
@Override
public void update(TextView view, float value) {
view.setText(String.format(Locale.US, "%.02f%%", value));
}
}, 0, 1)
這樣就實(shí)現(xiàn)了textView
從0-1的進(jìn)度顯示.原理如下:
public class AnimationListener {
...
public interface Update<V extends View>{
void update(V view, float value);
}
...
}
public class AnimationBuilder {
...
public AnimationBuilder custom(final AnimationListener.Update update, float... values) {
//遍歷所有view,實(shí)例化valueAnimator,并在onAnimationUpdate()回調(diào)接口里,回調(diào)
//update接口,最后把valueAnimator添加到animatorList中去
for (final View view : views) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(getValues(values));
if (update != null)
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//noinspection unchecked
update.update(view, (Float) animation.getAnimatedValue());
}
});
add(valueAnimator);
}
return this;
}
...
}
我們可以看到update
接口里可以傳入任何View
的子類,但是其實(shí)在AnimationBuilder
里會遍歷所有當(dāng)前動畫View
的并全部添加了這個valueAnimator
,這樣做有一個問題就是,雖然作者想傳入泛型是想不在回調(diào)方法里強(qiáng)制轉(zhuǎn)換從而直接做操作,但是這樣做是不安全的,如果現(xiàn)在我把thenAnimate(percent).custom(...);
方法寫成thenAnimate(percent, image).custom(...);
運(yùn)行時立馬會報ClassCastException
:
java.lang.ClassCastException: android.support.v7.widget.AppCompatImageView cannot be cast to android.widget.TextView
因?yàn)橛捎诿總€做動畫的view
都添加了這個回調(diào),再回調(diào)處理的時候又會直接當(dāng)成TextView
來處理所以會崩潰。所以我們在使用的時候一定要注意這個羹令。當(dāng)然在下文的個人評價中我也會給出解決辦法,這里我們知道就好了鲤屡。
下面的調(diào)用很相似我們就不看了,我們直接來看start()
方法.
5.start();方法的實(shí)現(xiàn)
首先是在AnimationBuilder
中直接調(diào)用了ViewAnimator
的start()
方法:
public void start() {
viewAnimator.start();
}
再來看ViewAnimator
的start()
方法:
public class ViewAnimator {
...
public ViewAnimator start() {
if (prev != null) {
//如果有上一個ViewAnimator則先調(diào)用上一個的start()方法
prev.start();
} else {
//創(chuàng)建AnimatorSet對象
animatorSet = createAnimatorSet();
//如果需要等待view繪制則監(jiān)聽onPreDraw()方法,
if (waitForThisViewHeight != null) {
waitForThisViewHeight.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
animatorSet.start();
waitForThisViewHeight.getViewTreeObserver().removeOnPreDrawListener(this);
return false;
}
});
} else {
//直接開始
animatorSet.start();
}
}
return this;
}
protected AnimatorSet createAnimatorSet() {
//新建一個animators列表
List<Animator> animators = new ArrayList<>();
//將所有的animationBuilder對象中的Animator對象添加
for (AnimationBuilder animationBuilder : animationList) {
animators.addAll(animationBuilder.createAnimators());
}
//如果標(biāo)記了waitForHeight,
//則返回animationBuilder里View數(shù)組的第一個view
for (AnimationBuilder animationBuilder : animationList) {
if (animationBuilder.isWaitForHeight()) {
waitForThisViewHeight = animationBuilder.getView();
break;
}
}
//如果有ValueAnimator 則單獨(dú)設(shè)置重復(fù)模式和重復(fù)次數(shù)
for (Animator animator : animators) {
if (animator instanceof ValueAnimator) {
ValueAnimator valueAnimator = (ValueAnimator) animator;
valueAnimator.setRepeatCount(repeatCount);
valueAnimator.setRepeatMode(repeatMode);
}
}
//設(shè)置AnimatorSet的參數(shù)
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animators);
animatorSet.setDuration(duration);
animatorSet.setStartDelay(startDelay);
if (interpolator != null)
animatorSet.setInterpolator(interpolator);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
//回調(diào)Start接口
if (startListener != null) startListener.onStart();
}
@Override
public void onAnimationEnd(Animator animation) {
//回調(diào)Stop接口
if (stopListener != null) stopListener.onStop();
//如果有下一個ViewAnimator則繼續(xù)執(zhí)行
if (next != null) {
next.prev = null;
next.start();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
return animatorSet;
}
...
}
從上到下應(yīng)該很清晰的看出,其實(shí)就是在內(nèi)部創(chuàng)建了AnimatorSet
對象,然后設(shè)置一些參數(shù),最后執(zhí)行,然后再在onAnimationEnd(Animator animation)
的接口里檢查是否還有動畫,從而一直鏈?zhǔn)降膱?zhí)行。
上面就是整個的ViewAnimator
主要的實(shí)現(xiàn)了,雖然看上去并不難,但是也不是很容易就能寫出來的福侈。值得我們好好學(xué)習(xí)酒来。
5.個人評價
最近在我負(fù)責(zé)的項(xiàng)目里,我們設(shè)計了大量的組合動畫與交互,如果使用原生的方法會使代碼量特別大,而且相當(dāng)難維護(hù),因此我使用了ViewAnimator
簡化了大量的動畫代碼,而且使代碼更易讀,可維護(hù)性就更好了。如果你的項(xiàng)目里也有比較多的動畫,強(qiáng)烈推薦ViewAnimator
而且這個庫并沒有幾個類,占用的體積非常小,推薦成為項(xiàng)目標(biāo)配肪凛。
最后還有兩點(diǎn)要說的問題就是:
1.AnimationListener.Update的問題
這個問題我們在上面已經(jīng)說過了,使用不當(dāng)會造成崩潰,而且我們把泛型作為參數(shù)傳入之后,我覺得意義并不大,完全可以在回調(diào)接口里直接根據(jù)value
直接操作我們的View
,但是由于ViewAnimator
里還有其他地方依賴Update
接口,所以我把AnimationListener.Update
修改成了下面這樣堰汉。經(jīng)測試使用完全沒有問題。
public class AnimationListener {
public interface Update {
void update(View view, float value);
}
}
2.為單獨(dú)的AnimationBuilder添加Interpolator的問題.
在使用中發(fā)現(xiàn)如果我同時組合了好幾個動畫之后,只能為這些同時動畫的屬性添加同一個Interpolator
這樣不滿足我想同時動畫多個View
但又要不同的Interpolator
需求.所以我就在ViewAnimator
的基礎(chǔ)上添加了單個Interpolator
的功能,而且給ViewAnimator
的作者發(fā)了pull request
.詳細(xì)原理我就不講了,比較簡單,大家可以在我fork
的分支上查看具體的實(shí)現(xiàn)方法:
Commit地址在這. 好了今天就寫到這吧伟墙。
我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達(dá)達(dá)達(dá)達(dá)sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.謝謝關(guān)注_,說不定什么時候會有福利哈.