View(5)の詳細-ビューのステータスと再描画プロセスの分析


普段はビューを使っているときに状態があることに気づくと思います.例えばボタンがあり、普通の状態では効果的ですが、指を押すと別の効果になり、ボタンをクリックしたような感じになります.もちろん、この効果はほとんどのAndroidプログラマーがどのように実現すべきか知っていると信じていますが、Viewを深く理解している以上、その背後にある実現原理がどのようなものなのか、今日は探ってみましょう.
表示ステータス
ビューステータスの種類は非常に多く、全部で十数種類ありますが、多くの場合、私たちはその中のいくつかしか使用しません.そのため、ここでは最もよく使われるいくつかのビューステータスだけを分析します.
1. enabled
現在のビューが使用可能かどうかを示します.setEnable()メソッドを呼び出してビューの使用可能な状態を変更できます.転送trueは使用可能であり、転送falseは使用不可です.これらの最大の違いは、使用できないビューがonTouchイベントに応答できないことです.
2. focused
現在のビューがフォーカスされているかどうかを示します.通常、ビューをフォーカスさせるには、キーボードの上下左右キーでビューを切り替え、requestFocus()メソッドを呼び出す2つの方法があります.現在のAndroid携帯電話にはキーボードがほとんどないため、基本的にrequestFocus()という方法でビューに焦点を当てることができます.一方、requestFocus()メソッドでは、ビューに焦点を当てることができるとは保証されません.ブール値の戻り値があり、trueの戻り値が焦点を得ることに成功した場合、falseの戻り値が焦点を得ることに失敗したことを示します.一般的に、EditTextなどのビューがfocusableとfocusable in touch modeが同時に設立された場合にのみフォーカスを取得できます.
3. window_focused
現在のビューがインタラクティブなウィンドウにあるかどうかを示します.この値はシステムによって自動的に決定され、アプリケーションは変更できません.
4. selected
現在のビューが選択されているかどうかを示します.1つのインタフェースで複数のビューが選択され、setSelected()メソッドを呼び出すとビューの選択状態が変更され、trueが選択され、falseが選択されていないことを示します.
5. pressed
現在のビューが押されているかどうかを示します.setPressed()メソッドを呼び出してこの状態を変更することができ、入力trueは押下を表し、入力falseは押下されていないを表す.通常、この状態はシステムによって自動的に付与されるが、開発者は自分でこの方法を呼び出して変更することもできる.
プロジェクトのdrawableディレクトリの下にselectorファイルを作成し、各状態で対応するバックグラウンドピクチャを表示するように構成できます.たとえばcomposeを作成します.bg.xmlファイルは、次のコードで記述されます.
<selector xmlns:android="http://schemas.android.com/apk/res/android">  

    <item android:drawable="@drawable/compose_pressed" android:state_pressed="true"></item>  
    <item android:drawable="@drawable/compose_pressed" android:state_focused="true"></item>  
    <item android:drawable="@drawable/compose_normal"></item>  

</selector> 

このコードは、ビューが正常な状態にある場合にcompose_を表示することを意味します.normalという背景図は、ビューがフォーカスを取得したり押されたりしたときにcompose_を表示します.pressedという背景図.
このselectorファイルを作成すると、次のようにボタンの背景図に設定するなど、レイアウトまたはコードで使用できます.
<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >  

    <Button android:id="@+id/compose" android:layout_width="60dp" android:layout_height="40dp" android:layout_gravity="center_horizontal" android:background="@drawable/compose_bg" />  

</LinearLayout>

今プログラムを実行して、このボタンは普通の状態と押した状態の時に異なる背景のピクチャーを表示して、下図のように:このように私達は1つのとても簡単な方法でボタンの押した効果を実現して、しかしその背景の原理はいったいどんなですか?これはまたソースコードの階層から分析します.
指がビューに押されると、ビューの状態が変化し、ビューのpressed状態がtrueであることはよく知られています.ビューの状態が変化するたびに、ViewのdrawableStateChanged()メソッドがコールバックされます.コードは次のようになります.
protected void drawableStateChanged() {  
    Drawable d = mBGDrawable;  
    if (d != null && d.isStateful()) {  
        d.setState(getDrawableState());  
    }  
} 

ここでの最初のステップは、まずmBGDrawableをDrawableオブジェクトに割り当てることです.では、このmBGDrawableは何ですか.setBackgroundResource()メソッドのコードを以下に示します.
public void setBackgroundResource(int resid) {  
    if (resid != 0 && resid == mBackgroundResource) {  
        return;  
    }  
    Drawable d= null;  
    if (resid != 0) {  
        d = mResources.getDrawable(resid);  
    }  
    setBackgroundDrawable(d);  
    mBackgroundResource = resid;  
}

