自定義VIEW須要知道的

MyView(Context context) {//java new view才調用次構造
    (context);
    init(, );
}

MyView(Context context, AttributeSet attrs) {//layout裏xml不帶style時被調用
    (context, attrs);
    init(attrs, );
}

MyView(Context context, AttributeSet attrs, defStyle) {//layout裏xml帶style時被調用
    (context, attrs, defStyle);
    init(attrs, defStyle);
}

如下爲轉載:
java

View的繪製流程分爲三個階段:
    Measure--測量
    Layout--放置視圖位置
    Draw--繪製
View樹的繪製流程是在ViewRootImpl類的performTraversals()方法(這個方法巨長)開始的。
每一個控件的實際寬高都是由父視圖和自身決定的,實際測量是在OnMeasure()方法中進行,因此在View的子類須要重寫OnMeasure()方法。
    /**
     * <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.
     * </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)
     */
     //final方法,子類不可重寫
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        //回調onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }
這個方法的兩個參數都是父View傳過來的,每一個參數由兩部分組成,高16位表示MODE,底16位表示SIZE。
MODE有三種類型分別爲:
MeasureSpec.EXACTLY:肯定大小
MeasureSpec.AT_MOST:最大大小
MeasureSpec.UNSPECIFIED:不肯定的

SIZE表示父View的大小,對於子View大小是由父View和子View共同決定的。
對於系統Window類的DecorView對象的Mode通常都爲MeasureSpec.EXACTLY,而Size分別對應屏幕寬高。

下面爲onMeasure()源碼:
/**
     * <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 overriden 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)
     */
     //View的onMeasure默認實現方法
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
子類應該重寫這個方法,重寫時必須調用setMeasuredDimension(int, int)方法來保存測量的寬度和高度。測量高度和寬度默認是背景的高度和寬度,最小也是視圖的最小寬度和高度。獲取最小寬度和高度的方法是:getSuggestedMinimumWidth()和getSuggestedMinimumHeight()。
setMeasuredDimension(int,int)方法調用完畢View的測量工做就會結束。

下面看看getDefaultSize(int,int)方法的實現:
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //經過MeasureSpec解析獲取mode與size
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
specMode等於AT_MOST或EXACTLY就返回specSize,這就是系統默認的規格。
getSuggestedMinimumHeight()和getSuggestedMinimumWidth()方法實現以下:
  protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
最小寬度和高度是由View的Background尺寸與經過設置View的minXXX屬性共同決定的。
setMeasuredDimension方法最終設置完成View的measure以後View的mMeasuredWidth和mMeasuredHeight成員纔會有具體的數值,因此若是咱們自定義的View或者使用現成的View想經過getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程以後被調用才能返回有效值。

Measure原理總結:
經過上面分析能夠看出measure過程主要就是從頂層父View向子View遞歸調用view.measure方法(measure中又回調onMeasure方法)的過程。具體measure核心主要有以下幾點:
一、MeasureSpec(View的內部類)測量規格爲int型,值由高16位規格模式specMode和低16位具體尺寸specSize組成。
二、View的measure方法是final的,不容許重載,View子類只能重載onMeasure來完成本身的測量邏輯。
三、最頂層DecorView測量時的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法肯定的(LayoutParams寬高參數均爲MATCH_PARENT,specMode是EXACTLY,specSize爲物理屏幕大小)。
四、ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法,簡化了父子View的尺寸計算。
五、只要是ViewGroup的子類就必需要求LayoutParams繼承子MarginLayoutParams,不然沒法使用layout_margin參數。
六、使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程以後被調用才能返回有效值。
2、遞歸Layout源碼分析:
ViewRootImpl的performTraversals中measure執行完成之後會接着執行mView.layout,具體以下:
private void performTraversals() {
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
}
ViewGroup的layout()方法:
   @Override
    public final void layout(int l, int t, int r, int b) {
        ......
        super.layout(l, t, r, b);
        ......
    }
實質調用的仍是View的layout()方法:
    public void layout(int l, int t, int r, int b) {
        ......
        //實質都是調用setFrame方法把參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變量
        //判斷View的位置是否發生過變化,以肯定有沒有必要對當前的View進行從新layout
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //須要從新layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //回調onLayout
            onLayout(changed, l, t, r, b);
            ......
        }
        ......
    }
判斷View的位置是否發生過變化,以肯定有沒有必要對當前的View進行從新layout()。

對比上面View的layout和ViewGroup的layout方法能夠發現,View的layout方法是能夠在子類重寫的,而ViewGroup的layout是不能在子類重寫的,言外之意就是說ViewGroup中只能經過重寫onLayout方法。那咱們接下來看下ViewGroup的onLayout方法,以下:
   @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
ViewGroup中onLayout()方法是抽象方法,ViewGroup的子類必須重寫這個方法,因此在自定義ViewGroup控件中,Onlayout配合onMeasure方法一塊兒使用能夠實現自定義View的複雜佈局。自定義View首先要調用OnMeasure進行測量,而後調用onLayout方法動態獲取子View和子View的測量大小,而後進行Layout佈局。重載onLayout的目的就是安排其Children在父View的位置,重載onLayout的一般作法就是寫一個for()循環調用每個字視圖的layout(l,t,r,b)方法,傳入不一樣的參數來肯定每個子視圖的在父視圖中顯示的位置,onLayout方法中的四個參數分別爲父視圖推薦的位置。

LinearLayout源碼分析:
public class LinearLayout extends ViewGroup {
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
}
分析VERTICAL模式下:
layoutVertical源碼分析:
    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
        int childTop;
        int childLeft;
        // Where right end of child should go
        //計算父窗口推薦的子View寬度
        final int width = right - left;
        //計算父窗口推薦的子View右側位置
        int childRight = width - mPaddingRight;
        // Space available for child
        //child可以使用空間大小
        int childSpace = width - paddingLeft - mPaddingRight;
        //經過ViewGroup的getChildCount方法獲取ViewGroup的子View個數
        final int count = getVirtualChildCount();
        //獲取Gravity屬性設置
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
        //依據majorGravity計算childTop的位置值
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               //計算出頂端的座標
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;
               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;
           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
        //重點!!!開始遍歷
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                //LinearLayout中其子視圖顯示的寬和高由measure過程來決定的,所以measure過程的意義就是爲layout過程提供視圖顯示範圍的參考值
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                //獲取子View的LayoutParams
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //依據不一樣的absoluteGravity計算childLeft位置
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;
                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
                childTop += lp.topMargin;
                //經過垂直排列計算調運child的layout設置child的位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
                i += getChildrenSkipCount(child, i);
            }
        }
    }
layout原理總結:
一、View.layout方法可被重載,ViewGroup.layout爲final的不可重載,ViewGroup.onLayout爲abstract的,子類必須重載實現本身的位置邏輯。
二、measure操做完成後獲得的是對每一個View經測量過的measuredWidth和measuredHeight,layout操做完成以後獲得的是對每一個View進行位置分配後的mLeft、mTop、mRight、mBottom,這些值都是相對於父View來講的。
三、凡是layout_XXX的佈局屬性基本都針對的是包含子View的ViewGroup的,當對一個沒有父容器的View設置相關layout_XXX屬性是沒有任何意義的
四、使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程以後被調用才能返回有效值。

四View繪製流程第三步:遞歸draw源碼分析:
draw的過程也是在ViewRootImpl的PerformTraversales()內部調用,其調用順序在Measure()和layout()以後,這裏的myView對於Activity來講就是phoneWindow.DecorView,ViewRootImpl中的代碼會建立一個Canvas對象,而後調用View的draw()方法來執行具體的繪製工做。

