blog

Android Viewの描画プロセス

Android Viewの描画処理は、Viewに関するコアな知識であり、高度なUIを理解するための必須条件です。Androidシステムでは、Windowは最も基本的なウィンドウ単位であり、各アクティビ...

May 20, 2020 · 19 min. read
シェア

はじめに

AndroidのViewの描画処理は、Viewに関連するコアな知識ポイントであり、高度なUIのために理解しなければならない前提条件です。Androidシステムにおいて、Windowは最も基本的なウィンドウ単位であり、各アクティビティは、PhoneWindowはWindowクラスの唯一の実装であり、Viewシステムであり、DecorViewの下にあるbond.WindowのActivityです。DecorViewは、FrameLayoutであり、独自のレイアウトのキャリアです。

I. ウィンドウの作成

startActivityは最終的にActivityThreadのhandleLaunchActivityメソッドを呼び出し、Activityを作成します。

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
 ....
 // Activityを作成するには、ActivityのonCreateメソッドを呼び出す。
 // こうしてウィンドウの作成が完了する
 Activity a = performLaunchActivity(r, customIntent);
 if (a != null) {
 r.createdConfig = new Configuration(mConfiguration);
 Bundle oldState = r.state;
 handleResumeActivity(r.tolen, false, r.isForward, !r.activity..mFinished && !r.startsNotResumed);
 }
}

正確な作成処理は、formLaunchActivityメソッドで以下のコードで行います:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
 	......
				// リフレクションに基づいてアクティビティを作成する
 Activity activity = null;
 try {
 java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
 activity = mInstrumentation.newActivity(
 cl, component.getClassName(), r.intent);
 } 
 			......
 try {
 Application app = r.packageInfo.makeApplication(false, mInstrumentation);
 if (activity != null) {
 // ...... 
 Window window = null;
 	......
 	// actitiyウィンドウにバインドする
 activity.attach(appContext, this, getInstrumentation(), r.token,
 r.ident, app, r.intent, r.activityInfo, title, r.parent,
 r.embeddedID, r.lastNonConfigurationInstances, config,
 r.referrer, r.voiceInteractor, window);
 }
 } 
 			......
 return activity;
 }

アクティビティのattachメソッドで、ウィンドウをアクティビティにバインドします:

final void attach(Context context, ......,Window window) {
 attachBaseContext(context);
 mFragments.attachHost(null /*parent*/);
 // PhoneWindowを作成し、アクティビティのmWindowメンバに割り当てる。
 mWindow = new PhoneWindow(this, window);
 mWindow.setWindowControllerCallback(this);
 mWindow.setCallback(this);
 mWindow.setOnWindowDismissedCallback(this);
 mWindow.getLayoutInflater().setPrivateFactory(this);
 	......
}

DecorView の作成と読み込み

final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, 	boolean reallyResume, int seq, String reason) {
 ......
 unscheduleGcIdler();
 mSomeActivitiesChanged = true;
 // アクティビティのonResumeメソッドを呼び出す
 r = performResumeActivity(token, clearHide, reason);
 if (r != null) {
 final Activity a = r.activity;
				......
 if (r.window == null && !a.mFinished && willBeVisible) {
 r.window = r.activity.getWindow();
 	// ウィンドウのDecorViewを取得する
 View decor = r.window.getDecorView();
 decor.setVisibility(View.INVISIBLE);
 	// WindowManagerはインターフェイスである。
 // また、ViewManagerインターフェイスを継承している。
 ViewManager wm = a.getWindowManager();
 WindowManager.LayoutParams l = r.window.getAttributes();
 a.mDecor = decor;
 l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 l.softInputMode |= forwardBit;
 ......
 // 実際の呼び出しはWindowManagerImplのaddViewメソッドである。
 if (a.mVisibleFromClient && !a.mWindowAdded) {
 a.mWindowAdded = true;
 wm.addView(decor, l);
 }
 ......
 } 
 ......
 }
}

