AndroidViewGroupのdrawとonDrawを呼び出すタイミング
View.draw
そしてView.onDraw
、呼び出し関係
まず、View.draw
とView.onDraw
は2つの異なるメソッドであり、View.draw
呼び出された場合View.onDraw
にのみ呼び出すことができます。でView.draw
、コードの次のセクションがあります。
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);//是否是实心控件
if (!dirtyOpaque) {
drawBackground(canvas);//绘制背景
}
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);//调用onDraw复制代码
上記のコードから、次のことがわかります。
View.draw
メソッドが呼び出すView.onDraw
- それ
dirtyOpaque
がfalse(透明で、固体ではない)の場合にのみ、View.onDraw
メソッドが呼び出されます。
したがって、ViewGroup.onDraw
メソッドを呼び出す場合は、次の2つの条件を満たす必要があります。
ViewGroup.draw
呼び出されるメソッドを取得してみてください- しましょう
draw
方法はdirtyOpaque
偽です。
私たちは、について話しましたので、View.draw
とView.onDraw
単純にここでは2つの下の違いを。Viewソースコードを見ると、View.draw
基本的に6つのステップで構成されていることがわかります。
- 背景の描画は、
View.drawBackground
メソッドを介して実現されます。 - 必要に応じて、キャンバスのレイヤーを保存してフェードの準備をし
Canvas.saveLayer
ます。必要に応じて、キャンバスのレイヤーを保存してフェードの準備をします。 View.onDraw
コンテンツの描画はメソッドを介して行われます。通常、ビューのカスタマイズではこのメソッドを使用してコンテンツを描画します。Canvasを入手したら、任意のコンテンツを描画して、パーソナライズされたカスタマイズを実現できます。- 子の
View.dispatchDraw
描画はメソッドを介して実行され、ViewGroupはこのメソッドを実装して独自の子ビューを描画します。 - 必要に応じて、フェードエッジを描画し、レイヤーを復元します。必要に応じて、フェードインおよびフェードアウトに関連するコンテンツを描画し、以前に保存したキャンバスレイヤー(レイヤー)を復元します。
View.onDrawScrollBars
描画装飾(スクロールバー)はメソッドによって実装され、スクロールバーを描画する操作はここで実装されます。
簡単に言えばView.draw
、現在のビューのすべてのコンテンツとサブビューのコンテンツの描画を担当する完全なセットです。またView.onDraw
、サブセットであるそれ自体に関連するコンテンツの描画のみを担当します。
ViewGroup.draw
いつ電話するか
実際、これはView.draw
呼び出しのタイミングでもあります。Viewソースコードを見るとView.draw
、View.draw
以下に示すように、3パラメーターメソッドで単一パラメーターメソッドが呼び出されることがわかります。
if (!hasDisplayList) {
//软件绘制
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
//跳过当前View的绘制,直接绘制子view
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
//此时坐标系已经切换到View自身坐标系了,可以纯碎的绘制当前view了,又回到了draw(canvas)
draw(canvas);
}
}
ソフトウェア描画では、3つのパラメーターはView.draw
、ビューの座標系を親ビューから現在のビューに切り替えてから、現在のビューに渡して描画します。一般に、描画する現在のビューへの引き渡しは、単一パラメーターView.draw
メソッドを呼び出すことによって実現されます。
ただし、最適化ロジックがあります。現在のビューを描画する必要がない場合(マークPFLAG_SKIP_DRAW
)、dispatchDraw
現在のビューの子ビューはメソッドを介して直接描画されます。
したがって、ViewGroup.draw
メソッドが呼び出されるかどうかは、mPrivateFlagsにPFLAG_SKIP_DRAW
フラグが含まれているかどうかに完全に依存します。
- mPrivateFlagsが含まれている場合
PFLAG_SKIP_DRAW
、現在のビューのdrawメソッドはスキップされ、メソッドが直接呼び出されてdispatchDraw
、現在のビューの子ビューが描画されます。 mPrivateFlags
含まれていない場合PFLAG_SKIP_DRAW
、現在のビューはdraw
メソッド、つまりすべてのコンテンツの描画を呼び出します。
それでPFLAG_SKIP_DRAW
、それはどのような要因に依存しますか?
setWillNotDraw
ビューにはsetWillNotDraw
メソッドがあります。コメントの観点からはView.draw
、最適化のためにメソッドをスキップするかどうかを制御することです。メソッドを見てみましょう:
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
この方法は非常に単純です。引き続きsetFlags
方法を検討します。
void setFlags(int flags, int mask) {
int old = mViewFlags;
//设置flags
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
int changed = mViewFlags ^ old;
//若mViewFlags前后没有变化,则直接返回
if (changed == 0) {
return;
}
int privateFlags = mPrivateFlags;
...
if ((changed & DRAW_MASK) != 0) {
if ((mViewFlags & WILL_NOT_DRAW) != 0) {
//mViewFlags设置了WILL_NOT_DRAW标志
if (mBseackground != null) {
//如果当前View有背景,那么取消mPrivateFlags的PFLAG_SKIP_DRAW标志,但是设置另外一个PFLAG_ONLY_DRAWS_BACKGROUND标志
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
} else {
//如果当前View没有背景,那么直接设置PrivateFlags的PFLAG_SKIP_DRAW标志
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
} else {
//因为mViewFlags没有设置WILL_NOT_DRAW标志,所以取消mPrivateFlags的PFLAG_SKIP_DRAW标志
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
}
requestLayout();
invalidate(true);
}
}
上記のコードから、ロゴをmPrivateFlags
設定するにはPFLAG_SKIP_DRAW
、次の2つの条件を満たす必要があることがわかります。
- ターゲット
mViewFlags
、WILL_NOT_DRAW
フラグを設定 - 現在
View
、背景画像はありません
PasssetWillNotDraw(true)
は、mViewFlagsのWILL_NOT_DRAW
フラグを設定する必要があります。この時点で現在のビューの背景画像がない場合は、フラグがmPrivateFlags
設定されPFLAG_SKIP_DRAW
ます。
ただし、この時点で現在のビューに背景画像がある場合、ロゴはキャンセルされ、同時にmPrivateFlags
別のPFLAG_SKIP_DRAW
ロゴが設定されPFLAG_ONLY_DRAWS_BACKGROUND
ます。setWillNotDraw
このメソッドの関連ロジックを次の図に示します。
setWillNotDraw
背景を設定する
ここに質問があります。操作中の場合、現在のビューの背景をキャンセルすると、現在のビューでマークをmPrivateFlags
リセットPFLAG_SKIP_DRAW
しますか?
回答:はい、これはまさにPFLAG_ONLY_DRAWS_BACKGROUND
ロゴの機能です。
View.setBackgroundDrawable
メソッドの実装を見てみましょう。
public void setBackgroundDrawable(Drawable background) {
if (background == mBackground) {
return;
}
if (background != null) {
...
mBackground = background;
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
//若当前View既设置PFLAG_SKIP_DRAW,又添加了背景,那么只能取消mPrivateFlags的PFLAG_SKIP_DRAW标志,同时替换成PFLAG_ONLY_DRAWS_BACKGROUND,这和setFlags方法里面的逻辑一致
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
}
}else{
//这里取消了背景图
mBackground = null;
if ((mPrivateFlags & PFLAG_ONLY_DRAWS_BACKGROUND) != 0){
/*
* This view ONLY drew the background before and we're removing
* the background, so now it won't draw anything
* (hence we SKIP_DRAW)
*/
//如果mPrivateFlags包含PFLAG_ONLY_DRAWS_BACKGROUND标志,说明之前mViewFlags设置了WILL_NOT_DRAW标志,但是因为之前当前View有背景图,那么只能先设置PFLAG_ONLY_DRAWS_BACKGROUND标志。现在当前View的背景图取消了,所以可以重新对mPrivateFlags设置PFLAG_SKIP_DRAW了
mPrivateFlags &= ~PFLAG_ONLY_DRAWS_BACKGROUND;
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
}
}
上記のコードのコメントはそれを明らかにしました。現在のビューの背景画像がキャンセルされた場合、システムがします置き換えるロゴmPrivateFlags
とPFLAG_ONLY_DRAWS_BACKGROUND
、再びロゴがPFLAG_SKIP_DRAW
。setBackgroundDrawable
このメソッドの関連ロジックを次の図に示します。
setBackgroundDrawable
この時点PFLAG_SKIP_DRAW
で、兆候の分析は終了しました。最初の質問に戻ります。デフォルトでViewGroup.draw
(ViewGroup.onDraw)メソッドが呼び出されないのはなぜですか。上記の比較分析では、次のことがわかります。確かViewGroup
にmPrivateFlags
マークされたPFLAG_SKIP_DRAW
フラグでは、どこに設定するのでしょうか。
元のデフォルトである初期化時のViewGroupでは、次のコードでフラグをmViewFlags
設定しWILL_NOT_DRAW
ます。また、デフォルトでは、ViewGroupには背景画像がないため、ViewGroupはmPrivateFlags
マークされていPFLAG_SKIP_DRAW
ます。その結果、ViewGroup.draw
メソッドは呼び出されないため、ViewGroup.onDraw
メソッドは呼び出されません。
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
...
}
要約すると
View.draw
、メソッドが呼び出されるかどうかを決定する直接的な要因**View.mPrivateFlags
は、PFLAG_SKIP_DRAWフラグ**を含めるかどうか、およびこのフラグを含めるには、2つの条件を同時に満たす必要があります。
View.mViewFlags
WILL_NOT_DRAW
ロゴが入っており、ロゴをView.setWillNotDraw(true)
設定できます。- 現在のビューには背景画像がありません。
したがって、ViewGroup.draw
呼び出されたい場合は、上記の条件のいずれかを破る必要があります。- 呼び出し
View.setWillNotDraw(false)
キャンセル、ロゴView.mViewFlags
**WILL_NOT_DRAW**
ViewGroup
背景画像を設定するには
ViewGroup.onDraw
いつ電話するか
以上のことから、ViewGroup.draw
呼び出されてもViewGroup.onDraw
必ずしも呼び出されないことがわかります。呼び出される前に、それがソリッドコントロールではない(View.mPrivateFlagsがマークされていないPFLAG_DIRTY_OPAQUE
)ViewGroup.onDraw
ことを確認する必要があります。
堅実なコントロール:コントロールのonDrawメソッドは、このコントロールのすべての領域が、描画するコンテンツによって完全にカバーされることを保証できます。つまり、このコントロールの対象となるコンテンツは、このコントロールが属する領域からは見えません。つまり、半透明の部分も空の部分もありません。
したがってView.mPrivateFlags
、どのような状況でマークされPFLAG_DIRTY_OPAQUE
ますか。ソースコードを見ると、関連するロジックがViewGroup.invalidateChild
メソッドに含まれていることがわかります。
//这里的child表示直接调用invalidate的子View。
public final void invalidateChild(View child, final Rect dirty) {
//计算子View是否是实心的
final boolean isOpaque = child.isOpaque() && !drawAnimation && child.getAnimation() == null && childMatrix.isIdentity();
//PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
do {
//循环遍历到ViewRootImpl为止
View view = null;//父View
if (parent instanceof View) {
view = (View) parent;
}
if (view != null) {
//给当前父View打上相应的flag
//父View若包含FADING_EDGE_MASK标识,那么只能打上FLAG_DIRTY标识,表示会调用ViewGroup.onDraw方法
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
//PFLAG_DIRTY和PFLAG_DIRTY_OPAQUE是互斥的
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
...
}
上記のコードから、View.invalidate
メソッドがトレースバックされることがわかりますViewRootImpl
。このプロセス中に、子コントロールがソリッドの場合、現在の親コントロールはPFLAG_DIRTY_OPAQUEとしてマークされ、それ以外の場合はPFLAG_DIRTYになります。PFLAG_DIRTY_OPAQUEフラグ
を含むコントロールの場合、メソッド(背景の描画)とメソッド(コンテンツの描画)は描画プロセス中にスキップされます。drawBackground
onDraw
ビューがソリッドであるかどうかの判断は完全にisOpaque
メソッドに依存し、メソッドのデフォルトの実装は、ビューにIDView.mPrivateFlags
が含まれているかどうかを確認することPFLAG_OPAQUE_MASK
です。PFLAG_OPAQUE_MASK
ロゴ(実線)は、PFLAG_OPAQUE_BACKGROUND
(背景実線)とPFLAG_OPAQUE_SCROLLBARS
(スクロールバー実線)で構成されています。つまり、ビューが背景ソリッドとスクロールバーソリッドを同時に満たす場合にのみ、不透明になります。
ビューがソリッドであるかどうかを計算する実際の方法はcomputeOpaqueFlags
次のとおりです。
protected void computeOpaqueFlags() {
// Opaque if:
// - Has a background
// - Background is opaque
// - Doesn't have scrollbars or scrollbars overlay
//若View包含背景,且背景是不透明的,则打上PFLAG_OPAQUE_BACKGROUND标识
if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
}
final int flags = mViewFlags;
//若没有横竖滚动条,或者滚动条是OVERLAY类型的,则打上PFLAG_OPAQUE_SCROLLBARS标识
if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
}
}
PFLAG_OPAQUE_BACKGROUND
とPFLAG_OPAQUE_SCROLLBARS
マークが同時にマークされている場合にのみ、現在のビューは塗りつぶされます。
このメソッドは、ビューの多くの場所で呼び出され、ビューがソリッドであるかどうかをリアルタイムで判断します。
もちろん、isOpaque
メソッドのデフォルトの実装が私たちのニーズを満たさない場合は、自分で実装することもできます。これは、公式に推奨されるアプローチでもあります。
デモ検証
以下では、デモを通じて上記のロジックを検証します。
- カスタムの親ViewGroupAと子ViewBを設定します。
- 親ViewGroupAでsetWillNotDraw(false)を呼び出して、親ViewGroupAの描画メソッドが呼び出されるようにします。
- Clickイベントを子ViewBに設定します。具体的な実装は、子ViewB.invalidateメソッドを呼び出すことです。
- 子ViewBをクリックして、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されるかどうかを確認します。
上記のデモを有効にするには、ソフトウェアで作成する必要があります。ハードウェア描画では、子ViewBがinvalidateメソッドを呼び出すと、子ViewB自体の描画メソッドのみがトリガーされ、その親ビューを再描画する必要はありません。
子ViewBに単色の背景を設定すると(子ViewBが単色になります)、次の結論を導き出すことができます。
- ビューツリーが初めてレンダリングされるときに、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されます。
- 続いて子ViewBをクリックすると、子ViewBのdrawメソッドとonDrawメソッドが呼び出され、親ViewGroupAのdrawメソッドも呼び出されますが、親ViewGroupAのonDrawメソッドは呼び出されません。
子ViewBの背景を設定しない場合(子ViewBが非ソリッドになる)、次の結論を導き出すことができます。
- ビューツリーが初めてレンダリングされるときに、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されます。
- その後、子ViewBをクリックすると、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されます。
もちろん、ビューがソリッドであるかどうかを制御するために、isOpaque
メソッドを直接書き直すこともできます。上記のように面倒なことは必要ありません。
要約すると、ビューツリーが初めてレンダリングされるとき、ViewGroup.drawメソッドが呼び出されている限り、ViewGroup.onDrawが呼び出されます。
ただし、後続の子View.invalidateが呼び出されると、ViewGroup.drawメソッドが呼び出され、子Viewが非ソリッドであるという前提で、ViewGroup.onDrawとViewGroup.drawBackgroundが呼び出されます。
総括する
最後に、画像を使用して、ViewGroupのdrawメソッドとonDrawメソッドの呼び出しロジック図を要約します。