draw()源碼分析:
public void draw(Canvas canvas) {
        ......
        /*
         * 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
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        // skip step 2 & 5 if possible (common case)
        ......
        // Step 2, save the canvas' layers
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Step 5, draw the fade effect and restore layers
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        ......
    }
draw方法經過調運drawBackground(canvas);方法實現了背景繪製:
源碼:
    private void drawBackground(Canvas canvas) {
        //獲取xml中經過android:background屬性或者代碼中setBackgroundColor()、setBackgroundResource()等方法進行賦值的背景Drawable
        final Drawable background = mBackground;
        ......
        //根據layout過程肯定的View位置來設置背景的繪製區域
        if (mBackgroundSizeChanged) {
            background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            rebuildOutline();
        }
        ......
            //調用Drawable的draw()方法來完成背景的繪製工做
            background.draw(canvas);
        ......
    }
對view的內容進行繪製:此處條用了onDraw();
   /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
該方法須要子類去實現。
對當前的全部子View進行繪製,若是當前的view沒有子view就不須要繪製 。

下面爲View中的draw()方法中的dispatchdRraw()方法源碼:
    /**
     * 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包含子類才須要重寫該方法。
ViewGroup的dispatchDraw方法源碼(這也就是說對當前View的全部子view進行繪製,若是當前的View沒有子view就不須要繪製的緣由,由於若是是view調用該方法是空的,而ViewGroup纔有實現)以下:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        ......
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ......
        for (int i = 0; i < childrenCount; i++) {
            ......
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            ......
            for (int i = disappearingCount; i >= 0; i--) {
                ......
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
    }
ViewGroup重寫了View的DispatchDraw()方法,該方法內部會遍歷每一個子View,而後調用drawChild()方法,咱們能夠看下ViewGroup的drawChild()方法,以下:
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
能夠看見drawChild()方法調用了View的draw()方法。因此說ViewGroup類已經爲咱們重寫了dispatchDraw()的功能實現,咱們通常不須要重載該方法,但能夠重載父類函數實現具體的功能。

6、對view的滾動條進行繪製:
能夠看到view的onDrawScrollBars()方法,因此咱們看下View的onDrawScrollBars(canvas);方法,以下:
    /**
     * <p>Request the drawing of the horizontal and the vertical scrollbar. The
     * scrollbars are painted only if they have been awakened first.</p>
     *
     * @param canvas the canvas on which to draw the scrollbars
     *
     * @see #awakenScrollBars(int)
     */
    protected final void onDrawScrollBars(Canvas canvas) {
        //繪製ScrollBars分析不是咱們這篇的重點,因此暫時不作分析
        ......
    }
能夠看見其實任何一個View都是有(水平垂直)滾動條的,只是通常狀況下沒讓它顯示而已。到此,View的draw繪製部分源碼分析完畢,咱們接下來進行一些總結。

Draw原理總結:

一、若是該View是一個ViewGroup,則須要遞歸繪製其所包含的全部子View。

二、View默認不會繪製任何內容,真正的繪製都須要本身在子類中實現。

三、View的繪製是藉助onDraw方法傳入的Canvas類來進行的。

四、區分View動畫和ViewGroup佈局動畫,前者指的是View自身的動畫,能夠經過setAnimation添加,後者是專門針對ViewGroup顯示內部子視圖時設置的動畫,能夠在xml佈局文件中對ViewGroup設置layoutAnimation屬性(譬如對LinearLayout設置子View在顯示時出現逐行、隨機、下等顯示等不一樣動畫效果)。

五、在獲取畫布剪切區(每一個View的draw中傳入的Canvas)時會自動處理掉padding,子View獲取Canvas不用關注這些邏輯,只用關心如何繪製便可。

六、默認狀況下子View的ViewGroup.drawChild繪製順序和子View被添加的順序一致,可是你也能夠重載ViewGroup.getChildDrawingOrder()方法提供不一樣順序。android

相關文章
相關標籤/搜索