WindowとDecorViewこの2つの主要な力に加えて、非常に重要な役割があるViewRootは、ViewRootは、WindowManagerとDecorViewの間のリンクであり、3つの主要なプロセスのViewは、それによって完了します。ActivityThreadでは、アクティビティオブジェクトが作成されると、DecorViewがウィンドウに追加され、同時にViewRootImplオブジェクトを作成し、ViewRootImplオブジェクトとDecorViewの関連を確立するために、関連するソースコードは次のとおりです:

// WindowManagerGlobalのaddViewメソッドでは、子Viewを描画する。
public void addView(View view, ViewGroup.LayoutParams params,
 Display display, Window parentWindow) {
 ......
 ViewRootImpl root;
 View panelParentView = null;
 synchronized (mLock) {
 ......
 // ViewRootImplインスタンスの作成
 root = new ViewRootImpl(view.getContext(), display);
 view.setLayoutParams(wparams);
 mViews.add(view);
 mRoots.add(root);
 mParams.add(wparams);
 }
	
 try {
 	// DecorViewをウィンドウにロードする
 root.setView(view, wparams, panelParentView);
 } catch (RuntimeException e) {
 // BadTokenException or InvalidDisplayException, clean up.
 synchronized (mLock) {
 final int index = findViewLocked(view, false);
 if (index >= 0) {
 removeViewLocked(index, true);
 }
 }
 throw e;
 }
}

III.ドローイングの全体的なプロセス

描画処理全体は、ViewRootクラスのformTraversals()関数で展開されます。この処理を簡単にまとめると、ビューのサイズを変更する必要があるかどうか、位置を変更する必要があるかどうか、再描画する必要があるかどうかということになります。

private void performTraversals() {
 ...
 //一番外側のViewGroupは、MATCH_PARENT,インスタンスが生成されるタイミングを決定する。
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
 	...
 //計測プロセス
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 	...
 //レイアウトの流れ
 performLayout(lp, mWidth, mHeight);
		...
 //描画処理
 performDraw();
 	...
}

おおよそのフローチャートを以下に示します:

メジャースペック

MeasureSpec は 32 ビットのプラスチック値で、上位 2 ビットが測定モード SpecMode、下位 30 ビットがテストモード SpecSize を示します。

測定モードは3つあります:

// モードを指定しなければ、親ビューは子ビューのサイズを制限しないので、子ビューはどのようなビューでもよい。
// このメソッドは通常内部で使用され、アプリケーション開発で使用されることはほとんどない。
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 精密モードでは、ビューの幅と高さは match_parentまたは特定の値を指定する
// 親ビューが子ビューの正確なサイズを決定していることを示す。
// 値はSpecSizeの値である。
public static final int EXACTLY = 1 << MODE_SHIFT;
// 最大計測モードでは、ビューの幅と高さをwrap()で指定する。_contentDecorViewツリー全体が描画されるまで。
// 子ビューのサイズは、親ビューが許容する最大サイズまでであれば、どのようなサイズでも構わない。
public static final int AT_MOST = 2 << MODE_SHIFT;

) DocerView 用 MeasureSpec の作成

//desiredWindowWidthとdesiredWindowHeightはスクリーンの寸法である。
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
private static int getRootMeaureSpec(int windowSize, int rootDimension) {
 int measureSpec;
 switch (rootDimension) {
 case ViewGroup.LayoutParams.MATRCH_PARENT:
 // Window can't resize. Force root view to be windowSize.
 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
 break;
 case ViewGroup.LayoutParams.WRAP_CONTENT 
 // Window can resize. Set max size for root view.
 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
 break
 default:
 // Window wants to be an exact size. Force root view to be that size.
 measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
 break;
 }
 return measureSpec;
}

) 子ビューの MeasureSpec の作成

子ビューの MeasureSpec 値は、子ビューのレイアウト・パラメータと親コンテナの MeasureSpec 値に基づいて計算され、具体的な計算ロジックは getChildMeasureSpec() にカプセル化されます。

