Android 補間動畫原理
這段時間專案中用到了動畫,所以趁熱打鐵,看看動畫原理
補間動畫
使用舉例
TranslateAnimation translateAnim = new TranslateAnimation(0, 100, 0, 100);
translateAnim.setDuration(1000);
translateAnim.setFillAfter(true);
testBut.startAnimation(translateAnim)
原始碼分析
invalidate(true);public void startAnimation(Animation animation) { animation.setStartTime(Animation.START_ON_FIRST_FRAME); setAnimation(animation); invalidateParentCaches(); invalidate(true); } protected void invalidateParentCaches() { if (mParent instanceof View) { ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED; } }
invalidateParentCaches();
方法中可以看到為當前該view的parent,就是所在的viewgroup的標誌為設定了PFLAG_INVALIDATED。所以viewgroup發生了重繪,這裡為什麼會這樣值得深入研究進一步分析?
invalidateParentCaches();
invalidate(true);//這樣很明顯只是導致了該view的重繪
為什麼這樣導致了view所在viewgroup的重繪
首先呼叫
所以可以得出結論,如果對viewgroup下的任何一個view執行動畫,那麼都會導致view執行整個繪製流程,不相信的話,可以自定義一個viewgroup然後重寫public void draw(Canvas canvas) { // Step 1, draw the background, if needed drawBackground(canvas); // Step 3, draw the content onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); }
draw(canvas),onDraw(canvas),dispatchDraw(canvas)方法在裡面列印log
這裡關鍵的是步驟4,dispatchDraw(canvas);會去繪製子view
ViewGroup類中的方法
只要子view可見或者子view設定了動畫,那麼就會對該子view呼叫drawChild(canvas, child, drawingTime)protected void dispatchDraw(Canvas canvas) { ........ for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { more |= drawChild(canvas, transientChild, drawingTime); } transientIndex++; if (transientIndex >= transientCount) { transientIndex = -1; } } int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } ....... }
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
回到View中的draw帶三個引數的過載方法,注意區別於draw(canvas)過載方法
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
..........
Transformation transformToApply = null;
boolean concatMatrix = false;
final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
final Animation a = getAnimation();//首先獲取當前view的動畫
if (a != null) {
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
transformToApply = parent.getChildTransformation();//獲取的Transformation物件,包含動畫矩陣
}
..........
if (!drawingWithRenderNode || transformToApply != null) {
restoreTo = canvas.save();//儲存該canvas
}
..........
if (transformToApply != null) {
..........
if (concatMatrix) {
if (drawingWithRenderNode) {
renderNode.setAnimationMatrix(transformToApply.getMatrix());
} else {
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());//為該canvas畫布應用了該動畫矩陣
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}
..........
}
..........
if (restoreTo >= 0) {
canvas.restoreToCount(restoreTo);//恢復到之前狀態的canvas,所以並不會影響到其它子view的繪製,即使他們使用的都是viewgroup傳遞下來的畫布
}
}
Transformation物件中包含一個矩陣和 alpha 值,矩陣是用來做平移、旋轉和縮放動畫的。1. 檢視層的繪製都是共用一個畫布canvas,其實都是在最底層的decorview在viewrootimpl中建立的。
viewgroup中的子view對canvas進行操作,並不會影響到其它子view還有該viewgroup,因為可以在draw(Canvas canvas, ViewGroup parent, long drawingTime)看到
每繪製一個子view,都會先對畫布狀態進行儲存save(),然後繪製完該子view之後。又會恢復restore(),所以如果在任何一個子view的onDraw(canvas)對canvas進行操作都不會
影響到所在的viewgroup和同級的其他子view,但是如果該view是viewgroup,會影響到其所有的子view的繪製,見第二點分析
2. 如果重寫viewgroup的onDraw(canvas)方法,然後對該畫布進行translate,concat等操作,就會影響到整個子view的繪製。
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
可以看到繪製該viewgroup過程中,是先呼叫onDraw(canvas)繪製其內容,然後繪製子view,而預設的onDraw(canvas)又是一個空實現,沒有進canvas進行儲存還原的操作,
所以導致viewgroup的onDraw(canvas)方法,然後對該畫布進行translate,concat等操作,就會影響到整個子view的繪製
回到動畫上的分析來,關鍵呼叫了applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
/**
* Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
* case of an active Animation being run on the view.
*/
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
Animation a, boolean scalingRequired) {
Transformation invalidationTransform;
final int flags = parent.mGroupFlags;
final boolean initialized = a.isInitialized();
if (!initialized) {
a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
onAnimationStart();
}
final Transformation t = parent.getChildTransformation();
boolean more = a.getTransformation(drawingTime, t, 1f);
if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
if (parent.mInvalidationTransformation == null) {
parent.mInvalidationTransformation = new Transformation();
}
invalidationTransform = parent.mInvalidationTransformation;
a.getTransformation(drawingTime, invalidationTransform, 1f);
} else {
invalidationTransform = t;
}
.............
}
final Transformation t = parent.getChildTransformation();boolean more = a.getTransformation(drawingTime, t, 1f);//操作Transformation物件t
這兩行是關鍵
所以進入Animation的getTransformation方法
public boolean getTransformation(long currentTime, Transformation outTransformation) {
if (mStartTime == -1) {
mStartTime = currentTime;
}
final long startOffset = getStartOffset();
final long duration = mDuration;
float normalizedTime;
if (duration != 0) {
normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
(float) duration;
} else {
// time is a step-change with a zero duration
normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
}
final boolean expired = normalizedTime >= 1.0f;
mMore = !expired;
if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
if (!mStarted) {
fireAnimationStart();
mStarted = true;
if (USE_CLOSEGUARD) {
guard.open("cancel or detach or getTransformation");
}
}
if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if (mCycleFlip) {
normalizedTime = 1.0f - normalizedTime;
}
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);//獲取一個0-1的值
applyTransformation(interpolatedTime, outTransformation);
}
.........
return mMore;
}
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);//插值器最終返回一個0-1的值applyTransformation(interpolatedTime, outTransformation);//然後用這個0-1的值,應用到Transformation物件上去
TranslateAnimation動畫呼叫Transformation物件的setTranslate,RotateAnimation呼叫Transformation物件的setRotate。。。。
public Animation() {
ensureInterpolator();
}
protected void ensureInterpolator() {
if (mInterpolator == null) {
mInterpolator = new AccelerateDecelerateInterpolator();
}
}
public void setInterpolator(Interpolator i) {
mInterpolator = i;
}
所以,預設情況下是AccelerateDecelerateInterpolator加速減速插值器Animation中applyTransformation預設是一個空實現,interpolatedTime是一個0-1的值
protected void applyTransformation(float interpolatedTime, Transformation t) {
}
//TranslateAnimation中實現
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float dx = mFromXDelta;
float dy = mFromYDelta;
if (mFromXDelta != mToXDelta) {
dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
}
if (mFromYDelta != mToYDelta) {
dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
}
t.getMatrix().setTranslate(dx, dy);
}
//RotateAnimation中實現
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime);
float scale = getScaleFactor();
if (mPivotX == 0.0f && mPivotY == 0.0f) {
t.getMatrix().setRotate(degrees);
} else {
t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale);
}
}
其實最終目的都是操作Transformation t物件,其實這個物件就是上文中的,transformToApply物件。在繪製子view過程中canvas.concat(transformToApply.getMatrix());
自定義補間動畫
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
/**
* Creates a new 3D rotation on the Y axis. The rotation is defined by its
* start angle and its end angle. Both angles are in degrees. The rotation
* is performed around a center point on the 2D space, definied by a pair
* of X and Y coordinates, called centerX and centerY. When the animation
* starts, a translation on the Z axis (depth) is performed. The length
* of the translation can be specified, as well as whether the translation
* should be reversed in time.
*
* @param fromDegrees the start angle of the 3D rotation
* @param toDegrees the end angle of the 3D rotation
* @param centerX the X center of the 3D rotation
* @param centerY the Y center of the 3D rotation
* @param reverse true if the translation should be reversed, false otherwise
*/
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
重寫applyTransformation 函式,interpolatedTime 就是 getTransformation 函 數傳下來的差值點,在這裡做了一個線性插值演算法來生成中間角度:float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime); Camera 類是用來實現繞 Y 軸旋轉後透視投影的,我們只需要其返回的 Matrix 值 , 這個值會賦給 Transformation
中的矩陣成員,當 ParentView 去為 ChildView 設定畫布時,就會用它來設定座標系,這樣 ChildView 畫出來的效果就是一個繞 Y 軸旋轉同時帶有透視投影的效果。利用這個動畫便可以作出像立體翻頁等比較酷的效果。
簡單的使用
Rotate3dAnimation rotate = new Rotate3dAnimation(0f, 180f, startAnim.getMeasuredWidth() / 2,
startAnim.getMeasuredHeight() / 2, 0f, true);
rotate.setFillAfter(true);
rotate.setDuration(2000);
startAnim.startAnimation(rotate);
startAnim這個View就能中心點繞著Z軸旋轉了觸控事件處理
觸控事件首先傳遞到ViewGroup中
ViewGroup的dispatchTouchEvent中有
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
……
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
可以看到offsetX,offsetY只對mScrollX,child.mLeft進行了取值。。。
transformedEvent.transform(child.getInverseMatrix());不是前面設定的補間動畫,而應該是屬性動畫。。
如果設定的是屬性動畫,所以能在動畫結束的位置獲取到觸控事件,但是補間動畫就不行了。。
public final Matrix getInverseMatrix() {
ensureTransformationInfo();
if (mTransformationInfo.mInverseMatrix == null) {
mTransformationInfo.mInverseMatrix = new Matrix();
}
final Matrix matrix = mTransformationInfo.mInverseMatrix;
mRenderNode.getInverseMatrix(matrix);
return matrix;
}
此時返回的Matrix物件跟mRenderNode物件有關聯。而屬性動畫會改變屬性,比如此時在一個view上設定一個TranslationX的屬性動畫,那麼必然會呼叫該view的setTranslationX方法,果然此時操作了mRenderNode物件。。。setRotationX方法也一樣
public void setTranslationX(float translationX) {
if (translationX != getTranslationX()) {
invalidateViewProperty(true, false);
mRenderNode.setTranslationX(translationX);
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
public void setRotationX(float rotationX) {
if (rotationX != getRotationX()) {
invalidateViewProperty(true, false);
mRenderNode.setRotationX(rotationX);
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
與Scroll的異同
1. 滑動之後還是可以處理觸控事件的,因為觸控事件處理mScrollX,mScrollY,可以在ViewGroup的dispatchTransformedTouchEvent方法中看到
2. 滑動的原理其實也是調整了該view畫布canvas的座標系,所以預設情況下,整個view都會滑動。比如viewgroup.scrollby(-10,10),那麼viewgroup的viewgroup在dispatchDraw時繪製該viewgroup,就會把該viewgroup的畫布translate10個單位,所以最終結果就是viewgroup在父控制元件中移動了10個單位一樣,如果不是viewgroup,
view.scrollby(-10,10)也是一樣。
3. 但是另外一種情況,比如系統自定義的view或者viewgroup,比如linearlayout佈局,button,textview控制元件(可以看到textview中的onDraw方法應用了mScrollX,mScrollY。但為什麼控制元件的位置不變呢? 很奇怪,不知道怎麼做到的),滑動的是本身的內容,自身的位置卻不變。這是怎麼做到的,需要進一步研究,如果自定義view直接繼承自view,或者直接繼承子viewgroup,那麼scrollto,scrollby移動的是整個控制元件。
viewgroup.scrollby(-10,-10)。那麼重繪的時候,會呼叫viewgroup的viewgroup的draw方法。。。
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
invalidateParentCaches();postInvalidateOnAnimation();
有木有很熟悉,跟上面動畫是基本差不多的,都是導致view的parent發生了重繪,進而導致了該view的重繪,但是不會引起viewgroup其它控制元件的重繪
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
drawBackground(canvas);
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
dispatchDraw繪製頂層parent的子view,接著呼叫draw三個引數的過載方法。繪製該viewgroup
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...........
int sx = 0;
int sy = 0;
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
final boolean offsetForScroll = cache == null && !drawingWithRenderNode;
int restoreTo = -1;
if (!drawingWithRenderNode || transformToApply != null) {
restoreTo = canvas.save();
}
if (offsetForScroll) {
canvas.translate(mLeft - sx, mTop - sy);
}
...........
}
此時viewgroup的畫布,被右下移動了10個單位。。所以最後viewgroup在parent中整個的被移動了10個單位。。。
另一方面,mybutton是一個Button控制元件
mybutton.scrollBy(-10, -10);那麼最終的結果是button的內容右下平移了10個單位,而控制元件本身還停留在原來位置,這個比較費解,需要進一步研究。。
linearlayout.scrollBy(-10, -10);
最終結果linearlayout不變,裡面的子控制元件全部右下移動10個單位,很奇怪。。。
見如下:startscroll按鈕分別對上下兩個viewgroup進行scrollBy(-10, -10);
第一個viewgroup是我自定義的,直接繼承子viewgroup,第二個viewgroup使用的系統自定義的linearlayout。。
那麼最終結果,自定義的viewgroup,整個的右下移動了10個單位。linearlayout位置並沒有移動,只是其中的子view發生了右下10個單位的移動
總結:
1. 相同點都是其實重繪了該view所在的viewgroup,進而重繪view本身,而不是直接重繪view本身(直接呼叫invalidate就是直接重繪view本身)。。。
2. 動畫結束的地方是處理不了觸控事件,但是scrollto,scrollby結束的地方可以處理到。。。
動畫總結
1. Animation中主要定義了動畫的一些屬性比如開始時間、持續時間、是否重複播放等,這個類主要有兩個重要的函式:getTransformation 和 applyTransformation,
在 getTransformation 中 Animation 會根據動畫的屬性來產生一系列的差值點,然後將這些差值點傳給 applyTransformation,
這個函式將根據這些點來生成不同的 Transformation,Transformation 中包含一個矩陣和 alpha 值,矩陣是用來做平移、旋轉和縮放動畫的,
而 alpha 值是用來做 alpha 動畫的(簡單理解的話,alpha 動畫相當於不斷變換透明度或顏色來實現動畫),以上面的平移矩陣為例子,
當呼叫 dispatchDraw 時會呼叫 getTransformation 來得到當前的 Transformation
2. Android動畫就是通過ParentView來不斷調整ChildView的畫布canvas座標系來實現的。發生動畫的其實是ParentView而不是該view
3. 補間動畫其實只是調整了子view畫布canvas的座標系,其實並沒有修改任何屬性,所以只能在原位置才能處理觸控事件。。。
參考: