AndroidViewGroupのdrawとonDrawを呼び出すタイミング

AndroidViewGroupのdrawとonDrawを呼び出すタイミング

View.drawそしてView.onDraw、呼び出し関係

まず、View.drawView.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复制代码

上記のコードから、次のことがわかります。

  1. View.drawメソッドが呼び出すView.onDraw
  2. それdirtyOpaqueがfalse(透明で、固体ではない)の場合にのみView.onDrawメソッドが呼び出されます。

したがって、ViewGroup.onDrawメソッドを呼び出す場合は、次の2つの条件を満たす必要があります。

  1. ViewGroup.draw呼び出されるメソッドを取得してみてください
  2. しましょうdraw方法はdirtyOpaque偽です。

私たちは、について話しましたので、View.drawView.onDraw単純にここでは2つの下の違いを。Viewソースコードを見ると、View.draw基本的に6つのステップで構成されていることがわかります。

  1. 背景の描画は、View.drawBackgroundメソッドを介して実現さます。
  2. 必要に応じて、キャンバスのレイヤーを保存してフェードの準備をしCanvas.saveLayerます。必要に応じて、キャンバスのレイヤーを保存してフェードの準備をします。
  3. View.onDrawコンテンツの描画はメソッドを介して行われます。通常、ビューのカスタマイズではこのメソッドを使用してコンテンツを描画します。Canvasを入手したら、任意のコンテンツを描画して、パーソナライズされたカスタマイズを実現できます。
  4. 子のView.dispatchDraw描画メソッドを介して実行され、ViewGroupはこのメソッドを実装して独自の子ビューを描画します。
  5. 必要に応じて、フェードエッジを描画し、レイヤーを復元します。必要に応じて、フェードインおよびフェードアウトに関連するコンテンツを描画し、以前に保存したキャンバスレイヤー(レイヤー)を復元します。
  6. View.onDrawScrollBars描画装飾(スクロールバー)はメソッドによって実装さ、スクロールバーを描画する操作はここで実装されます。

簡単に言えばView.draw、現在のビューのすべてのコンテンツとサブビューのコンテンツの描画を担当する完全なセットです。またView.onDraw、サブセットであるそれ自体に関連するコンテンツの描画のみを担当します。

ViewGroup.drawいつ電話するか

実際、これはView.draw呼び出しのタイミングであります。Viewソースコードを見るView.drawView.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フラグが含まれているかどうかに完全に依存します

  1. mPrivateFlagsが含まれている場合PFLAG_SKIP_DRAW、現在のビューのdrawメソッドはスキップされ、メソッドが直接呼び出されてdispatchDraw、現在のビューの子ビューが描画されます。
  2. 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つの条件を満たす必要があることがわかります

  1. ターゲットmViewFlagsWILL_NOT_DRAWフラグを設定
  2. 現在View、背景画像はありません

PasssetWillNotDraw(true)は、mViewFlagsのWILL_NOT_DRAWフラグを設定する必要があります。この時点で現在のビューの背景画像がない場合は、フラグmPrivateFlags設定されPFLAG_SKIP_DRAWます。
ただし、この時点で現在のビューに背景画像がある場合、ロゴはキャンセルされ、同時にmPrivateFlags別のPFLAG_SKIP_DRAWロゴ設定されPFLAG_ONLY_DRAWS_BACKGROUNDます。setWillNotDrawこのメソッドの関連ロジックを次の図に示します。

setWillNotDrawsetWillNotDraw

背景を設定する

ここに質問があります。操作中の場合、現在のビューの背景をキャンセルすると、現在のビューでマークを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;
    }
}
}

上記のコードのコメントはそれを明らかにしました。現在のビューの背景画像がキャンセルされた場合、システムがします置き換えるロゴmPrivateFlagsPFLAG_ONLY_DRAWS_BACKGROUND、再びロゴがPFLAG_SKIP_DRAWsetBackgroundDrawableこのメソッドの関連ロジックを次の図に示します。

setBackgroundDrawablesetBackgroundDrawable