/**
*
* 目標は、親コントロールの測定値と子ビューのレイアウトパラメータLayoutParamsを組み合わせて* 最も可能性の高い子*	viewDecorViewツリーの測定仕様。
* @param spec 親コントロールの測定仕様
* @param padding 親コントロールで既に占有されているサイズ
* @param childDimension child viewLayoutParamsで寸法をレイアウトする
* @return child view 測定仕様
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimesion) {
 int specMode = MeasureSpec.getMode(spec);//親コントロールの測定モード
 int specSize = MeasureSpec.getSize(spec);//親コントロールのサイズを測定する
 // paddingは、親コンテナで既に占有されているスペースの量であるため、子要素に利用可能なスペースの量は
 // サイズは、親コンテナのサイズからパディングを引いたものである。
 int size = Math.max(0, specSize - padding);
 int resultSize = 0;
 int resultMode = 0;
 switch (sepcMode) {
 // 親コントロールの測定モードが正確モードの場合、つまり正確な寸法を持つ場合
 case MeasureSpec.EXACTLY:
 		//childのlayoutパラメータが固定値の場合、例えば"layout_width" = "100dp"
						//この時、明らかに子Viewの測定仕様を決定することができ、測定サイズは100dpであり、測定モードもEXACTLYである。
 if (childDimension >= 0) {
 resultSize = childDimension;
 resultMode = MeasureSpec.EXACTLY;
 } 
 		//childのlayoutパラメータが"match_parent"つまり、親コントロールの
 		//この時点で、親コントロールは精密モードになっている。つまり、親コントロールは自身のサイズを決定でき、子コントロールも自身のサイズを決定できる。
 		else if (childDimension == LayoutParams.MATCH_PARENT) {
 // Child wants to be our size. So be it.
 resultSize = size;
 resultMode = MeasureSpec.EXACTLY;
 } 
 		//childのlayoutパラメータが"wrap_content"つまり、独自のロジックでサイズを決めたい場合は
 		//例えば、TextViewは、設定した文字列のサイズに基づいてサイズを決定する。
						//サイズは自分で決めることができるが、親コントロールのサイズより大きくすることはできない。
						//つまり、測定パターンはAT_MOST親コントロールのサイズは、親コントロールのサイズである。
 		else if (childDimesion == LayoutParams.WRAP_CONTENT) {
 resultSize = size;
 resultMode = MeasureSpec.AT_MOST;
 }
 break;
 // 親コントロールの測定モードが最大モードの場合、つまり、親コントロールは自身のサイズをまだ知らないが、サイズがsize
 case MeasureSpec.AT_MOST:
 		//同様に、親コントロールはまだ自分のサイズを知らないが、子コントロールは自分のサイズを決定できるため、子コントロールのニーズを優先する。
 if (childDimension >= 0) {
 resultSize = childDimension;
 resultMode = MeasureSpec.EXACTLY;
 } 
 		//child親コントロールと同じサイズにしたいが、親はそのサイズがわからないので、子もそのサイズがわからない。
 		//しかし、繰り返すが、子のサイズの上限は、親コントロールのサイズの上限でもある。
 		else if (childDimension == LayoutParams.MATCH_PARENT) {
 resultSize = size;
 resultMode = MeasureSpec.AT_MOST;
 } 
 		//child独自のロジックでサイズを決めたい場合は、親コントロールを超えない範囲で独自にサイズを決める。
 		else if (childDimension == LayoutParams.WRAP_CONTENT) {
 resultSize = size;
 resultMode = MeasureSpec.AT_MOST;
 }
 break;
 // Parent asked to see how big we want to be
 case MeasureSpec.UNSPECIFIED:
 if (childDimension >= 0) {
 // Child wants a specific size... let him have it
 resultSize = childDimension;
 resultMode = MeasureSpec.EXACTLY;
 } else if (childDimension == LayoutParams.MATCH_PARENT) {
 // Child wants to be our size... find out how big it should be
 resultSize = 0;
 resultMode = MeasureSpec.UNSPECIFIED;
 } else if (childDimension == LayoutParams.WRAP_CONTENT) {
 // Child wants to determine its own size....
 // find out how big it should be
 resultSize = 0;
 resultMode == MeasureSpec.UNSPECIFIED;
 }
 break;
 }
 return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
  1. ビューの幅と高さが固定されている場合、ビューの MeasureSpec は正確で、親コンテナの MeasureSpec に関係なく、Layoutparams のサイズに従います;
  2. ビューの幅と高さが match_parent であるとき、親コンテナのモードが精密モードであれば、ビューも精密モードとなり、そのサイズは親コンテナの残りのスペースとなります;
  3. ビューの幅と高さがwrap_contentである場合、ビューのモードは常に最大化され、親コンテナのモードが正確であるか最大化されているかに関係なく、そのサイズは親コンテナの残りのスペースを超えることはできません。
  4. Unspecifiedモード、このモードは主にシステム内に複数のMEASUREMENTSがある場合に使用され、一般的には気にする必要はありません。

measure

テストの流れは ViewRootImpl.performMeasure() で始まります:

//=============ViewRootImpl.java==============
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
 ......
 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 ......
}

ここでのmViewには、ViewRootImplのsetViewメソッドで値が代入されます。

//=============ViewRootImpl.java==============
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
 	...
 // ここでのビューは、ActivityThreadのaddViewメソッドで渡されたもので、DecorViewである。
 mView = view;
 		....
 // ここでのattrsは、ActivityThreadのaddViewメソッドで渡されたものである。
 mWindowAttributes.copyFrom(attrs);
 		...
}
 

ご覧のように、ViewRootImplのperformMeasureメソッドから始まり、DecorViewのmeasure()操作から、Viewのmeasure()メソッドへとコードをたどっていく、一番最初のテストフローです:

//=============View.java==============
/**
 * <p>
 * This is called to find out how big a view should be. The parent
 * supplies constraint information in the width and height parameters.
 * </p>
 *
 * <p>
 * The actual measurement work of a view is performed in
 * {@link #onMeasure(int, int)}, called by this method. Therefore, only
 * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
 	viewツリーの実際の計測はonMeasure()メソッドで行われる。
 * </p>
 *
 *
 * @param widthMeasureSpec Horizontal space requirements as imposed by the
 * parent
 * @param heightMeasureSpec Vertical space requirements as imposed by the
 * parent
 *
 * @see #onMeasure(int, int)
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
 ....
 // measure ourselves, this should set the measured dimension flag back
 onMeasure(widthMeasureSpec, heightMeasureSpec);
 .... 
}

このおなじみのonMeasure(int,int)メソッドをもう一度見てください:

//=============View.java==============
**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension(int, int)このメソッドをオーバーライドする場合、View の幅と高さの寸法を保存するメソッドを呼び出す必要があります。このメソッドの呼び出しは、測定フェーズの終了も意味します。

ViewGroup の測定では、主に measureChild メソッドを使用します:

/**
 * すべての子Viewに、子ViewのMeasureSpecとパディングを考慮して、自分自身を測定するように要求する。.
 * 状態がGONEの子Viewはすべてスキップされ、主な作業はgetChildMeasureSpecメソッドで処理される。
 
 * @param widthMeasureSpec The width requirements for this view
 * @param heightMeasureSpec The height requirements for this view
 */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
 final int size = mChildrenCount;
 final View[] children = mChildren;
 for (int i = 0; i < size; ++i) {
 final View child = children[i];
 if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
 measureChild(child, widthMeasureSpec, heightMeasureSpec);
 }
 }
}
protected void measureChild(View child, int parentWidthMeasureSpec,
 int parentHeightMeasureSpec) {
 final LayoutParams lp = child.getLayoutParams();
 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
 mPaddingLeft + mPaddingRight, lp.width);
 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
 mPaddingTop + mPaddingBottom, lp.height);
 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
/** 
 * このメソッドでは、各子ビューのMeasureSpecを計算する。
 * このメソッドは前のサブセクション"子ビューの MeasureSpec を作成する"これについては
 * 目標は、ChildViewのMeasureSpecとLayoutParamsを組み合わせて、最適な結果を得ることである。
 *
 * @param spec このViewを描画するための要件
 * @param padding 現在の唯一のView上の現在のViewのパディングは、マージンを含むこともできる。
 *
 * @param childDimension 現在の次元での特定の参照
 * @return 子ビューのMeasureSpec
 */
 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
 .........
 // 子ビューの測定要件とサイズに基づいて、子ビューの MeasureSpec を作成する。
 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
 }

そして、冒頭で述べたViewのmeasure()->onMeasure()->setDimention()メソッドに戻ります。

layout

ViewクラスのonLayout()メソッドはオーバーライドする必要はありませんが、ViewGroupのonLayoutは抽象メソッドですので、このメソッドを実装する必要があります。

レイアウトプロセスは、測定された寸法を通してビューのmMeasuredWidthとmMeasuredHeightを取得し、子Viewのlayout(l,t,r,b)メソッドを通して親レイアウトにおける子Viewの相対位置を決定します。

ViewRootImplのformLayoutメソッドから始めます:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
 int desiredWindowHeight) {
 ···
 //hostこれがsetViewメソッドで渡されたDecorViewである。
 final View host = mView;
 ···
 try {
 // DecorViewのlayoutメソッドが呼び出されるが、これはViewGroupのlayoutメソッドでもある。
 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
 ···
 if (numViewsRequestingLayout > 0) {
 ···
 if (validLayoutRequesters != null) {
 ···
 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
 ···
 }
 }
 } finally {
 ···
 }
 ···
}

