Androidアニメーションの描画原理を実現
15014 ワード
タイトル:Androidアニメーション実装ペイント原理date:2016-10-05 15:00
本文はKuTear's Blogに発表して、転載して明記してください
プリフェッチ
タグビットについてAndroidでは非常に多く使われていますが、簡単に言えばバイナリの1つを使って1つの状態を表し、次は簡単に栗を挙げます.
上記の考え方は
Viewアニメーション描画の流れ
Viewでは、
ビューツリーの最上位は
ここはコードが多く、選択的に見ても論理的には関数が呼び出されている
ここで呼び出す
OK、上の節で述べた関数がここに現れました.いったいどういうことなのか見てみましょう.上の関数の多くはタグビットの設定とタグビットの判断であるが,具体的にどういう意味かはまだはっきりしていないが,ここでの
ここで関数を呼び出した
また、
前編のWindow/WindowManagerとWindowManagerSystemの理解によれば、解析
QA
ViewGroupとそのサブクラスonDraw(canvas)は実行されていません
本文はKuTear's Blogに発表して、転載して明記してください
プリフェッチ
タグビットについてAndroidでは非常に多く使われていますが、簡単に言えばバイナリの1つを使って1つの状態を表し、次は簡単に栗を挙げます.
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アニメーション描画の流れ
Viewでは、
View
の描画過程が関数draw(canvas)
から始まることを知っていますが、この関数を分析してみましょう.その注釈部分によって、簡略化版の論理コードを簡単に得ることができます.//View.java
@CallSuper
public 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);
}
ビューツリーの最上位は
DecorView
、それはFrameLayout
のサブクラスであり、描画はルートノードから(RootViewImpl
中dispatchDraw()
)であり、上のコードによって描画がサブView
に配布され、サブView
きっとViewGroup
のサブクラスであるので、ViewGroup
のdispatchDraw(canvas)
、View
のこの関数は空で実現されている.描画を配布する必要はないからである.//ViewGroup.java
@Override
protected void dispatchDraw(Canvas canvas) {
//balabala
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);
}
}
//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.java
boolean 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.java
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
invalidate(true)
関数の主な役割は、Viewツリーの再描画を要求することですが、具体的にどのように描かれているのか、次に下を見てみましょう.//View.java
void 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 && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
}
//....
}
OK、上の節で述べた関数がここに現れました.いったいどういうことなのか見てみましょう.上の関数の多くはタグビットの設定とタグビットの判断であるが,具体的にどういう意味かはまだはっきりしていないが,ここでの
mAttachInfo
Viewが初めてattachしてWindowに着いたとき,View Rootが自分の子Viewに伝えたものである.このAttachInfoの後は、レイアウト体系に沿って最下層のViewまで伝わりますが、次はViewParent
いったい何をしたのでしょうか?//ViewGroup.java
public 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()
ここでこの関数の実装は2つあり,1つはViewGroup
のうち,もう1つは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全体の再描画を要求しており、具体的なコードは以下の通りである.@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread(); //UI UI
...
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}
前編のWindow/WindowManagerとWindowManagerSystemの理解によれば、解析
ViewRootImpl
の描画時に解析したことがあるが、関数scheduleTraversals()
の論理は実は1つRunnable
を実行しているが、これRunnable
実は関数を実行しているdoTraversal()
、関数doTraversal()
が呼び出されるperformTraversals()
、ここまで再描画が始まっていることが分かった.全体的にアニメーションの実行は全体View Tree
再描画を招くが、Android内部には1枚の画像を移動するなどの最適化があり、本格的に再描画する必要はなく、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 , View
public RenderNode updateDisplayListIfDirty() {
...
//
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
...
} else {
draw(canvas); // 6
}
...
}