編輯:關於Android編程
NineoldAndroids是Github上一個著名的動畫庫,簡單來說,NineOldAndroids是一個向下兼容的動畫庫,主要是使低於API 11的系統也能夠使用View的屬性動畫。
網上已經有一些文章,介紹了這個庫的設計,包括類結構和思想,例如
NineOldAnimations 源碼解析
NineoldAndroids動畫庫源碼分析
上面兩篇文章都比較詳細的介紹了NineoldAndroids的源碼,可以說為大家看源碼帶來很大的方便。
那為什麼我還要寫這篇文章呢?
我們來看NineoldAndroids的類結構圖:
因為NineoldAndroids的類結構比較復雜,即使單純看上面兩篇文章,也可能把人搞糊塗。
本篇文章將剝離NineoldAndroids的具體細節,嘗試只是顯示其核心功能,也就是說寫出一個簡易的NineoldAndroids,並且在這個過程當中,了解Android實現動畫的原理和思想。
一理通百理明,與君共勉。
現在我們先來看看該動畫庫的使用:
public class MainActivity extends Activity {
TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.mtext);
//Value動畫,設置目標值為3000
CValueAnimator valueAnimator = CValueAnimator.ofInt(1000,2000,3000);
//設置動畫時間
valueAnimator.setDuration(4000);
valueAnimator.addUpdateListener(new CValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(CValueAnimator animation) {
//將動畫值,更新到textView
mTextView.setText(animation.getAnimatedValue() + );
mTextView.setTranslationY((Integer) animation.getAnimatedValue());
mTextView.invalidate();
}
});
//啟動動畫
valueAnimator.start();
}
}
使用方式和NineoldAndroids完全一樣,也和Android原生的方式一樣,對於使用過動畫效果的朋友來說,應該非常簡單。
首先來看類設計圖,這圖相比原來的NineoldAndroids,做了很多精簡,只是希望大家更加容易看懂NineoldAndroids的本質。
在進行下一步的分析之前,我們先來了解一下一些核心的類以及它們的作用。
CValueAnimator : 該類是 Animator 的子類,實現了動畫的整個處理邏輯,也是最為核心的類;TimeInterpolator : 時間插值器,它的作用是根據時間流逝的百分比來計算出當前屬性值改變的百分比,系統預置的有 LinearInterpolator(線性插值器:勻速動畫)、AccelerateDecelerateInterpolator(加速減速插值器:動畫兩頭慢中間快)和 DecelerateInterpolator(減速插值器:動畫越來越慢)等;CTypeEvaluator : TypeEvaluator 的中文翻譯為類型估值算法,它的作用是根據當前屬性改變的百分比來計算改變後的屬性值,系統預置的有 IntEvaluator(針對整型屬性)、FloatEvaluator(針對浮點型屬性);CPropertyValuesHolder : PropertyValuesHolder 是持有目標屬性 Property、setter 和 getter 方法、以及 KeyFrameSet 的類;CKeyFrame : 一個 keyframe 對象由一對 time / value 的鍵值對組成,可以為動畫定義某一特定時間的特定狀態,Animator 傳入的一個個參數映射為一個個 keyframe,存儲相應的動畫的觸發時間和屬性值;CKeyFrameSet : 存儲一個動畫的關鍵幀集合;在調用start()方法之前,我們使用
CValueAnimator valueAnimator = CValueAnimator.ofInt(1000,2000,3000);
做了動畫的初始化工作,那麼我們具體做了上面呢?
簡單而言,就是把傳入的屬性值,例如例子中是1000,2000,3000,封裝成關鍵幀對象CKeyFrame。
所謂關鍵幀,就是在動畫過程中一定要出現的幀。
我們知道,所謂動畫也不可能是完全連續的,肯定會有一些間隔,只是間隔小於人眼視覺暫留時間,所以看起來就是連續的了。
所以從1000-2000這個過程,也不可能是完全連續的,也許是1000,1100,…1900,2000
其中一些幀就被丟失了,絕對不能丟失的幀,稱為關鍵幀。
關鍵幀保留兩個屬性,一個是該幀所在的時間(其實是一個百分比),一個是幀值。
public abstract class CKeyframe implements Cloneable {
/**
* 時間
*/
float mFraction;
/**
* 屬性值類型
*/
Class mValueType;
/**
* 插值器
*/
private /*Time*/Interpolator mInterpolator = null;
public static CKeyframe ofInt(float fraction, int value) {
return new IntCKeyframe(fraction, value);
}
public static CKeyframe ofInt(float fraction) {
return new IntCKeyframe(fraction);
}
public abstract Object getValue();
/**
* INT類型值得關鍵幀
*/
public static class IntCKeyframe extends CKeyframe {
/**
* 關鍵幀的值
*/
int mValue;
IntCKeyframe(float fraction, int value) {
mFraction = fraction;
mValue = value;
mValueType = int.class;
}
IntCKeyframe(float fraction){
mFraction = fraction;
mValueType = int.class;
}
public int getIntValue() {
return mValue;
}
public void setValue(Object value) {
if (value != null && value.getClass() == Integer.class) {
mValue = ((Integer)value).intValue();
}
}
@Override
public Object getValue() {
return mValue;
}
}
public float getFraction() {
return mFraction;
}
public /*Time*/Interpolator getInterpolator() {
return mInterpolator;
}
}
我們生成關鍵幀對象以後,將關鍵幀存入一個集合,稱為關鍵幀集合,也就是CKeyFrameSet類。
CKeyFrameSet類中有一個CTypeEvaluator成員對象,這對象可以通過當前動畫進行的百分比,計算出兩個關鍵幀之間的值。
public interface CTypeEvaluator {
public T evaluate(float fraction, T startValue, T endValue);
}
public class IntCEvaluator implements CTypeEvaluator {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
可以看到,IntCEvaluator其實就是一個線性計算,fraction是百分比,startValue是起始值,endValue是目標值,不同的fraction會產生不同的結果。
顯然對於兩個關鍵幀來說,前一個關鍵幀的值,就是起始值,後一個關鍵幀的值,就是目標值。
在CKeyFrameSet中是這樣調用這個方法:
public int getIntValue(float fraction) {
if (mNumKeyframes == 2) {//只有兩個關鍵幀的情況
if (firstTime) {
firstTime = false;
firstValue = ((CKeyframe.IntCKeyframe) mKeyframes.get(0)).getIntValue();
lastValue = ((CKeyframe.IntCKeyframe) mKeyframes.get(1)).getIntValue();
deltaValue = lastValue - firstValue;
}
if (mInterpolator != null) {
fraction = mInterpolator.getInterpolation(fraction);
}
if (mEvaluator == null) {
return firstValue + (int)(fraction * deltaValue);
} else {
return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue();
}
}
....
CKeyframe.IntCKeyframe prevKeyframe = (CKeyframe.IntCKeyframe) mKeyframes.get(0);
for (int i = 1; i < mNumKeyframes; ++i) {//多個關鍵幀
CKeyframe.IntCKeyframe nextKeyframe = (CKeyframe.IntCKeyframe) mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
float intervalFraction = (fraction - prevKeyframe.getFraction()) /
(nextKeyframe.getFraction() - prevKeyframe.getFraction());
int prevValue = prevKeyframe.getIntValue();
int nextValue = nextKeyframe.getIntValue();
return mEvaluator == null ?
prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
intValue();
}
prevKeyframe = nextKeyframe;
}
// shouldn't get here
return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue();
}
方法有點長,大家可以看只有兩個關鍵幀的情況是怎麼計算的,就比較簡單,其實就是線性計算。
問題是,難道每次我們都希望1000-2000直接是線性增長的嗎?如果我希望先快後慢呢?
了解插值器的朋友,應該就明白,這個功能我們可以通過Interpolator去做,但是Interpolator改變的是fraction的增長速度,也就是加速度(勉強可以這樣理解)。
從而實現非線性效果,所以顯然CKeyframeSet要持有一個Interpolator對象。
顧名思義,CPropertyValuesHolder就是持有屬性和值得一個類。
CPropertyValuesHolder是動畫庫中的一個核心類,但是在本簡易庫削減了其功能,因為我們只需要實現值得變化,沒有針對具體的屬性,例如scale,rotate等,所以不需要提供View屬性修改的方法。
其實這個類,是為ObjectAnimator做了比較大的准備,但是本篇文章不涉及。
我們關注的是,這個類持有CKeyFrameSet。
public class CPropertyValuesHolder implements Cloneable {
/**
* 屬性名稱
*/
String mPropertyName;
/**
* 關鍵幀集合
*/
CKeyframeSet mKeyframeSet = null;
private static final CTypeEvaluator sIntEvaluator = new IntCEvaluator();
private static final CTypeEvaluator sFloatEvaluator = new FloatCEvaluator();
private CTypeEvaluator mEvaluator = sIntEvaluator;
/**
* 屬性值
*/
private Object mAnimatedValue;
/**
* 屬性值類型
*/
Class mValueType;
private CPropertyValuesHolder(String propertyName) {
mPropertyName = propertyName;
}
/**
* 返回一個屬性值類型為int的CPropertyValuesHolder
* @param propertyName
* @param values
* @return
*/
public static CPropertyValuesHolder ofInt(String propertyName, int... values) {
return new IntCPropertyValuesHolder(propertyName, values);
}
/**
* 屬性值類型為int的CPropertyValuesHolder
*/
static class IntCPropertyValuesHolder extends CPropertyValuesHolder {
IntCKeyframeSet mIntKeyframeSet;
public IntCPropertyValuesHolder(String propertyName, int... values) {
super(propertyName);
setIntValues(values);
}
@Override
public void setIntValues(int... values) {
super.setIntValues(values);
mIntKeyframeSet = (IntCKeyframeSet) mKeyframeSet;
}
}
/**
* 返回當前屬性值
* @return
*/
Object getAnimatedValue() {
return mAnimatedValue;
}
/**
* 設置屬性值類型,這裡具體到int類型
* 對於每個Int類型值,例如100,200,1000
* 生成一個CKeyFrame關鍵幀對象,並且將這些對象包裝成一個set集合
* @param values
*/
public void setIntValues(int... values) {
mValueType = int.class;
mKeyframeSet = CKeyframeSet.ofInt(values);
}
void init() {
if (mEvaluator == null) {
// We already handle int and float automatically, but not their Object
// equivalents
mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
(mValueType == Float.class) ? sFloatEvaluator :
null;
}
if (mEvaluator != null) {
// KeyframeSet knows how to evaluate the common types - only give it a custom
// evaluator if one has been set on this class
mKeyframeSet.setEvaluator(mEvaluator);
}
}
/**
* 讓CKeyframeSet通過關鍵幀計算屬性值
* @param fraction
*/
void calculateValue(float fraction) {
mAnimatedValue = mKeyframeSet.getValue(fraction);
}
}
/**
* CPropertyValuesHolder是一個包裝類
* 可以看做是,需要動畫的屬性或者值的對象實例
*/
CPropertyValuesHolder[] mValues;
/**
* 屬性值類型為int的動畫
* @param values
* @return
*/
public static CValueAnimator ofInt(int... values) {
CValueAnimator anim = new CValueAnimator();
anim.setIntValues(values);
return anim;
}
/**
* 根據一系列屬性值,生成CPropertyValuesHolder
* CPropertyValuesHolder這個類的意義就是,持有某個屬性的一系列值,
* 例如scale(縮放屬性),其若干個值為100,200,1000等。
* 也就是說在規定時間內,scale的值會從100增長到1000
* @param values
*/
public void setIntValues(int... values) {
if (values == null || values.length == 0) {
return;
}
if (mValues == null || mValues.length == 0) {
//屬性名稱為,說明只是數值改變,和具體屬性無關
setValues(new CPropertyValuesHolder[]{CPropertyValuesHolder.ofInt(, values)});
} else {
CPropertyValuesHolder valuesHolder = mValues[0];
valuesHolder.setIntValues(values);
}
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}
public void setValues(CPropertyValuesHolder... values) {
mValues = values;
mInitialized = false;
}
原理就是根據1000,2000,3000,生成了一個CPropertyValuesHolder對象,並且將它保存了起來。
目前萬事俱備,只等調用start()方法開始動畫了。
直接來看簡化過後的start()方法
public void start() {
if (Looper.myLooper() == null) {//當前線程必須調用了Looper.loop()方法
throw new AndroidRuntimeException(Animators may only be run on Looper threads);
}
mPlayingState = STOPPED;
sPendingAnimations.get().add(this);
/**
* 初始化動畫時間,也就是設置起始運行時間為0
* 並且計算起始屬性值值
*/
setCurrentPlayTime(getCurrentPlayTime());
mPlayingState = STOPPED;
mRunning = true;
//通知監聽器,動畫開始
if (mListeners != null) {
ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationStart(this);
}
}
//Handler,通過自己給自己發送消息,實現不斷進行動畫
AnimationHandler animationHandler = sAnimationHandler.get();
if (animationHandler == null) {
animationHandler = new AnimationHandler();
sAnimationHandler.set(animationHandler);
}
animationHandler.sendEmptyMessage(ANIMATION_START);
}
動畫過程我們可以這樣想:
1、獲取當前時間為startTime,即動畫起始時間,並且初始化動畫狀態,例如1000,2000,3000,那麼setCurrentPlayTime()方法的其中一個工作就是初始化狀態為1000
2、通知動畫開始監聽器,動畫開始
3、使用AnimationHandler實現循環,首先給AnimationHandler發送了一條ANIMATION_START信息
顯然,主要工作就是在AnimationHandler裡面進行的
/**
* 該handler用於處理兩個消息
* ANIMATION_START也就是動畫開始
* ANIMATION_FRAME也就是運行某一幀
*/
private static class AnimationHandler extends Handler {
@Override
public void handleMessage(Message msg) {
boolean callAgain = true;
//當前運行動畫隊列
ArrayList animations = sAnimations.get();
switch (msg.what) {
case ANIMATION_START:
//當前等候動畫隊列
ArrayList pendingAnimations = sPendingAnimations.get();
if (animations.size() > 0) {
callAgain = false;
}
while (pendingAnimations.size() > 0) {//如果等候的動畫大於0
ArrayList pendingCopy =
(ArrayList) pendingAnimations.clone();
pendingAnimations.clear();
int count = pendingCopy.size();
for (int i = 0; i < count; ++i) {
CValueAnimator anim = pendingCopy.get(i);
// If the animation has a startDelay, place it on the delayed list
anim.startAnimation();//啟動這些動畫
}
}
case ANIMATION_FRAME:
//當前時間
long currentTime = AnimationUtils.currentAnimationTimeMillis();
//當前已經結束的動畫隊列
ArrayList endingAnims = sEndingAnims.get();
//正在運行的對話數量
int numAnims = animations.size();
int i = 0;
while (i < numAnims) {
CValueAnimator anim = animations.get(i);
if (anim.animationFrame(currentTime)) {//更新每個運行動畫的數值,如果已經結束,加入endingAnims對象
endingAnims.add(anim);
}
if (animations.size() == numAnims) {
++i;
} else {
//在動畫運行過程中,可能有些動畫被取消
--numAnims;
endingAnims.remove(anim);
}
}
if (endingAnims.size() > 0) {
for (i = 0; i < endingAnims.size(); ++i) {
endingAnims.get(i).endAnimation();
}
endingAnims.clear();
}
// If there are still active or delayed animations, call the handler again
// after the frameDelay
//如果還有活動的動畫,在默認每幀間隔時間以後,再次調用,更新屬性值
if (callAgain && (!animations.isEmpty())) {
sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -
(AnimationUtils.currentAnimationTimeMillis() - currentTime)));
}
break;
}
}
}
這個類做了這些工作:
其實就是調用了等待隊列pendingAnimations中CValueAnimator對象的startAnimation()方法
/**
* 啟動動畫
*/
private void startAnimation() {
//初始化動畫
initAnimation();
//將等候隊列中的動畫,加入運行對象
sAnimations.get().add(this);
}
在該方法中,初始化了動畫(其實這裡調用initAnimation()沒有實際作用,因為之前已經初始化過了);
然後就是將動畫放入運行隊列。
我們注意到ANIMATION_START狀態以後,並沒有使用break,所以會接著執行ANIMATION_FRAME
對每個運行動畫,調用其了animationFrame()方法
/**
* 根據當前時間,計算運行百分比,然後調用animateValue更新當前屬性值
* @param currentTime
* @return
*/
boolean animationFrame(long currentTime) {
boolean done = false;
if (mPlayingState == STOPPED) {
mPlayingState = RUNNING;
if (mSeekTime < 0) {
mStartTime = currentTime;
} else {
mStartTime = currentTime - mSeekTime;
// Now that we're playing, reset the seek time
mSeekTime = -1;
}
}
switch (mPlayingState) {
case RUNNING:
case SEEKED:
float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
if (fraction >= 1f) {//百分比大於1,結束動畫
done = true;
fraction = Math.min(fraction, 1.0f);
}
animateValue(fraction);
break;
}
return done;
}
這個方法會根據當前時間,判斷動畫是否已經結束,如果是,返回true,這些動畫就會進入sEndingAnims隊列,做最後的結束通知工作。
否則,其實就是調用了自己的CPropertyValuesHolder計算當前屬性值
/**
* 根據百分比,更新屬性值
* @param fraction
*/
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);//讓CPropertyValuesHolder計算屬性值
}
//通知監聽器,屬性值更新
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
走完兩個case,新的屬性值就計算出來了,我們通過getAnimatedValue()就可以拿到
/**
* 獲取當前屬性值
* @return
*/
public Object getAnimatedValue() {
if (mValues != null && mValues.length > 0) {
return mValues[0].getAnimatedValue();
}
// Shouldn't get here; should always have values unless ValueAnimator was set up wrong
return null;
}
那麼怎麼讓動畫繼續計算下一個屬性值呢?
注意最後
// If there are still active or delayed animations, call the handler again
// after the frameDelay
//如果還有活動的動畫,在默認每幀間隔時間以後,再次調用,更新屬性值
if (callAgain && (!animations.isEmpty())) {
sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -
(AnimationUtils.currentAnimationTimeMillis() - currentTime)));
}
也就是在規定的幀間隔以後,AnimationHandler給自己再次發送一個ANIMATION_FRAME消息,進行下一次屬性值的計算。
最後,如上面所說,animationFrame()返回true,才真正結束動畫!
1 背景前面分析那麼多系統源碼了,也該暫停下來休息一下,趁昨晚閒著看見一個有意思的需求就操練一下分析源碼後的實例演練—-自定義控件。這個實例很適合
現在市面上的很多的應用,都帶有下拉列表的功能,將所有選項都放在下拉列表中,當用戶點擊選擇的時候,彈出所有的選項,用戶選擇一項後,下拉列表自動隱藏,很多下拉列表都是用Lis
Galaxy S7 俨然已經成為目前最受關注的智能手機之一,許多人還將其稱作是目前最佳的智能手機。三星在這部手機身上帶回了對於 microSD 卡的支持,但
上圖Tab的背景效果,和帶陰影的圓角矩形,是怎麼實現的呢?大部分的人會讓美工切圖,用點九圖做背景。但是,如果只提供一張圖,會怎麼樣呢?比如,中間的Tab背景紅色底線的像素