次にViewGroupのレイアウトを見てください:

@Override
// このメソッドはfinalによって変更され、measureと同様に、変更やオーバーライドはできない。
public final void layout(int l, int t, int r, int b) {
 if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
 if (mTransition != null) {
 mTransition.layoutChange(this);
 }
 	//Viewのlayoutメソッドは直接呼び出される。
 super.layout(l, t, r, b);
 } else {
 // record the fact that we noop'd it; request layout when transition finishes
 mLayoutCalledWhileSuppressed = true;
 }
}

ビューのレイアウトメソッドをもう一度見てみましょう

public void layout(int l, int t, int r, int b) {
 ···
 boolean changed = isLayoutModeOptical(mParent) ?
 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
 onLayout(changed, l, t, r, b);
 ···
 }
 ···
}

setOpticalFrame(l,t,r,b)ご覧のように、実際のレイアウトメソッドは、setFrame(l,t,r,b)メソッドと、setOpticalFrameの中にあるsetFrame(l,t,r,b)メソッドを呼び出します。このメソッドの4つのパラメータは、親Viewでの位置を決定します。

setFrame(l,t,r,b)メソッドには、ビューを再描画する必要があるかどうかを判断するためのブール値が設定されます。

子 View のレイアウトは、すべて onLayout() メソッドで行われます。

また、レイアウト処理は再帰的に行われ、子Viewがまだ親Viewである場合、子Viewのonlayout nullメソッドに遭遇するまでレイアウトを続け、子Viewのレイアウト処理は終了します。

ドロー・プロセス

やはり ViewRootImpl の performDraw() メソッドから始めます:

// ----------------ViewRootImpl---------------
private void performDraw() {
 ...
 try {
 draw(fullRedrawNeeded);
 } finally {
 ...
 }
	...
}
private void draw(boolean fullRedrawNeeded) {
 Surface surface = mSurface;
 if (!surface.isValid()) {
 return;
 }
		.......
 if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
 if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
 ......
 } else {
 ......
 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
 return;
 }
 }
 }
 	......
}
 private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty) {
								......
 // このmViewはDecorViewとも呼ばれる。
 mView.draw(canvas);
 					......
 }

つまり、DecorViewのdraw(canvas)メソッドが最終的に呼び出されます。

// ----------------DecorView---------------
@Override
 public void draw(Canvas canvas) {
 	// 親クラスのdrawメソッドを呼び出す
 super.draw(canvas);
 if (mMenuBackground != null) {
 mMenuBackground.draw(canvas);
 }
}

