[セットトップ]Android Viewの描画メカニズムをソースコードの観点から分析する(一)


Androidの学習の道では、誰もがAndroidのソースコードをめくることが避けられない.ソースコードの角度から問題を分析してこそ、Android開発を本当に遊ぶことができるからだ.最近仕事が暇なので、何か书きたいと思って、ちょうど自分で整理してもいいです.ビューの表示メカニズムがカスタムビューの基礎であり、面接でよく聞かれる質問でもあることを考慮して、この文を記録し、皆さんと共有し、レベルが限られているため、皆さんがレンガをたたいてくれることを望んでいます.感謝に堪えません.カスタマイズされたviewがある同業者は、viewの表示がactivityのsetContentViewメソッドに依存してPhoneWindowフォームに依存していることを知っておくべきで、表示の過程で、このviewは測定(measure)、レイアウト(layout)、draw(描画)の3段階を経験し、measure段階は各Viewの大きさを得ることである.Layoutフェーズは各ViewのUI上の座標を計算し、drawフェーズは前の2つのフェーズのデータに基づいてUI描画を行う.描画が完了すると、インタフェースに表示されます.
measure:
xmlがuiに表示されると、layoutのidをactivityのsetContentViewに転送し、最終的にはViewRootのperformTraversalsメソッドを呼び出し、viewの描画を担当します.
       private void performTraversals() {
        final View host = mView;
        if (DBG) {
            host.debug();
        }
        if (host == null || !mAdded)
            return;
        ......
         Rect frame = mWinFrame;
        if (mFirst) {
            fullRedrawNeeded = true;
            mLayoutRequested = true;
            DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics();
            desiredWindowWidth = packageMetrics.widthPixels;
            desiredWindowHeight = packageMetrics.heightPixels;
            ......
        }
        ........
        boolean insetsChanged = false;
        if (mLayoutRequested) {
            getRunQueue().executeActions(attachInfo.mHandler);
            if (mFirst) {
                host.fitSystemWindows(mAttachInfo.mContentInsets);
                mAttachInfo.mInTouchMode = !mAddedTouchMode;
                ensureTouchModeLocally(mAddedTouchMode);
            } else {
                if (!mAttachInfo.mContentInsets.equals(mPendingContentInsets)) {
                    mAttachInfo.mContentInsets.set(mPendingContentInsets);
                    host.fitSystemWindows(mAttachInfo.mContentInsets);
                    insetsChanged = true;
                }
                if (!mAttachInfo.mVisibleInsets.equals(mPendingVisibleInsets)) {
                    mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
                }
                if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                        || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    windowResizesToFitContent = true;
                    DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics();
                    desiredWindowWidth = packageMetrics.widthPixels;
                    desiredWindowHeight = packageMetrics.heightPixels;
                }
            }

            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

            host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...........
        }

getRootMeasureSpecに入力されるパラメータの1つはlpのプロパティであり、lpの定義は次のとおりです.
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
WindowManager.LayoutParams lp = mWindowAttributes;

public LayoutParams() {
      super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
      type = TYPE_APPLICATION;
      format = PixelFormat.OPAQUE;
}

つまりlpを意味しますwidthとlp.heightの値はすべてmatch_parent. getRootMeasureSpecメソッドでは、windowSizeとrootDimensionに従って測定仕様を返します.lp.widthは、例えばdecorViewのwidthです.widthの値がMATCH_の場合PARENT、では戻ってくる計測規格はwindowSize+MeasureSpecです.EXACTLYの値は、WRAP_CONTENTはwindowSize+MeasureSpecです.AT_MOSTの値.
private int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_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;
    }

Windows SizeとrootDimensionでマッピングされたmode値を加算する理由については、次のコードを参照してください.
public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        public static int makeMeasureSpec(int size, int mode) {
            return size + mode;
        }
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        ..........
    }

MeasureSpec.makeMeasureSpecでは単純にsizeとmodeを加算して返すだけです.ここで、このような戻り値は、AT_MOST、EXACTLYはmodeを表しており、いずれも2ビットを占有しておりint値であり、int値はjavaで32ビットを占有しているため、他の30ビットはAndroidでsizeとして設計されている.すなわち、各高2ビットはspecModeを表し、低30ビットはサイズの大きさを表す.前の操作が完了するとhostの呼び出しが開始されます.measure操作でviewのサイズを計算します.hostはDecorViewを表し、decorViewはFrameLayoutつまりView Groupであるが、ViewではViewから継承され、viewのmeasureメソッドは書き換えられないのでhost.measureはやはりviewのmeasureメソッドで、このメソッドではViewGroupのonMeasure(widthMeasureSpec,heightMeasureSpec)メソッドを呼び出しているので、FrameLayoutのonMeasureメソッドに移動します
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int count = getChildCount();

        int maxHeight = 0;
        int maxWidth = 0;

        // Find rightmost and bottommost child
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
            }
        }

        // Account for padding too
        maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
        maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
                resolveSize(maxHeight, heightMeasureSpec));
    }

このメソッドにはmeasureChildWithMarginsメソッドが呼び出されていますが、実はView Groupのサブクラスは測定時にこのメソッドを実行し、最後にViewクラスのmeasureメソッドを呼び出して測定します.プロトタイプは以下の通りです.
 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  + heightUsed, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

ビューのmeasureメソッドの2つのパラメータは、親ビューが最終的に使用するフォームサイズが親ビューと子ビューで共に決定されるため、幅と高さのmeasureSpecに対応します.したがってchildWidthMeasureSpecは、親ビューがサブビューに渡される推奨値です.サイズを測定する場合、paddingとmarginの値もサイズの一部とします.上記はviewの測定過程だけです.LinearLayoutの測定過程を分析します.LinearLayoutの測定では、以下が主なコードです.
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

    /** *            */
    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        int maxWidth = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;        
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;

        // See how tall everyone is. Also remember max width.
        //       View,     View    ,     View  measure   
        //           view
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                // //  child  Null, mTotalLength 0 
                mTotalLength += measureNullChild(i);
                continue;
            }
            if (child.getVisibility() == View.GONE) {
                ////  child   ,    
               i += getChildrenSkipCount(child, i);
               continue;
            }
            //  child LayoutParams 
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            // weight    totalWeight,weight    xml    layout_weight     
            totalWeight += lp.weight;

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                 /**    View mode EXACTLY,  height==0   lp.weight>0      measure  child,   topMargin bottoMargin     totaoLength  */  
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            } else {
                int oldHeight = Integer.MIN_VALUE;
                //   View  EXACLTY,  lp.height == 0 && lp.weight > 0,    View height  WRAP_CONTENT 
                if (lp.height == 0 && lp.weight > 0) {
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);

                if (oldHeight != Integer.MIN_VALUE) {
                   lp.height = oldHeight;
                }

                final int childHeight = child.getMeasuredHeight();
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }


            .....................

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {

                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (useLargestChild && heightMode == MeasureSpec.AT_MOST) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

        //          padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        //   View      heightSize
        int heightSize = mTotalLength;

        // Check against our minimum height
        //             mBGDrawable.getMinimumHeight()
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
         //   heightSize    ,    LinearLayout xml      ,     Activity   
        //  heightSize          ,                ,      
        // Reconcile our calculated size with the heightMeasureSpec
        //   heightMeasureSpec size    ,    heightsize ==heightMeasureSpec size,        
        heightSize = resolveSize(heightSize, heightMeasureSpec);

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds
        //        delta
        int delta = heightSize - mTotalLength;
        if (delta != 0 && totalWeight > 0.0f) {
            //     weightsum  , weightSum  weightsum   ,    totalWeight 
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

                float childExtra = lp.weight;
                if (childExtra > 0) {
                    // Child said it could absorb extra space -- give him his share
                    //     : view weight    *      / weight  
                    int share = (int) (childExtra * delta / weightSum);
                    weightSum -= childExtra;
                    delta -= share;

                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight +
                                    lp.leftMargin + lp.rightMargin, lp.width);

                    // TODO: Use a field like lp.isMeasured to figure out if this
                    // child has been previously measured
                    if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                        // child was measured once already above...
                        // base new measurement on stored values
                        //      view   
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }

                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    } else {
                        // child was skipped in the loop above.
                        // Measure for this first time here 
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));
                    }
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);
        }

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        //     View    ,       
        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);

        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }
    }

測定の過程で、すべてのサブビューを巡回し、すべてのサブビューの総高さとtotalWeightを計算し、各サブビューに対してmeasure操作を行う.サブビューを巡るchildの過程でchildがNullであればmTotalLengthに0を加え、childが見えない場合は何もせずに次のサブビューを巡り続け、childのlayoutParamsを手に入れ、childのlayout_を取り出すWeight、totalWeightにweight値を追加します.次に、親ビューが提供する高さmodeがMeasureSpecに等しいと判断する.EXACTLYで、childのheightが0に等しく、childのweightの値が0より大きい場合は、このchildを測定しない.weightがあるので、すべてのchildを測定した後、weightの値に基づいて高さを割り当てるが、ここではchildのmargin値を加算しなければならない.weightがどのように変わっても、margin値は影響しないからだ.mTotalLength=Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin).ここでmaxを取るのはchildのmarginが負数である可能性があるからです.前の条件を満たさない場合modeはMeasureSpecに等しい.EXACTLYは、childを測定する必要があり、測定を経てchildの高さを知ることができ、この高さをmTotalLengthに加えます.この条件では、childのheightが0に等しく、childのweightの値が0より大きい場合、現在のviewのheightをLayoutParamsに設定する.WRAP_CONTENT、それから測定します.今までは全体の高さを測っただけで、まだweightに従って高さを分配していません.ははは、こんなに多くの読者が気絶しそうだと言っていましたが、大丈夫です.weightの分配について、ソースコードを集めて例を挙げてお話しします(次の文章を参照).次にmTotalLengthに親ビューのpaddingTopとpaddingBottom値を加えてheightSizeを得ます.ただし、各ビューにはバックグラウンドがある、バックグラウンドには高さがあるので、heightSizeとgetSuggestedMinimumHeight()の値の大きさを比較し、最大値をheightSize、getSuggestedMinimumHeight()が返すmBGDrawableとする.getMinimumHeight()はdrawableバックグラウンドの最小高さです.親ビューのheightMeasureSpecのmodeが正確であれば、このheightSizeの最終値も親ビューで指定されたサイズであるべきであるため、resolveSizeによって再びheightSizeに値を割り当てます.次にweightに従って高さを割り当て始めます.高さを割り当てる前に、残りの高さheightSize-mTotalLengthが0ではなく、totalWeightが0より大きいことを保証します.次にサブviewを巡回してサブviewのweightを得,weightが0より大きい場合,式:childのweight属性値*残高/weight総和によりchildが残高からどれだけの高さ値を得るかを算出し,残高値をリフレッシュしchildに高さを再設定して測定する.最後にすべての子供のView測定が完了し、setMeasuredDimensionを呼び出して自分のサイズを設定します.