Android動畫實現繪製原理
讀前補充
關於標記位在Android運用的是非常的多,簡單的說就是使用二進位制中的一位表示一個狀態,下面簡單的舉個例子。
private int flag = 0;private static final int NEED_DRAW = 0x1; //0001; //表示需要繪製private static final int HAS_ANIMATION = 0x2; //0010;//表示有動畫private static final int HAS_BACKGROUND = 0x4 //0100;//有背景色private static final int HAS_FORGROUND = 0x8 //1000;//有前景色void action(){ if(flag & NEED_DRAW == NEED_DRAW){ draw(); } }void draw(){ if(flag & HAS_FORGROUND == HAS_FORGROUND){ drawForground(); } ... }void clearDrawFlag(){ flag &= ~NEED_DRAW; }void setDrawFlag(){ flag |= NEED_DRAW; }
上面的思維在View
中使用的比較的多,我們只要記住flag & HAS_FORGROUND == HAS_FORGROUND
就表示flag
存在HAS_FORGROUND
標記位,另外,也可以一次判斷多個標記的存在情況。
if(flag & (HAS_FORGROUND | HAS_BACKGROUND) == (HAS_FORGROUND | HAS_BACKGROUND)){ //前景和背景同時存在 }
View Animation繪製的流程
在View中,我們知道View
的繪製過程是從函式draw(canvas)
開始,下面我們來分析一下該函式,根據它的註釋部分,我們很容易得到簡化版的邏輯程式碼。
//View.java@CallSuperpublic void draw(Canvas canvas) { // Step 1, draw the background, if needed drawBackground(canvas); // skip step 2 & 5 if possible (common case) // Step 3, draw the content 非透明才繪製 if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); }
我們知道,View樹的頂層是DecorView
,它是FrameLayout
的子類,繪製是從根節點開始(RootViewImpl
中的dispatchDraw()
),根據上面的程式碼會分發繪製到子View
,有子View
一定是ViewGroup
的子類,所以看看ViewGroup
的dispatchDraw(canvas)
,View
的該函式是個空實現,因為根本就不需要分發繪製。
//ViewGroup.java@Overrideprotected void dispatchDraw(Canvas canvas) { //balabala for (int i = 0; i = 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); } } //balabala}protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
這裡程式碼好多,選擇性的看吧,反正邏輯就是呼叫了函式drawChild()
,然而 drawChild()
就是直接呼叫child
的draw(Canvas canvas, ViewGroup parent, long drawingTime)
函式,這裡就是把繪製分發到子View
,使得整個View Tree
得以繪製。OK,我們接著看到底是怎麼繪製的。
//View.javaboolean draw(Canvas canvas, ViewGroup parent, long drawingTime){ boolean drawingWithRenderNode = mAttachInfo != null && mAttachInfo.mHardwareAccelerated && hardwareAcceleratedCanvas; final Animation a = getAnimation(); if (a != null) { //如果有動畫就在這個函式內部請求重繪,return True if the animation is still running more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired); concatMatrix = a.willChangeTransformationMatrix(); if (concatMatrix) { mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM; } transformToApply = parent.getChildTransformation(); } if(drawingWithRenderNode){ renderNode = updateDisplayListIfDirty(); //其內部呼叫draw(canvas) } return more; }private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired) { //t為根據動畫產生的Transformation{mAlpha,mMatrix},可以控制基本的動畫 boolean more = a.getTransformation(drawingTime, t, 1f); if (more) { final RectF region = parent.mInvalidateRegion; a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region, invalidationTransform); // The child need to draw an animation, potentially offscreen, so // make sure we do not cancel invalidate requests // //這裡設定了一個表示該View含有動畫的標記位,在後面會使用這個標記位 parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION; final int left = mLeft + (int) region.left; final int top = mTop + (int) region.top; //這裡請求重繪,根據動畫改變的子View的可繪製區域的位置。 //我們知道View Animation 是沒有改變原本View的屬性,包括寬高位置。 parent.invalidate(left, top, left + (int) (region.width() + .5f), top + (int) (region.height() + .5f)); } //balabala}public void invalidate(int l, int t, int r, int b) { final int scrollX = mScrollX; final int scrollY = mScrollY; invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false); }public RenderNode updateDisplayListIfDirty() { //.... if(condition){ .... draw(canvas); } //....}
這裡呼叫invalidateInternal()
函式就是請求重繪,具體怎麼重繪,我們在下面會講。
上面的過程是View
繪製的大體流程,下面看看設定動畫的情況。我們設定View動畫的入口函式一般都是startAnimation()
,下面我們從這裡入手。
//View.javapublic void startAnimation(Animation animation) { animation.setStartTime(Animation.START_ON_FIRST_FRAME); setAnimation(animation); invalidateParentCaches(); invalidate(true); }
invalidate(true)
函式的主要作用是請求View樹進行重繪,但是具體是怎麼繪製的呢,我們接著往下看。
//View.javavoid invalidate(boolean invalidateCache) { //這裡的mLeft,mRight都是相當於父View來講的。所以這裡的引數就是當前View所在的區域。 invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true); }void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) { //不需要繪製的情況(View不可見&&沒有動畫等) if (skipInvalidate()) { return; } //... if(condition/*滿足需要繪製的條件*/){ final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; if (p != null && ai != null && lOK,在上面小節提到的函式在這裡出現了,我們看看究竟是怎麼一回事。上面函式大多是設定標記位和判斷標記位,具體什麼意思還不是很清楚,我們重點看下那一小段,這裡的
mAttachInfo
是在View第一次attach到Window時,ViewRoot傳給自己的子View的。這個AttachInfo之後,會順著佈局體系一直傳遞到最底層的View,下面看看ViewParent
到底做了什麼?//ViewGroup.javapublic final void invalidateChild(View child, final Rect dirty) { ViewParent parent = this; final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) == PFLAG_DRAW_ANIMATION; //applyLegacyAnimation()中有設定這個標記位 final int[] location = attachInfo.mInvalidateChildLocation; location[CHILD_LEFT_INDEX] = child.mLeft; location[CHILD_TOP_INDEX] = child.mTop; //localtion存放的子view的右上角的座標 ... do { View view = null; if (parent instanceof View) { view = (View) parent; } //這裡意味著如果子View有動畫,那麼父View也要設定上動畫標記位,一直到頂層ViewRootImpl if (drawAnimation) { if (view != null) { view.mPrivateFlags |= PFLAG_DRAW_ANIMATION; } else if (parent instanceof ViewRootImpl) { ((ViewRootImpl) parent).mIsAnimating = true; } } // If the parent is dirty opaque or not dirty, mark it dirty with the opaque // flag coming from the child that initiated the invalidate if (view != null) { if ((view.mViewFlags & FADING_EDGE_MASK) != 0 && view.getSolidColor() == 0) { opaqueFlag = PFLAG_DIRTY; } if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) { view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag; } } parent = parent.invalidateChildInParent(location, dirty); if (view != null) { // Account for transform on current parent Matrix m = view.getMatrix(); if (!m.isIdentity()) { RectF boundingRect = attachInfo.mTmpTransformRect; boundingRect.set(dirty); m.mapRect(boundingRect); dirty.set((int) (boundingRect.left - 0.5f), (int) (boundingRect.top - 0.5f), (int) (boundingRect.right + 0.5f), (int) (boundingRect.bottom + 0.5f)); } } } while (parent != null); } }這裡呼叫了函式
invalidateChildInParent()
,需要注意的是這裡這個函式的實現有兩個,一個是ViewGroup
中,而另一個是ViewRootImpl
。//ViewGroup.java//總體來講,這裡就是修改一些引數,使其滿足當前的ViewGroup,比如座標等。public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) { if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN || (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) { if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) != FLAG_OPTIMIZE_INVALIDATE) { dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX, location[CHILD_TOP_INDEX] - mScrollY); if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) { dirty.union(0, 0, mRight - mLeft, mBottom - mTop); } final int left = mLeft; final int top = mTop; if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) { if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) { dirty.setEmpty(); } } mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; location[CHILD_LEFT_INDEX] = left; location[CHILD_TOP_INDEX] = top; if (mLayerType != LAYER_TYPE_NONE) { mPrivateFlags |= PFLAG_INVALIDATED; } return mParent; } else { mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID; location[CHILD_LEFT_INDEX] = mLeft; location[CHILD_TOP_INDEX] = mTop; if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) { dirty.set(0, 0, mRight - mLeft, mBottom - mTop); } else { // in case the dirty rect extends outside the bounds of this container dirty.union(0, 0, mRight - mLeft, mBottom - mTop); } if (mLayerType != LAYER_TYPE_NONE) { mPrivateFlags |= PFLAG_INVALIDATED; } return mParent; } } return null; }而在
ViewRootImpl
中就是請求整個View Tree進行重繪,具體的程式碼如下。@Overridepublic void invalidateChild(View child, Rect dirty) { invalidateChildInParent(null, dirty); }@Overridepublic ViewParent invalidateChildInParent(int[] location, Rect dirty) { checkThread(); //UI執行緒才可以操作UI ... invalidateRectOnScreen(dirty); return null; }private void invalidateRectOnScreen(Rect dirty) { ... if (!mWillDrawSoon && (intersected || mIsAnimating)) { scheduleTraversals(); } }根據上篇,我們透過分析
ViewRootImpl
的繪製的時候分析過,函式scheduleTraversals()
的邏輯其實是執行一個Runnable
,而這個Runnable
其實就是去執行函式doTraversal()
,而函式doTraversal()
會呼叫performTraversals()
,到這裡我們發現它開始重繪了。總體來說就是動畫的執行會導致整個View Tree
重繪,但是Android內部有一些最佳化,比如一張圖片做移動,我們不需要真正的去重新繪製,Android內部提供快取機制,不會顯示的再呼叫onDraw(canvas)
函式。
到這裡,動畫的執行邏輯大體清楚了。QA
ViewGroup及其子類onDraw(canvas)沒有執行
ViewGroup
沒有背景時預設是不會執行onDraw(canvas)
方法的,具體的原因我們在下面分析。public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initViewGroup(); initFromAttributes(context, attrs, defStyleAttr, defStyleRes); }private void initViewGroup() { // ViewGroup doesn't draw by default if (!debugDraw()) { setFlags(WILL_NOT_DRAW, DRAW_MASK); //設定相關的標記位 } .... }//#View.java//可以看見在View裡也包含這個設定//willNotDraw = true ,不繪畫public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }//View.java//WILL_NOT_DRAW = 0x00000080;//DRAW_MASK = 0x00000080;void setFlags(int flags, int mask) { .... int old = mViewFlags; mViewFlags = (mViewFlags & ~mask) | (flags & mask);//(flags & mask)= 0x00000080 int changed = mViewFlags ^ old; .... if ((changed & DRAW_MASK) != 0) { if ((mViewFlags & WILL_NOT_DRAW) != 0) { if (mBackground != null || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) { //需要繪製時的標記位,透過這裡我們知道,有背景時設定是無效的,這裡很容易驗證 mPrivateFlags &= ~PFLAG_SKIP_DRAW; } else { mPrivateFlags |= PFLAG_SKIP_DRAW; //不要繪製,新增標記位 } } else { mPrivateFlags &= ~PFLAG_SKIP_DRAW; } requestLayout(); invalidate(true); } ... }//View.java//這個函式上面有分析,在boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)//中呼叫,使用這個函式來繪製View本身部分,透過這裡,我們可以看到如果新增了PFLAG_SKIP_DRAW標記//那麼該View的繪製會被跳過,從而直接分發到子Viewpublic RenderNode updateDisplayListIfDirty() { ... //當包含標記位時直接分發 if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { dispatchDraw(canvas); ... } else { draw(canvas); //文章開頭的6步繪製過程 } ... }
作者:KuTear
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4686/viewspace-2809923/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android 動畫實現Android動畫
- Android UI繪製流程及原理AndroidUI
- Android模擬器繪製實現概述Android
- Android View繪製原理:繪製流程排程、測算等AndroidView
- Android 動畫框架實現Android動畫框架
- canvas繪製動畫的技巧Canvas動畫
- Flutter 中如何繪製動畫Flutter動畫
- canvas實現手動繪製矩形Canvas
- [Android]多層波紋擴散動畫——自定義View繪製Android動畫View
- 靈魂畫師,Android繪製流程——Android高階UIAndroidUI
- 使用python matplotlib實現動圖繪製Python
- 使用canvas繪製圓弧動畫Canvas動畫
- Flutter動畫實現原理淺析Flutter動畫
- Android繪製(三):Path結合屬性動畫,讓圖示動起來!Android動畫
- [-Flutter 自定義元件-] 蛛網圖+繪製+動畫實踐Flutter元件動畫
- 前端動畫實現以及原理淺析前端動畫
- 動畫函式的繪製及自定義動畫函式動畫函式
- View繪製——畫多大?View
- View繪製——畫在哪?View
- 高效動畫實現原理-Jetpack Compose 初探索動畫Jetpack
- Fiori裡花瓣的動畫效果實現原理動畫
- Android——Activity切換炫酷動畫實現Android動畫
- Android優化——繪製優化之android系統顯示原理(一)Android優化
- Lottie Android 動畫製作與使用Android動畫
- iOS UI繪製原理iOSUI
- canvas lineWidth 繪製原理Canvas
- canvas lineWidth繪製原理Canvas
- View繪製——怎麼畫?View
- 說說Android動態換膚實現原理吧Android
- Android OpenGLES繪製天空盒Android
- 如何繪製三維動畫設計和製作場景更好動畫
- WPF 使用動畫繪製一個點贊大拇指動畫
- 使用 CSS 繪製帶有動畫效果的 React LogoCSS動畫ReactGo
- ai繪畫商業化來了,ai繪畫軟體“數畫”實現全民創作變現模式!AI模式
- Unity Shader- UV動畫原理及簡易實現Unity動畫
- Android 屬性動畫實戰Android動畫
- Kitten 動態繪製 Y 軸方向立方體的實現方法
- Vue canvas繪製圓形進度條動畫載入VueCanvas動畫