super.draw(canvas)メソッドをトレースすると、最終的にViewのdrawメソッドにジャンプします。

 /**
 * Manually render this view (and all of its children) to the given Canvas.
 * The view must have already done a full layout before this function is
 * called. When implementing a view, implement
 * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
 * If you do need to override this method, call the superclass version.
 *
 * @param canvas The Canvas to which the View is rendered.
 */
 @CallSuper
 public void draw(Canvas canvas) {
 final int privateFlags = mPrivateFlags;
 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
 mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
 /*
 * Draw traversal performs several drawing steps which must be executed
 * in the appropriate order:
 *
 * 1. Draw the background
 * 2. If necessary, save the canvas' layers to prepare for fading
 * 3. Draw view's content
 * 4. Draw children
 * 5. If necessary, draw the fading edges and restore layers
 * 6. Draw decorations (scrollbars for instance)
 */
 // Step 1, draw the background, if needed
 int saveCount;
 if (!dirtyOpaque) {
 drawBackground(canvas);
 }
 // skip step 2 & 5 if possible (common case)
 final int viewFlags = mViewFlags;
 boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
 boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
 if (!verticalEdges && !horizontalEdges) {
 // Step 3, draw the content
 if (!dirtyOpaque) onDraw(canvas);
 // Step 4, draw the children
 dispatchDraw(canvas);
 drawAutofilledHighlight(canvas);
 // Overlay is part of the content and draws beneath Foreground
 if (mOverlay != null && !mOverlay.isEmpty()) {
 mOverlay.getOverlayView().dispatchDraw(canvas);
 }
 // Step 6, draw decorations (foreground, scrollbars)
 onDrawForeground(canvas);
 // Step 7, draw the default focus highlight
 drawDefaultFocusHighlight(canvas);
 if (debugDraw()) {
 debugDrawFocus(canvas);
 }
 // we're done...
 return;
 }

注釈でお分かりのように、一般的な描画プロセスは7つのステップに分かれていますが、ステップ2と5は通常、大まかなステップごとに無視してかまいません:

ステップ1. 背景の描画; ステップ2. スキップの無視; ステップ3. コンテンツの描画; ステップ4. サブビューの描画; ステップ5. スキップの無視; ステップ6. 装飾の描画; ステップ7. デフォルトフォーカスハイライトの描画。

各サブビューの具体的な内容は同じではありませんので、あなたは、独自のロジックを実装する必要があり、カスタムビューは、一般的にメソッドを書き換える必要があり、メソッドは空のメソッドです、つまり、3番目のステップonDraw()を見ることができます。

4番目のステップ、dispatchDraw(canvas)。

// ----- View.java -----
/**
 * Called by draw to draw the child views. This may be overridden
 * by derived classes to gain control just before its children are drawn
 * (but after its own view has been drawn).
 * @param canvas the canvas on which to draw the view
 */
 protected void dispatchDraw(Canvas canvas) {
 }

Viewのメソッドは空の実装であることがわかりますが、コメントには、ViewにサブViewが含まれている場合は、メソッドを実装し、メソッドのViewGroupに移動して確認する必要があることが非常に明確に書かれています:

// ------ViewGroup---------
@Override
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;
 }
 }
		....
}

ご覧のように、子Viewを走査する際、drawChildViewメソッドを呼び出し、最終的に子Viewのdrawメソッドを呼び出します。同様に、子Viewも子Viewを持っている場合、drawChild()メソッドを呼び出すために下へトラバースし続け、子Viewを描画し続け、最後にリーフViewまで、そしてDecorViewツリー全体の描画まで再帰的に描画します。

Read next

Glide読み込み Gifラグ最適化アイデア分析

グライド画像フレームワークは、直接GIF画像をロードすることができますが、銀行協力プロジェクトを行うには、ページを出荷する必要があるため、GIF画像をロードする必要がありますが、GIF画像をロードするグライドフレームワークの使用で見つかった、明らかに遅延があることがわかりました。 グライドロードGIF画像のソースコードを確認した後、その:ロードGIF画像フレーム内のグライド、レンダリングの最後のフレームだけでなく、次の...

May 20, 2020 · 16 min read