編輯:關於Android編程
在寫程序的時候遇到了Tween動畫幾個問題:
1, 執行動畫的時候點擊事件仍然在動畫開始的位置?
2, XXXAnimation的構造參數裡面的值具體是什麼意思?
3, 平移動畫中fromXValue和toXValue旋轉動畫中fromDegrees和toDegrees取負值有什麼不同??(相信很多人也有疑惑)
4, RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四個參數是怎麼確定旋轉原點的?確定的旋轉原點在哪裡?
Android動畫分為:
Tween Animation View動畫也叫補間動畫
Drawable Animation 也叫Frame 幀動畫
Property Animation(3.0以後加入)
主要研究Tween動畫
我在寫程序的時候經常由於參數設置不當(主要是從多少度旋轉為多少度,有時是負的度數)得不到想要的效果。因此打算把動畫的實現框架研究一下。
研究之前請看這篇文章:Android中圖像變換Matrix的原理 了解一下Matrix矩陣的相關知識。明白圖形的各種轉換就是要得到對應的變換矩陣。
首先說一下動畫的大概繪制框架過程,不然由於我寫的比較亂可能看暈了。
調用startAnimation會設置與View關聯的animation,然後會重繪視圖,重繪視圖的時候調用到drawChild,這時獲取與View綁定的Animation,不為null了,只要動畫時間沒有結束就會通過繪制的時間獲得變換矩陣,然後將畫布原點平移到視圖的左上角?(是不是這樣?)繪制新的一幀。繪制完又會重繪,然後獲取新的一幀的轉換矩陣…..循環下去,直到動畫結束就不再重繪視圖了。
回到View的onDraw函數裡面,onDraw函數做了如下工作。
1. Draw the background
2. If necessary, save the canvas' layers toprepare for fading
3. Draw view's content
4. Draw children
5. If necessary, draw the fading edges andrestore layers
6. Draw decorations (scrollbars forinstance)
當是ViewGroup的時候會執行第四步,dispatchDraw(canvas);
@Override protected void dispatchDraw(Canvas canvas) { // LayoutAnimationController比較熟悉,是讓ViewGroup的子控件有動畫效果,以前沒發現竟然也是在這裡發生的。 final LayoutAnimationController controller = mLayoutAnimationController; ... // We will draw our child's animation, let's reset the flag //下面對子View動畫進行處理。 mPrivateFlags &= ~DRAW_ANIMATION; mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; boolean more = false; final long drawingTime = getDrawingTime(); if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) { for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } else { for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } ... }
肯定會執行到drawChild(canvas, child, drawingTime); 在該函數顧名思義就是繪制子控件。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean more = false; final int cl = child.mLeft; final int ct = child.mTop; final int cr = child.mRight; final int cb = child.mBottom; final int flags = mGroupFlags; Transformation transformToApply = null; //取得該View綁定的動畫 final Animation a = child.getAnimation(); boolean concatMatrix = false; //如果該View有了動畫那麼就會進入if判斷執行,沒有動畫就僅僅繪制該控件。 if (a != null) { if (mInvalidateRegion == null) { mInvalidateRegion = new RectF(); } final RectF region = mInvalidateRegion; final boolean initialized = a.isInitialized(); if (!initialized) { //調用Animation的初始化函數,在這面會解析Animation的各個參數。對不同的xy類型和值進行轉換。 a.initialize(cr - cl, cb - ct, getWidth(), getHeight()); a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct); child.onAnimationStart(); } if (mChildTransformation == null) { mChildTransformation = new Transformation(); } //取得變換(平移,旋轉或縮放等)信息,傳進去的drawingTime代表了繪制的時間毫秒值,取得的結果放進mChildTransformation裡面。 //mChildTransformation是一個圖形轉換信息的類,包含了一個矩陣Matrix,和alpha值。Matrix就是圖形轉換矩陣。 //more是該函數的返回值,查看代碼很容易分析出來:如果動畫沒有結束就一只返回true,知道動畫結束返回false。 more = a.getTransformation(drawingTime, mChildTransformation); transformToApply = mChildTransformation; //默認返回true concatMatrix = a.willChangeTransformationMatrix(); //more==true進入循環 if (more) { //more==true when the animation is not over if (!a.willChangeBounds()) { if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) == FLAG_OPTIMIZE_INVALIDATE) { mGroupFlags |= FLAG_INVALIDATE_REQUIRED; } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) { mPrivateFlags |= DRAW_ANIMATION; //動畫沒有結束就會不停調用invalidate函數對動畫view進行重繪 invalidate(cl, ct, cr, cb); } } else { a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply); mPrivateFlags |= DRAW_ANIMATION; final int left = cl + (int) region.left; final int top = ct + (int) region.top; //動畫沒有結束就會不停調用invalidate函數對動畫view進行重繪 invalidate(left, top, left + (int) region.width(), top + (int) region.height()); } } } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) == FLAG_SUPPORT_STATIC_TRANSFORMATIONS) { if (mChildTransformation == null) { mChildTransformation = new Transformation(); } final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation); if (hasTransform) { final int transformType = mChildTransformation.getTransformationType(); transformToApply = transformType != Transformation.TYPE_IDENTITY ? mChildTransformation : null; concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0; } } ... child.computeScroll(); //分析簡單情況下視圖都是在可視范圍內,sx和sy應該等於0?? final int sx = child.mScrollX; final int sy = child.mScrollY; ... final boolean hasNoCache = cache == null; final int restoreTo = canvas.save(); if (hasNoCache) { canvas.translate(cl - sx, ct - sy); } else { //here translate the canvas's zuobiao???? @auth:qhyuan //將畫布平移到(cl,ct)點,cl和ct是childView的左上角到屏幕(0,0)點的距離。這點非常重要,在重繪動畫的時候畫布的左邊在發生變化!!!並不是一直在屏幕的(0,0)點。 //平移後的坐標體系和最初不一樣,一般情況下坐標的原點會移動至View的左上角。 canvas.translate(cl, ct); if (scalingRequired) { // mAttachInfo cannot be null, otherwise scalingRequired == false final float scale = 1.0f / mAttachInfo.mApplicationScale; canvas.scale(scale, scale); } } float alpha = 1.0f; if (transformToApply != null) { if (concatMatrix) { int transX = 0; int transY = 0; if (hasNoCache) { transX = -sx; transY = -sy; } //兩個參數為0 canvas.translate(-transX, -transY); // transformToApply是從Animation取得的轉換信息類,取得變換矩陣。這個變換矩陣在不同時刻都不一樣,因為傳過去的drawingTime不一樣。 //對畫布進行變換矩陣轉換,實現動畫效果。 canvas.concat(transformToApply.getMatrix()); canvas.translate(transX, transY); mGroupFlags |= FLAG_CLEAR_TRANSFORMATION; } ... if (alpha < 1.0f && hasNoCache) { final int multipliedAlpha = (int) (255 * alpha); if (!child.onSetAlpha(multipliedAlpha)) { canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha, Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); } else { child.mPrivateFlags |= ALPHA_SET; } } } else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) { child.onSetAlpha(255); } ... return more; }
接下來看一下Animation類:動畫類裡面有兩個重要的函數protected void applyTransformation(float interpolatedTime,Transformation t)
和public boolean getTransformation(long currentTime, TransformationoutTransformation);
Transformation類包含了一個變換矩陣和alpha值。
applyTransformation函數:傳入一個差值時間,會填充一個Transformation類。會在getTransformation函數裡面調用,Animation類的applyTransformation是個空實現,具體的XXXAnimation在繼承自Animation時會實現applyTransformation函數。
getTransformation函數:會在drawChild函數裡面調用。
public boolean getTransformation(long currentTime, Transformation outTransformation) { // currentTime 會在drawChild函數中通過getDrawTime傳過來 if (mStartTime == -1) { mStartTime = currentTime; } final long startOffset = getStartOffset(); final long duration = mDuration; float normalizedTime; if (duration != 0) { //歸一化時間,這樣normalizedTime是介於0和1之間的值。 normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / (float) duration; } else { // time is a step-change with a zero duration normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f; } // expired表示“過期”,如果歸一化時間大於1 ,expired == true,即expire表示動畫結束了 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) { if (mListener != null) { //記錄開始動畫 mListener.onAnimationStart(this); } mStarted = true; } if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f); if (mCycleFlip) { normalizedTime = 1.0f - normalizedTime; } //通過歸一化的時間得到插值時間,類似於一個函數f(t)根據歸一化的時間得到插值時間。 //插值時間的作用就是得到變化速率改變的效果,例如線性插值就是f(t)=t final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime); //調用applyTransformation函數,具體實現在繼承自Animation的類中實現 //簡單地說就是傳入插值時間,然後該函數根據插值時間填充具體的轉換矩陣,不同的時刻對應不同的轉換矩陣,通過該轉換矩陣就會繪制出在不同位置的圖形。 applyTransformation(interpolatedTime, outTransformation); } //如果動畫結束了會執行下面 if (expired) { if (mRepeatCount == mRepeated) { if (!mEnded) { mEnded = true; if (mListener != null) { mListener.onAnimationEnd(this); } } } else { if (mRepeatCount > 0) { mRepeated++; } if (mRepeatMode == REVERSE) { mCycleFlip = !mCycleFlip; } mStartTime = -1; mMore = true; if (mListener != null) { mListener.onAnimationRepeat(this); } } } if (!mMore && mOneMoreTime) { mOneMoreTime = false; return true; } //通過分析發現總是返回true,除非動畫結束了。 //always return true until the expired==true(the animation is over) @auth:qhyuan return mMore; }
applyTransformation函數在Animation裡面默認是空實現,需要在子類中實現,也就是說自定義動畫需要實現applyTransformation函數。
插值類也很簡單,是一個接口,只有一個函數。
public interface Interpolator { float getInterpolation(float input); }
常用的子類有:
AccelerateDecelerateInterpolator 在動畫開始與結束的地方速率改變比較慢,在中間的時候加速
AccelerateInterpolator 在動畫開始的地方速率改變比較慢,然後開始加速
AnticipateInterpolator 開始的時候向後然後向前甩
AnticipateOvershootInterpolator 開始的時候向後然後向前甩一定值後返回最後的值
BounceInterpolator 動畫結束的時候彈起
CycleInterpolator 動畫循環播放特定的次數,速率改變沿著正弦曲線
DecelerateInterpolator 在動畫開始的地方快然後慢
LinearInterpolator 以常量速率改變
OvershootInterpolator 向前甩一定值後再回到原來位置
也可以自定義interpolator,只需要實現getInterpolation函數就可以了。
前面簡單的介紹了一下動畫繪制所涉及的一些函數,接下來以執行平移動化為例將動畫的執行過程走一遍:
1.定義完XXXAnimation後執行View的startAnimation
public void startAnimation(Animation animation) { animation.setStartTime(Animation.START_ON_FIRST_FRAME); //設置Animation域 setAnimation(animation); //請求重繪視圖 invalidate(); }
setAnimation函數如下:
public void setAnimation(Animation animation) { //將animation設置給View的mCurrentAnimation屬性 mCurrentAnimation = animation; if (animation != null) { animation.reset(); } }
2.然後請求重繪視圖會執行onDraw函數,最後必然會執行到dispatchDraw函數,又會執行到drawChild(canvas,child, drawingTime); drawingTime函數是這次繪制的時間毫秒值。drawChild函數前面解釋過。
3.首先獲取View所關聯的Animation,然後調用Animation的初始化函數,在這面會解析Animation的各個參數。對不同的xy類型和值進行轉換,initialize函數也會在Animation的子類中實現。TranslateAnimation中的initialize函數如下,在該函數裡面直接調用resolveSize函數解析構造參數中的值。
public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth); mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth); mFromYDelta = resolveSize(mFromYType, mFromYValue, height, parentHeight); mToYDelta = resolveSize(mToYType, mToYValue, height, parentHeight); }
resolveSize函數如下,根據是絕對坐標、相對控件自身還是相對父控件和具體的值解析出解析後的值,很容易看出絕對坐標,直接返回對應值,相對自身和相對父控件就是用相對值結余0到1之間的值乘以子控件或者父視圖寬高的值。
protected float resolveSize(int type, float value, int size, int parentSize) { switch (type) { case ABSOLUTE: return value; case RELATIVE_TO_SELF: return size * value; case RELATIVE_TO_PARENT: return parentSize * value; default: return value; } }
4.然後就根據重繪的時間毫秒值通過getTransformation函數獲得對應的轉換矩陣。在這個函數裡面會先調用interpolatedTime = mInterpolator.getInterpolation(normalizedTime);根據時間獲得插值時間。
然後調用applyTransformation函數,TranslateAnimation的該函數具體如下:
protected void applyTransformation(float interpolatedTime, Transformation t) { float dx = mFromXDelta; float dy = mFromYDelta; //開始X坐標值和結束值不一樣 if (mFromXDelta != mToXDelta) { //某個時刻(插值時間)對應的dx,如果是線性插值interpolatedTime和normalizedTime是一樣的。 dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime); } if (mFromYDelta != mToYDelta) { dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime); } //對轉換矩陣進行重新設置,將位移差設置進轉換矩陣中。平移動化其實也是變換矩陣和原來的坐標點的相乘。 t.getMatrix().setTranslate(dx, dy); }
只要該動畫沒有結束getTransformation函數會一直返回true。然後判斷返回true又會調用invalidate函數,接下來就是重復2,3,4的步驟了,但是重復執行的時候繪制時間不一樣,於是獲得的轉換矩陣不一樣,得到的新的視圖的位置就不一樣。如果返回false說明動畫執行完成了,就不在重繪視圖了。繪制控件時前面說過在繪制視圖的時候會調用canvas.translate(cl - sx, ct - sy);簡單地說就是將畫布的坐標體系從屏幕左上角移動至動畫視圖的左上角。然後每次動畫的時候canvas.concat(transformToApply.getMatrix());對話不的矩陣轉換操作,然後繪圖就實現了對動畫的轉換。
整個過程稍微有點復雜,有些函數我還沒看的很明白,不過大致的思路就是這樣。
回頭看開始提出的幾個問題:
1, 執行動畫的時候其實並不是該控件本身在改變,而是他的父View完成的。startAnimation(anim)其實是給這個View設置了一個animation,而不是進行實際的動畫繪制。他的位置其實根本沒有改變,還是有layout所指定的位置決定的。
2, 參數的意思在代碼裡面的注釋解釋過了。
3, fromXValue和toXValue旋轉動畫中fromDegrees和toDegrees取正負值是有區別的,具體要看代碼裡面,轉換矩陣是怎麼生成的。比如平移動化裡面:
dx = mFromXDelta + ((mToXDelta - mFromXDelta) *interpolatedTime);
插值時間從0變到1,假設現在時相對自身的類型,控件本身寬度為100,mFromXDelta這些值在初始化的時候已經解析成了實際值(從0~1.0這種相和相對自身還是相對父View變成了像素寬高度)
從0變到1.0f的效果是:從當前位置向右平移100,原因是第一幀圖像的dx為0,最後一幀dx為100
從-1.0f變到0的效果是:從當前位置的左邊100處向右平移到當前位置,原因是第一幀圖像的dx為-100,最後一幀的dx為0
旋轉動畫中通過指定開始結束角度的正負實現順時針和逆時針旋轉是類似的道理。大家可以自行感悟一下下面四個動畫的區別,來看一下正負始末值對旋轉的影響。
rotateAnimation= new RotateAnimation(0, 90, Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT,0); rotateAnimation= new RotateAnimation(90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0); rotateAnimation= new RotateAnimation(-90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0); rotateAnimation = new RotateAnimation(0, -90,Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT, 0);
4, RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四個參數的問題:
由於前面說過了,動畫的時候,畫布平移到了View的左上角,因此對於旋轉動畫來說參考的坐標原點始終是View左上角。而旋轉的旋轉點是可以任意指定的,該旋轉點參考的坐標應該是View左上角。
手機屏幕中心處有一個點,現在我們想以屏幕最左邊的那一邊的中點為圓心旋轉360度,代碼應該是:
new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, 0);//注意是-0.5f
以屏幕左上角旋轉:
new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, -0.5f);//注意是負的。
以自己中心旋轉:
new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);
以自己最下面的中點旋轉:
new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 1.0f);
其他旋轉點以此類推。我以前只是大致看過這些參數,所以在寫圍繞屏幕左上角旋轉的代碼時,想當然寫成了:
new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT, 0,Animation.RELATIVE_TO_PARENT, 0);
我本來想相對於父View來說(0,0)就是屏幕的左上角那一個點。結果當然不正確,測試一下也可以看出這個時候的旋轉點仍然是View的左上角,是不是和RELATIVE_TO_PARENT沒有半點關系?
總結一下就是說:後面的四個參數只是能算出來相對於畫布坐標的距離,僅此而已,並不能看到是相對父View的值就忘了畫布的原點在哪。
我在分析的時候只是大致的將動畫的繪制過程研究了一下,實際的代碼很復雜的。如果那裡解釋的不對的地方歡迎大家和我交流。
測試動平移和旋轉動畫的參數Demo:點此下載
greenDAO是時下Android最流行的一款ORM框架,其性能高,可加密,使用簡潔,做android開發,如果會使用它,工作量會大大減小。其性能與其他ORM框架之比較
本篇開始分析按鍵消息事件分發(PS:本篇文章中源碼均是android 6.0,請知曉)先看下Agenda:ViewRootImpl中的dispatchInputEvent
實現功能:實現MyLoveMusicActivity(音樂收藏界面)實現MyRecordMusicActivity(最近播放界面)實現MyMusicListFragmen
android-pdf-viewer在android studio應用問題說明小白一枚,之前一直是做.NET開發的,最近需要弄一個新聞app,能力有限,只能借助HTML5