7行目でResourceのgetDrawable()メソッドを呼び出してresidをDrawableオブジェクトに変換し、setBackgroundDrawable()メソッドを呼び出してこのDrawableオブジェクトを転送し、setBackgroundDrawable()メソッドで転送されたDrawableオブジェクトをmBGDrawableに割り当てます.
レイアウトファイルでandroid:backgroundプロパティで指定したselectorファイルは、setBackgroundResource()メソッドを呼び出すのと同じ効果があります.つまりdrawableStateChanged()メソッドのmBGDrawableオブジェクトは、実は私たちが指定したselectorファイルです.
次にdrawableStateChanged()メソッドの4行目にgetDrawableState()メソッドを呼び出してビューステータスを取得します.コードは次のようになります.
public boolean setState(final int[] stateSet) {  
    if (!Arrays.equals(mStateSet, stateSet)) {  
        mStateSet = stateSet;  
        return onStateChange(stateSet);  
    }  
    return false;  
}

ここでArraysを呼び出します.equals()メソッドは,ビュー状態の配列が変化したか否かを判断し,変化した場合はonStateChange()メソッドを呼び出し,そうでなければfalseに直接戻る.しかし、DrawableのonStateChange()メソッドではfalseを簡単に返しただけで、論理的な処理はありません.これはなぜですか.これは主にmBGDrawableオブジェクトがselectorファイルによって作成されているためであり、このファイルによって作成されたDrawableオブジェクトはいずれもStateListDrawableインスタンスであるため、ここで呼び出されたonStateChange()メソッドは実際にStateListDrawableのonStateChange()メソッドを呼び出しているので、早速見てみましょう.
@Override  
protected boolean onStateChange(int[] stateSet) {  
    int idx = mStateListState.indexOfStateSet(stateSet);  
    if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "  
            + Arrays.toString(stateSet) + " found " + idx);  
    if (idx < 0) {  
        idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);  
    }  
    if (selectDrawable(idx)) {  
        return true;  
    }  
    return super.onStateChange(stateSet);  
} 

ここでindexOfStateSet()メソッドを呼び出して、現在のビューステータスに対応するDrawableリソースの下付きラベルを見つけ、9行目でselectDrawable()メソッドを呼び出して下付きラベルを入力すると、ビューの背景図が現在のビューステータスに対応する画像に設定されます.
前の記事では、どのビューの表示も非常に科学的な描画プロセスを経なければならないと述べていますが、背景図の描画はdraw()メソッドで行われていることは明らかですが、なぜselectDrawable()メソッドが背景図の変更を制御できるのでしょうか.これで、ビューの再描画の流れを検討します.
ビューの再描画
ビューはActivityのロードが完了した後に自動的に画面に描画されますが、Activityと対話するときに、ビューの状態を変更したり、コントロールを表示したり非表示にしたりするなど、ビューを動的に更新する必要があります.その時、前に描いたビューは実は期限切れになっていたので、ビューを再描画する必要があります.
ビューのsetVisibility()、setEnabled()、setSelected()などのメソッドを呼び出すとビューが再描画されますが、手動でビューを強制的に再描画する場合はinvalidate()メソッドを呼び出して実装できます.もちろん、setVisibility()やsetEnabled()やsetSelected()などのメソッドの内部もinvalidate()メソッドを呼び出すことで実現されていますが、invalidate()メソッドのコードがどのようなものかを見てみましょう.
Viewのソースコードには、いくつかのinvalidate()メソッドのリロードと1つのinvalidateDrawable()メソッドがあります.もちろん、それらの原理は同じです.そのため、コードは次のように1つだけ分析します.
void invalidate(boolean invalidateCache) {  
    if (ViewDebug.TRACE_HIERARCHY) {  
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);  
    }  
    if (skipInvalidate()) {  
        return;  
    }  
    if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) ||  
            (invalidateCache && (mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID) ||  
            (mPrivateFlags & INVALIDATED) != INVALIDATED || isOpaque() != mLastIsOpaque) {  
        mLastIsOpaque = isOpaque();  
        mPrivateFlags &= ~DRAWN;  
        mPrivateFlags |= DIRTY;  
        if (invalidateCache) {  
            mPrivateFlags |= INVALIDATED;  
            mPrivateFlags &= ~DRAWING_CACHE_VALID;  
        }  
        final AttachInfo ai = mAttachInfo;  
        final ViewParent p = mParent;  
        if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {  
            if (p != null && ai != null && ai.mHardwareAccelerated) {  
                p.invalidateChild(this, null);  
                return;  
            }  
        }  
        if (p != null && ai != null) {  
            final Rect r = ai.mTmpInvalRect;  
            r.set(0, 0, mRight - mLeft, mBottom - mTop);  
            p.invalidateChild(this, r);  
        }  
    }  
}

このメソッドでは5行目にskipInvalidate()メソッドを呼び出して現在のViewを再描画する必要があるかどうかを判断し、判断の論理も比較的簡単であり、Viewが非表示でアニメーションが実行されていない場合は再描画する必要はないと考えられる.その後、透明度の判断が行われ、Viewにタグビットが追加され、22行目と29行目にView ParentのinvalidateChild()メソッドが呼び出されます.ここで、View Parentは実際には現在のビューの親ビューであるため、View GroupのinvalidateChild()メソッドに呼び出されます.コードは次のようになります.
public final void invalidateChild(View child, final Rect dirty) {  
    ViewParent parent = this;  
    final AttachInfo attachInfo = mAttachInfo;  
    if (attachInfo != null) {  
        final boolean drawAnimation = (child.mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION;  
        if (dirty == null) {  
            ......  
        } else {  
            ......  
            do {  
                View view = null;  
                if (parent instanceof View) {  
                    view = (View) parent;  
                    if (view.mLayerType != LAYER_TYPE_NONE &&  
                            view.getParent() instanceof View) {  
                        final View grandParent = (View) view.getParent();  
                        grandParent.mPrivateFlags |= INVALIDATED;  
                        grandParent.mPrivateFlags &= ~DRAWING_CACHE_VALID;  
                    }  
                }  
                if (drawAnimation) {  
                    if (view != null) {  
                        view.mPrivateFlags |= DRAW_ANIMATION;  
                    } else if (parent instanceof ViewRootImpl) {  
                        ((ViewRootImpl) parent).mIsAnimating = true;  
                    }  
                }  
                if (view != null) {  
                    if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&  
                            view.getSolidColor() == 0) {  
                        opaqueFlag = DIRTY;  
                    }  
                    if ((view.mPrivateFlags & DIRTY_MASK) != DIRTY) {  
                        view.mPrivateFlags = (view.mPrivateFlags & ~DIRTY_MASK) | opaqueFlag;  
                    }  
                }  
                parent = parent.invalidateChildInParent(location, dirty);  
                if (view != null) {  
                    Matrix m = view.getMatrix();  
                    if (!m.isIdentity()) {  
                        RectF boundingRect = attachInfo.mTmpTransformRect;  
                        boundingRect.set(dirty);  
                        m.mapRect(boundingRect);  
                        dirty.set((int) boundingRect.left, (int) boundingRect.top,  
                                (int) (boundingRect.right + 0.5f),  
                                (int) (boundingRect.bottom + 0.5f));  
                    }  
                }  
            } while (parent != null);  
        }  
    }  
}

ここでは10行目にwhileループが入り、ViewParentが空でないときはループし続けることがわかります.このwhileループでは、現在のレイアウトの親レイアウトが取得され、そのinvalidateChildInParent()メソッドが呼び出されます.ViewGroupのinvalidateChildInParent()メソッドでは、主に再描画が必要な矩形領域を計算するために使用されます.ここでは、それにかかわらず、最外層のルートレイアウトにループすると、ViewRootのinvalidateChildInParent()メソッドが呼び出されます.コードは次のようになります.
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {  
    invalidateChild(null, dirty);  
    return null;  
} 

ここのコードはとても簡単で、invalidateChild()メソッドを呼び出しただけです.では、もう一度フォローしてみましょう.
public void invalidateChild(View child, Rect dirty) {  
    checkThread();  
    if (LOCAL_LOGV) Log.v(TAG, "Invalidate child: " + dirty);  
    mDirty.union(dirty);  
    if (!mWillDrawSoon) {  
        scheduleTraversals();  
    }  
}  

この方法も長くありません.6行目にscheduleTraversals()という方法を呼び出しました.では、フォローし続けます.
public void scheduleTraversals() {  
    if (!mTraversalScheduled) {  
        mTraversalScheduled = true;  
        sendEmptyMessage(DO_TRAVERSAL);  
    }  
}

ここでsendEmptyMessage()メソッドが呼び出され、DO_が渡されたことがわかります.TRAVERSALパラメータ.Android非同期メッセージ処理メカニズムを知っている人は、どのHandlerもsendEmptyMessage()メソッドを呼び出してメッセージを送信し、handleMessage()メソッドでメッセージを受信することができますが、ViewRootのクラス定義を見てみると、Handlerから継承されていることがわかります.つまり、ここでsendEmptyMessage()メソッドを呼び出したメッセージは、ViewRootのhandleMessage()メソッドで受信されます.では、handleMessage()メソッドのコードを見てみましょう.次のようにします.
public void handleMessage(Message msg) {  
    switch (msg.what) {  
    case DO_TRAVERSAL:  
        if (mProfile) {  
            Debug.startMethodTracing("ViewRoot");  
        }  
        performTraversals();  
        if (mProfile) {  
            Debug.stopMethodTracing();  
            mProfile = false;  
        }  
        break;  
    ......  
} 

おなじみのコードが出てきました!ここでは7行目にperformTraversals()メソッドを呼び出しました.これが前の文章で学んだビュー描画の入り口ではないでしょうか.多くの転々とした呼び出しが行われたが,ビューのinvalidate()メソッドを呼び出すとperformTraversals()メソッドに確実に移行し,描画プロセスを再実行することが確認された.
invalidate()メソッドは最終的にperformTraversals()メソッドに呼び出されますが、measureおよびlayoutフローは再実行されません.ビューには再測定を強制するフラグビットがなく、サイズも変化していないため、drawフローのみが実行できます.ビューの描画プロセスを完全に再実行したい場合は、invalidate()メソッドを使用するのではなく、requestLayout()を呼び出す必要があります.この方法の流れはinvalidate()法よりも簡単であるが,中心思想はそれほど悪くないので,ここでは詳しく分析しない.
セクション:http://blog.csdn.net/guolin_blog/article/details/17045157