Viewをカスタマイズする場合、Viewをリフレッシュするためにinvalidateメソッドを使用するのが一般的です。この記事では、invalidateの実装を分析します。invalidateにはいくつかのオーバーロードされたメソッドがありますが、最終的な実装は似ているので、ここではinvalidate()の分析から始めます。
frameworks/base/core/java/android/view/View.java
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future. This must be called from a UI thread. To call from a non-UI thread,
* call {@link #postInvalidate()}.
*/
//ビュー全体を無効にすると、ビューのonDrawが呼び出される、つまり、ビューを再描画する処理が発生する。
public void invalidate() {
invalidate(true);//trueつまり、リフレッシュ・キャッシュも無効にする必要がある。
}
//trueこれは、リフレッシュ・キャッシュを無効にする必要があることを意味し、falseに設定されている場合は、ビューのコンテンツまたはサイズが変更されていないことを意味する。
void invalidate(boolean invalidateCache) {
if (skipInvalidate()) {//この処理をスキップするかどうかは、描画をスキップする条件を満たす必要がある:ビューが表示されておらず、現在のアニメーションが実行されていない。
return;
}
/*
* 条件を更新することができる:
* 1: 描画が必要で、境界が設定されている
* 2現在の描画キャッシュはまだ有効だが、それを無効にする必要がある。
* 3ビューは無効化される。
* 4: 透明度の変更
* */
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) ||
(invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) ||
(mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED || isOpaque() != mLastIsOpaque) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;//描画マーカーをリセットする
mPrivateFlags |= PFLAG_DIRTY;//マーク PFLAG_DIRTY これは、ビューが無効化されたことを意味する。
if (invalidateCache) {//キャッシュを無効にする必要がある
mPrivateFlags |= PFLAG_INVALIDATED;//マーカーの失敗
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;//有効なマーカーのキャッシュをクリアする。
}
final AttachInfo ai = mAttachInfo;//ビューのアタッチ情報を取得する。
final ViewParent p = mParent;//親ビュー
//noinspection PointlessBooleanExpression,ConstantConditions
if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
if (p != null && ai != null && ai.mHardwareAccelerated) {//ハードウェア・アクセラレーションが有効になっている。
// fast-track for GL-enabled applications; just invalidate the whole hierarchy
// with a null dirty rect, which tells the ViewAncestor to redraw everything
p.invalidateChild(this, null);//ハードウェアアクセラレーションが有効な場合、ビューのすべての領域がダーティとしてマークされ、viewRootはすべてのコンテンツを再描画する。
return;
}
}
//ハードウェア・アクセラレーションが有効になっていない。
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop);//Rectをビューのサイズに設定する。
// Don't call invalidate -- we don't want to internally scroll
// our own bounds
p.invalidateChild(this, r);//親ビューのinvalidateChildメソッドを呼び出す。
}
}
}
mRight-mLeftはViewの幅、mBottom-mTopはViewの高さであるためです。ViewParentは、実際には現在のビューの親Viewまたはビュー階層のViewRootであるため、invalidateChildはViewGroupまたはViewRootImplに送られます。続きを読む
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;//child viewアニメーションが実行されているかどうか
Matrix childMatrix = child.getMatrix();
final boolean isOpaque = child.isOpaque() && !drawAnimation &&
child.getAnimation() == null && childMatrix.isIdentity();//リクエストを開始したビューが不透明であろうとなかろうと、アニメーションを実行するビューは不透明とはみなされない。
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;//child view onDrawのダーティ・フラグは
//ダーティ領域の始点の座標
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
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);
} while (parent != null);
}
}
invalidateChildは2つのパラメータを受け取ります。invalidateを開始した子ビューがダーティ領域で、ダーティ領域の開始点は親ビューの子ビューの左上隅の座標です。whileループでは、アニメーションの実行を無視し、現在の親ビューにダーティマーカーを設定します。子ビューのビューを記録するために、親ビューのこのダーティなマーカーは、その後の描画は、onDraw描画を介してビューを描画する必要があるかどうかを判断するためにマークで使用され、その後invalidateChildInParentメソッドを呼び出し、親ビューの現在の親ビューに戻り、ViewRootImplまで。このループは、実際には、Viewの親Viewの起動からViewRootの実行invalidateChildInParent処理までのView階層であり、その間にinvalidateChildInParentは実際に何かを行うのでしょうか?invalidateChildInParentに渡されるのは、現在の親Viewの左上にあるダーティエリアの座標と、ダーティエリアです。
// /frameworks/base/core/java/android/view/ViewGroup.java
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;//同時にViewParent PFLAGを設定する。_DRAWING_CACHE_VALID0としてマークされる。
//位置を更新する
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
mLocalDirtyRect.union(dirty);
}
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);//現在のVeiwGroup領域とマージする。
}
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;//ViewGrouponMeasureとonLayoutのマークアップは無効になる。
mLocalDirtyRect.union(dirty);
}
return mParent;
}
}
return null;
}
invalidateChildInParentでは、まず現在の親ビューのオフセットによってダーティエリアが計算されます。開始ダーティエリアは無効化を開始した子ビューで、次に親ビューの左上隅です。次に、FLAG_CLIP_CHILDRENが設定されているかどうか、つまりレイアウトファイルでandroid:clipChildrenが設定されているかどうかを判断してダーティエリアを計算します。この属性はデフォルトでtrueに設定されています。CLIP_CHILDRENが設定されている、つまりレイアウトファイルでandroid:clipChildrenが設定されており、デフォルトではtrueに設定されている場合、この属性は子ビューが描画領域の親ビューにあることを制限するために使用されます。falseに設定すると、FLAG_CLIP_CHILDRENが設定されていない場合、子ビューが親ビューの描画領域を超えても良いことを意味します。この場合、ダーティ領域は現在の親ビューの領域に設定されます。それ以外の場合、FLAG_CLIP_CHILDREN が設定されていると、ダーティ領域は現在のダーティ領域と親ビューの領域の交点に従って設定されます。交点がない場合、dirtyは空になります。次に、現在の親ビューの左上隅の位置を更新して、親ビューのダーティエリアのオフセットの次の計算に備えます。1層の計算の後、最終的にViewRootImplのinvalidateChildInParentに戻ります。
// /frameworks/base/core/java/android/view/ViewRootImpl.java
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (dirty == null) {//ダーティエリアはnullであり、これはビューツリー全体を再描画しなければならないことを意味する。
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {//実行中のアニメーションがなく、ダーティ・エリアがなければ、描画する必要はない。
return null;
}
if (mCurScrollY != 0 || mTranslator != null) {
mTempRect.set(dirty);
dirty = mTempRect;
if (mCurScrollY != 0) {
dirty.offset(0, -mCurScrollY);
}
if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(dirty);
}
if (mAttachInfo.mScalingRequired) {
dirty.inset(-1, -1);
}
}
final Rect localDirty = mDirty;
if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
mAttachInfo.mSetIgnoreDirtyState = true;
mAttachInfo.mIgnoreDirtyState = true;
}
// Add the new dirty rect to the current one
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
// Intersect with the bounds of the window to skip
// updates that lie outside of the visible region
final float appScale = mAttachInfo.mApplicationScale;
final boolean intersected = localDirty.intersect(0, 0,
(int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
if (!intersected) {
localDirty.setEmpty();
}
/*ViewRootImpl同じViewParentでも、itとViewGroup invalideChildInParentの違いは以下の通りである。
* ViewRootImplダーティ領域がカウントされた後、schedualTraversalsが実行され、描画処理が行われる。*/
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
return null;
}
checkThreadは更新スレッドがuiスレッドであるかどうかをチェックします。もしdirty areaがnullであれば、ビューツリー全体を描画する必要があることを意味し、もしdirty areaがnullであるか、アニメーションが実行されていなければ、それ以上進む必要はありません。mCurScrollY が null でない場合は、ページがスクロールされたことを意味するので、それに応じてダーティ領域を再計算する必要があります。ダーティ領域は、localDrity内の現在のビューツリーのダーティ領域に追加されます。次に、現在のダーティエリアとページ全体の領域が交差されます。通常、intersectedはtrueであり、これは交差があることを意味します。
// /frameworks/base/core/java/android/view/ViewRootImpl.java
private void performDraw() {
boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
try {
draw(fullRedrawNeeded, updateTranformHint);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
private void draw(boolean fullRedrawNeeded, boolean updateTranformHint) {
Surface surface = mSurface;
if (!surface.isValid()) {//surfaceもし無効であれば
return;
}
final Rect dirty = mDirty;
//ページ全体を描画する場合は、dirtyを画面全体のサイズに設定する。
if (fullRedrawNeeded) {
attachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
//再描画領域が空でないか、アニメーションが実行されている。
if (!dirty.isEmpty() || mIsAnimating) {
//ハードウェア・アクセラレーションが有効になっている
if (attachInfo.mHardwareRenderer != null && attachInfo.mHardwareRenderer.isEnabled()) {
}else{
//ソフトウェアが描画領域を汚く描写する
if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
return;
}
}
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
boolean scalingRequired, Rect dirty) {
Canvas canvas;
try {
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
//再描画領域に従ってキャンバスを取得する。
canvas = mSurface.lockCanvas(dirty);
}catch(...){
}
try{
mView.draw(canvas);//view
}finally{
surface.unlockCanvasAndPost(canvas);
}
return true;
}
これは実際には draw(boolean fullRedrawNeeded, boolean updateTranformHint) メソッドの呼び出しであり、非ハードウェアアクセラレーションシナリオでは内部的にDrawSoftwareはこのメソッドの内部で呼び出され、描画を行います。 ここでのダーティエリアは、実際には現在のビューツリーの計算によるダーティエリアであることに注意してください。もちろん、ビューの無効化を呼び出した後に計算されたダーティ領域も含まれます。このダーティエリアをmSurfaceにクロップエリアを設定してCanvasを返すことで、その後の描画はこのCanvasのクロップエリアで行われます。
Viewのドロー描画プロセスは以下のように構成されています:
- Draw the background
- If necessary, save the canvas' layers to prepare for fading
- Draw view's content
- Draw children
- If necessary, draw the fading edges and restore layers
- Draw decorations
public void draw(Canvas canvas) {
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); //サブビューが不透明でアニメーションしないかどうか。
background.draw(canvas);
if (!dirtyOpaque) onDraw(canvas);//子ビューが不透明な場合、現在のビューを覆ってしまうのでスキップする。
dispatchDraw(canvas);
onDrawScrollBars(canvas);
}
描画処理でonDrawをコールバックするかどうかはdirtyOpaqueによって決定され、dirtyOpaqueはフラグPFLAG_DIRTY_OPAQUEが設定されているかどうかによって決定されます。このフラグは、上記のinvalidateChild?このフラグは、子ビューが不透明であり、アニメーションしていないことを示します。したがって、子ビューは親ビューの上にあり、現在のビューを上書きするので、この時点でビューを描画する必要はありません。ViewGroupは、コンテナコントロールとして、デフォルトでは何も描画せず、透明なコントロールであることに注意してください。
dispatchDrawプロセスは、サブビューを描画するために使用されます。
// /frameworks/base/core/java/android/view/ViewGroup.java
@Override
protected void dispatchDraw(Canvas canvas) {//サブビューの描画
final int count = mChildrenCount;
final View[] children = mChildren;
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);
}
}
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChildはviewの別のオーバーロードされたメソッドを呼び出します。
// /frameworks/base/core/java/android/view/View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
mPrivateFlags |= PFLAG_DRAWN;
/*quickReject現在のビューの領域がキャンバスの切り抜き領域の外にあるかどうかを判断し、外にある場合はtrueを返し、現在のビューが描画をスキップすることを示す。*/
if (!concatMatrix &&
(flags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&
(mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
/* 1. view関連付けのない行列マトリックス
* 2. view描画領域がキャンバスのトリミング領域の外にある。
* 3. clipchildtrueに設定する
* 4. アニメーションは実行されない
* **/
mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
return more;
}
if (!layerRendered) {
if (!hasDisplayList) {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);//サブビューの描画
} else {
draw(canvas);//描画そのものとそのサブビュー
}
} else {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((HardwareCanvas) canvas).drawDisplayList(displayList, null, flags);
}
}
}
drawメソッドを見てわかるように、描画処理のたびにサブビューを描画する必要はなく、特にinvalidateをトリガーにビューを描画する場合は、Canvasがダーティエリアを元に切り出し領域を設定し、ViewGroupが描画する際にサブビューの領域が切り出し領域に入っているかどうかを判断し、入っていなければ描画する必要はありません。そうでない場合は、描画する必要がないので、そのまま返します。これはキャンバスのquickRejectによって決定されます。このフラグは、View が描画される必要があるかどうかを制御するために使用され、ViewGroups にはデフォルトで設定されています。onDrawは呼び出されません。描画させたい場合は、 setWillNotDraw(false) で PFLAG_SKIP_DRAW フラグをクリアします。これにより、ビュー(ViewGroup)の描画処理に入りますが、onDrawを呼び出せるかどうかは、以下のように判断する必要があります。
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
void setFlags(int flags, int mask) {
if ((changed & DRAW_MASK) != 0) {
if ((mViewFlags & WILL_NOT_DRAW) != 0) {
if (mBackground != null) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
} else {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
} else {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
}
requestLayout();
invalidate(true);//invalidateによるリフレッシュ
}
}
ここでは、invalidate のフローを分析します。invalidate は描画処理をトリガーしますが、onMeasure と onLayout はトリガーしないことに注意してください。