Android面試Android進(jìn)階(十五)-自定義View相關(guān)1

問:自定義View有幾個構(gòu)造函數(shù),及自定義View的主要流程

答:自定義View中共有四個構(gòu)造函數(shù),一般只需要實現(xiàn)一個參數(shù)及兩個參數(shù)的構(gòu)造函數(shù)即可。自定義View過程中,主要流程有:measure、layout、draw即 測量、布局、繪制,這里面涉及到MeasureSpec、Paint、Canvas、Path等很多重要類。
自定義View的實現(xiàn)方式有很多:自定義組合控件、繼承系統(tǒng)View 如繼承TextView、繼承系統(tǒng)ViewGroup 如繼承LinearLayout、繼承View繼承ViewGroup等。

自定義View的四個構(gòu)造方法:

class MyView : View {
    /**
     * 在java代碼里new的時候會用到
     */
    constructor(context: Context?) : super(context) {}

    /**
     * 在xml布局文件中使用時自動調(diào)用
     */
    constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {}

    /**
     * 不會自動調(diào)用,如果有默認(rèn)style時,在第二個構(gòu)造函數(shù)中調(diào)用
     */
    constructor(context: Context?, @Nullable attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

    /**
     * 只有在API版本>21時才會用到
     * 不會自動調(diào)用,如果有默認(rèn)style時,在第二個構(gòu)造函數(shù)中調(diào)用
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    constructor(context: Context?, @Nullable attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
    }
}

自定義View的三個主要流程:measure、layout、draw
1、Measure測量流程,從View的measure方法為入口,該方法只是做了一些初始化,之后調(diào)用onMeasure方法。來看onMeasure()方法:

    //View的onMeasure源碼
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

首先看到的是onMeasure()方法要求傳入兩個int類型的參數(shù),分別是寬高。這里需要了解一下 MeasureSpec 是什么東西。
MeasureSpec是View的內(nèi)部類,值保存在一個int值當(dāng)中,一個int有32位,前兩位是 mode(模式),后30位是 size(大小) 即:MeasureSpec = mode + size
其中mode的值有三種,UNSPECIFIED,EXACTLY、AT_MOST,

模式 意義 對應(yīng)
EXACTLY 精準(zhǔn)模式,View需要一個精確值,這個值即為MeasureSpec當(dāng)中的Size match_parent
AT_MOST 最大模式,View的尺寸有一個最大值,View不可以超過MeasureSpec當(dāng)中的Size值 wrap_content
UNSPECIFIED 無限制,View對尺寸沒有任何限制,View設(shè)置為多大就應(yīng)當(dāng)為多大 不怎么用

在ViewGroup中的 MeasureSpec測量源碼如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //獲取測量模式
        int specMode = MeasureSpec.getMode(spec);
        //獲取測量大小
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // 當(dāng)父View要求一個精確值時,為子View賦值
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //如果子view有自己的尺寸,則使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //當(dāng)子View是match_parent,將父View的大小賦值給子View
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 如果子View是wrap_content,設(shè)置子View的最大尺寸為父View
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局給子View一個最大界限
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // 如果子view有自己的尺寸,則使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 當(dāng)子View是match_parent,父View的尺寸為子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 如果子View是wrap_content,父View的尺寸為子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局對子View沒有做任何限制
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                //如果子view有自己的尺寸,則使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //因父布局沒有對子View做出限制,當(dāng)子View為MATCH_PARENT時則大小為0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //因父布局沒有對子View做出限制,當(dāng)子View為WRAP_CONTENT時則大小為0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //通過Mode 和 Size 生成新的SpecMode 返回
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

注:這里只是給子View設(shè)置了MeasureSpec參數(shù),真正的大小是在View中具體設(shè)置的,只是給子View做了一個限制。子View的測量模式由自身的LayoutParams和父View的MeasureSpec來決定。

回過頭來看View.onMeasure()方法:就一行代碼,但是有三個函數(shù):
setMeasuredDimension(int measuredWidth, int measuredHeight) :該方法用來設(shè)置View的寬高
getDefaultSize(int size, int measureSpec): 該方法用來獲取View默認(rèn)的寬高
getSuggestedMinimumWidth(): 該方法用于獲取Android:minWidth屬性的值,如果沒有則為0(如果有背景還需要判斷背景與mMinWidth的大小,取大值)
這里面其實最重要的是getDefaultSize方法,對于AT_MOST和EXACTLY在View當(dāng)中的處理是完全相同的,在我們自定義View時要對這兩種模式做出處理。

  /**
  *   有兩個參數(shù)size和measureSpec
  *   1、size表示View的默認(rèn)大小,它的值是通過`getSuggestedMinimumWidth()方法來獲取的,之后我們再分析。
  *   2、measureSpec則是我們之前分析的MeasureSpec,里面存儲了View的測量值以及測量模式
  */
  public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        //從這里我們看出,對于AT_MOST和EXACTLY在View當(dāng)中的處理是完全相同的。所以在我們自定義View時要對這兩種模式做出處理。
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

ViewGroup除了測量自身,還需要測量子View的大小,ViewGroup中提供了對子View的測量方法:measureChildren(),在measureChildren中遍歷所有子View,調(diào)用measureChild(),在measureChild中調(diào)用了View的measure()方法,讓子View測量自身大小。

2、layout布局流程,layout()過程,對于View來說用來計算View的位置參數(shù),對于ViewGroup來說,除了要測量自身位置,還需要測量子View的位置。其實layout最重要的在自定義ViewGroup時的重寫,對其子類進(jìn)行布局。

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //onLayout方法是一個空實現(xiàn),我們在自定義View的時候關(guān)注重新這個onLayout方法即可,可以看看LinearLayout的onLayout方法
            onLayout(changed, l, t, r, b);
            //省略很多代碼....
        }
    }