この時点PFLAG_SKIP_DRAWで、兆候の分析は終了しました。最初の質問に戻ります。デフォルトでViewGroup.draw(ViewGroup.onDraw)メソッドが呼び出されないのはなぜですか。上記の比較分析では、次のことがわかります。確かViewGroupmPrivateFlagsマークされた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つの条件を同時に満たす必要があります。

  1. View.mViewFlagsWILL_NOT_DRAWロゴが入っており、ロゴをView.setWillNotDraw(true)設定できます
  2. 現在のビューには背景画像がありません。
    したがって、ViewGroup.draw呼び出されたい場合は、上記の条件のいずれかを破る必要があります。
  3. 呼び出しView.setWillNotDraw(false)キャンセル、ロゴView.mViewFlags**WILL_NOT_DRAW**
  4. ViewGroup背景画像設定するには

ViewGroup.onDrawいつ電話するか

以上のことから、ViewGroup.draw呼び出されてもViewGroup.onDraw必ずしも呼び出されないことがわかります。呼び出される前に、それがソリッドコントロールではない(View.mPrivateFlagsがマークされていないPFLAG_DIRTY_OPAQUEViewGroup.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フラグ
を含むコントロールの場合、メソッド(背景の描画)とメソッド(コンテンツの描画)は描画プロセス中にスキップされますdrawBackgroundonDraw

ビューがソリッドであるかどうかの判断は完全に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_BACKGROUNDPFLAG_OPAQUE_SCROLLBARSマークが同時にマークされている場合にのみ、現在のビューは塗りつぶされます。
このメソッドは、ビューの多くの場所で呼び出され、ビューがソリッドであるかどうかをリアルタイムで判断します。
もちろん、isOpaqueメソッドのデフォルトの実装が私たちのニーズを満たさない場合は、自分で実装することもできます。これは、公式に推奨されるアプローチでもあります。

デモ検証

以下では、デモを通じて上記のロジックを検証します。

  1. カスタムの親ViewGroupAと子ViewBを設定します。
  2. 親ViewGroupAでsetWillNotDraw(false)を呼び出して、親ViewGroupAの描画メソッドが呼び出されるようにします。
  3. Clickイベントを子ViewBに設定します。具体的な実装は、子ViewB.invalidateメソッドを呼び出すことです。
  4. 子ViewBをクリックして、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されるかどうかを確認します。

上記のデモを有効にするには、ソフトウェアで作成する必要があります。ハードウェア描画では、子ViewBがinvalidateメソッドを呼び出すと、子ViewB自体の描画メソッドのみがトリガーされ、その親ビューを再描画する必要はありません。

子ViewBに単色の背景を設定すると(子ViewBが単色になります)、次の結論を導き出すことができます。

  1. ビューツリーが初めてレンダリングされるときに、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されます。
  2. 続いて子ViewBをクリックすると、子ViewBのdrawメソッドとonDrawメソッドが呼び出され、親ViewGroupAのdrawメソッドも呼び出されますが、親ViewGroupAのonDrawメソッドは呼び出されません

子ViewBの背景を設定しない場合(子ViewBが非ソリッドになる)、次の結論を導き出すことができます。

  1. ビューツリーが初めてレンダリングされるときに、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されます。
  2. その後、子ViewBをクリックすると、親ViewGroupAと子ViewBのdrawメソッドとonDrawメソッドが呼び出されます。

もちろん、ビューがソリッドであるかどうかを制御するために、isOpaqueメソッドを直接書き直すこともできます。上記のように面倒なことは必要ありません。

要約すると、ビューツリーが初めてレンダリングされるとき、ViewGroup.drawメソッドが呼び出されている限り、ViewGroup.onDrawが呼び出されます。
ただし、後続の子View.invalidateが呼び出されると、ViewGroup.drawメソッドが呼び出され、子Viewが非ソリッドであるという前提で、ViewGroup.onDrawとViewGroup.drawBackgroundが呼び出されます。

総括する

最後に、画像を使用して、ViewGroupのdrawメソッドとonDrawメソッドの呼び出しロジック図を要約します。

ViewGroupのdrawメソッドとonDrawメソッドの論理図を呼び出す

おすすめ

転載: blog.csdn.net/C_biubiubiu/article/details/112181454