3、draw繪制流程:draw繪制流程就是繪制View的過程,整個過程可以分為6個步驟:
1.如果需要,繪制背景
2.如果有必要,保存當(dāng)前canvas
3.繪制View的內(nèi)容
4.繪制子View
5.如果有必要,繪制邊緣,陰影等
6.繪制裝飾,如滾動條等

public void draw(Canvas canvas) {

        int saveCount;
        // 1. 如果需要,繪制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // 2. 如果有必要,保存當(dāng)前canvas。
        final int viewFlags = mViewFlags;
      
        if (!verticalEdges && !horizontalEdges) {
            // 3. 繪制View的內(nèi)容。
            if (!dirtyOpaque) onDraw(canvas);

            // 4. 繪制子View。
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // 如果有必要,繪制邊緣,陰影等
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // 6. 繪制裝飾,如滾動條等等。
            onDrawForeground(canvas);

            // we're done...
            return;
        }
    }
    
    /**
    *  1.繪制View背景
    */
    private void drawBackground(Canvas canvas) {
        //獲取背景
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();

        //獲取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,則會在平移后的canvas上面繪制背景。
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
    
    /**
    * 3.繪制View的內(nèi)容,該方法是一個空的實現(xiàn),在各個業(yè)務(wù)當(dāng)中自行處理。
    */
    protected void onDraw(Canvas canvas) {
    }
    
    /**
    * 4. 繪制子View。該方法在View當(dāng)中是一個空的實現(xiàn),在各個業(yè)務(wù)當(dāng)中自行處理。
    *  在ViewGroup當(dāng)中對dispatchDraw方法做了實現(xiàn),主要是遍歷子View,并調(diào)用子類的draw方法,一般我們不需要自己重寫該方法。
    */
    protected void dispatchDraw(Canvas canvas) {

    }

4、Paint、Canvas、Path等相關(guān)內(nèi)容可以看看: 扔物線的自定義View

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容