自定義View繪制流程分析

/ 前言 /

本文用于記錄自定義View的基礎(chǔ)步驟以及一些基礎(chǔ)的信息,后期可能針對具體的點寫一些補充性的文章。

image

/ View中關(guān)于四個構(gòu)造函數(shù)參數(shù) /

自定義View中View的構(gòu)造函數(shù)有四個。

//  主要是在java代碼中創(chuàng)建一個View時所調(diào)用,沒有任何參數(shù),一個空的View對象
public ChildrenView(Context context) {
 super(context);
}
// 在布局文件中使用該自定義view的時候會調(diào)用到,一般會調(diào)用到該方法
public ChildrenView(Context context, AttributeSet attrs) {
    this(context, attrs,0);
}
//如果你不需要View隨著主題變化而變化,則上面兩個構(gòu)造函數(shù)就可以了
//下面兩個是與主題相關(guān)的構(gòu)造函數(shù)

public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}
//
public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
} 

四個參數(shù)解釋:

  • context:上下文

  • AttributeSet attrs:從xml中定義的參數(shù)

  • int defStyleAttr:主題中優(yōu)先級最高的屬性

  • int defStyleRes:優(yōu)先級次之的內(nèi)置于View的style(這里就是自定義View設(shè)置樣式的地方)

多個地方定義屬性,優(yōu)先級排序 Xml直接定義 > xml中style引用 > defStyleAttr>defStyleRes > theme直接定義。

/ 自定義屬性說明 /

image

除了基本類型的不說 講一下其它幾個吧:

  • color:引用顏色

  • dimension:引用字體大小


//定義
<attr name = "text_size" format = "dimension" />
//使用:
    app:text_size = "28sp" 
或者 
    app:text_size = "@android:dimen/app_icon_size" 

  • enum:枚舉值

//定義
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
//使用:
    app:orientation = "vertical" 

  • flags:標志 (位或運行) 主要作用=可以多個值

//定義
  <attr name="gravity">
            <flag name="top" value="0x01" />
            <flag name="bottom" value="0x02" />
            <flag name="left" value="0x04" />
            <flag name="right" value="0x08" />
            <flag name="center_vertical" value="0x16" />
    </attr>
// 使用
app:gravity = Top|left 

  • fraction:百分數(shù)

//定義:
<attr name = "transparency" format = "fraction" />
//使用:
  app:transparency = "80%" 

  • reference:參考/引用某一資源ID

//定義:
 <attr name="leftIcon" format="reference" />
//使用:
app:leftIcon = "@drawable/圖片ID" 

  • 混合類型:屬性定義時指定多種類型值

//屬性定義
 <attr name = "background" format = "reference|color" />
//使用
android:background = "@drawable/圖片ID" 
//或者
android:background = "#FFFFFF" 

/ 自定義控件類型 /

image

自定義組合控件步驟

自定義屬性

在res/values目錄下的attrs.xml文件中。


<resources>
<declare-styleable name="CustomView">
        <attr name="leftIcon" format="reference" />
        <attr name="state" format="boolean"/>
        <attr name="name" format="string"/>
    </declare-styleable>
</resources> 

布局中使用自定義屬性

在布局中使用。


<com.myapplication.view.CustomView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:leftIcon="@mipmap/ic_temp"
                app:name="溫度"
                app:state="false" /> 

view的構(gòu)造函數(shù)獲取自定義屬性

class DigitalCustomView : LinearLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        LayoutInflater.from(context).inflate(R.layout.view_custom, this)
        var ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
        mIcon = ta.getResourceId(R.styleable.CustomView_leftIcon, -1) //左圖像
        mState = ta.getBoolean(R.styleable.DigitalCustomView_state, false)
        mName = ta.getString(R.styleable.CustomView_name)
        ta.recycle()
        initView()
    }

} 

上面給出大致的代碼,記得獲取context.obtainStyledAttributes(attrs, R.styleable.CustomView),最后要釋放掉ta.recycle(),這里之所以調(diào)用recycle資源,是因為使用了緩存池來減少常用對象的頻繁創(chuàng)建和釋放所帶來的性能開銷。

繼承系統(tǒng)控件

就是繼承系統(tǒng)已經(jīng)提供好給我們的控件例如TextView、LinearLayout等,分為View類型或者ViewGroup類型的兩種。主要根據(jù)業(yè)務(wù)需求進行實現(xiàn),實現(xiàn)重寫的空間也很大,主要看需求。比如需求 :在文字后面加個顏色背景。

根據(jù)需要一般這種情況下我們是希望可以復用系統(tǒng)的onMeaseur和onLayout流程.直接復寫onDraw方法。

class Practice02BeforeOnDrawView : AppCompatTextView {
    internal var paint = Paint(Paint.ANTI_ALIAS_FLAG)
    internal var bounds = RectF()

    constructor(context: Context) : super(context) {}

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

    init {
        paint.color = Color.parseColor("#FFC107")
    }

    override fun onDraw(canvas: Canvas) {
        // 把下面的繪制代碼移到 super.onDraw() 的上面,就可以讓原主體內(nèi)容蓋住你的繪制代碼了
        // (或者你也可以把 super.onDraw() 移到這段代碼的下面)
        val layout = layout
        bounds.left = layout.getLineLeft(1)
        bounds.right = layout.getLineRight(1)
        bounds.top = layout.getLineTop(1).toFloat()
        bounds.bottom = layout.getLineBottom(1).toFloat()
       //繪制方形背景
        canvas.drawRect(bounds, paint)
        super.onDraw(canvas)
    }
} 

這里會涉及到畫筆Paint()、畫布canvas、路徑Path、繪畫順序等的一些知識點,后面再詳細說明。

直接繼承View

這種就是類似TextView等,不需要去輪訓子View只需要根據(jù)自己的需求重寫onMeasure()、onLayout()、onDraw()等方法便可以,要注意點就是記得Padding等值要記得加入運算。


private int getCalculateSize(int defaultSize, int measureSpec) {
        int finallSize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
     //  根據(jù)模式對
        switch (mode) {
            case MeasureSpec.EXACTLY: {
              ...
                break;
            }
            case MeasureSpec.AT_MOST: {
                ...
                break;
            }
            case MeasureSpec.UNSPECIFIED: {
               ...
                break;
            }
        }
        return finallSize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getCalculateSize(120, widthMeasureSpec);
        int height = getCalculateSize(120, heightMeasureSpec);
        setMeasuredDimension(width, height);
}

  //畫一個圓
    @Override
    protected void onDraw(Canvas canvas) {
        //調(diào)用父View的onDraw函數(shù),因為View這個類幫我們實現(xiàn)了一些基本的而繪制功能,比如繪制背景顏色、背景圖片等
        super.onDraw(canvas);
        int r = getMeasuredWidth() / 2;
        //圓心的橫坐標為當前的View的左邊起始位置+半徑
        int centerX = getLeft() + r;
        //圓心的縱坐標為當前的View的頂部起始位置+半徑
        int centerY = getTop() + r;

        Paint paint = new Paint();
        paint.setColor(Color.RED);
        canvas.drawCircle(centerX, centerY, r, paint);
    } 

直接繼承ViewGroup

類似實現(xiàn)LinearLayout等,可以去看那一下LinearLayout的實現(xiàn) 基本的你可能要重寫onMeasure()、onLayout()、onDraw()方法,這塊很多問題要處理,包括輪訓childView的測量值以及模式進行大小邏輯計算等,這個篇幅過大后期加多個文章寫詳細的。這里寫個簡單的需求,模仿LinearLayout的垂直布局。


class CustomViewGroup :ViewGroup{

    constructor(context:Context):super(context)
    constructor(context: Context,attrs:AttributeSet):super(context,attrs){
            //可獲取自定義的屬性等
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //將所有的子View進行測量,這會觸發(fā)每個子View的onMeasure函數(shù)
        measureChildren(widthMeasureSpec, heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        val childCount = childCount
        if (childCount == 0) {
            //沒有子View的情況
            setMeasuredDimension(0, 0)
        } else {
            //如果寬高都是包裹內(nèi)容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我們將高度設(shè)置為所有子View的高度相加,寬度設(shè)為子View中最大的寬度
                val height = getTotalHeight()
                val width = getMaxChildWidth()
                setMeasuredDimension(width, height)
            } else if (heightMode == MeasureSpec.AT_MOST) {
                //如果只有高度是包裹內(nèi)容
                //寬度設(shè)置為ViewGroup自己的測量寬度,高度設(shè)置為所有子View的高度總和
                setMeasuredDimension(widthSize, getTotalHeight())
            } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有寬度是包裹內(nèi)容
                //寬度設(shè)置為子View中寬度最大的值,高度設(shè)置為ViewGroup自己的測量值
                setMeasuredDimension(getMaxChildWidth(), heightSize)

            }
        }
    /***
     * 獲取子View中寬度最大的值
     */
    private fun getMaxChildWidth(): Int {
        val childCount = childCount
        var maxWidth = 0
        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            if (childView.measuredWidth > maxWidth)
                maxWidth = childView.measuredWidth

        }
        return maxWidth
    }

    /***
     * 將所有子View的高度相加
     */
    private fun getTotalHeight(): Int {
        val childCount = childCount
        var height = 0
        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            height += childView.measuredHeight

        }

        return height
    }

    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val count = childCount
        var currentHeight = t
        for (i in 0 until count) {
            val child = getChildAt(i)
            val h = child.measuredHeight
            val w = child.measuredWidth
            //擺放子view
            child.layout(l, currentHeight, l + w, currentHeight + h)
            currentHeight += h
        }
    }
} 

image

主要兩點 先 measureChildren()輪訓遍歷子View獲取寬高,并根據(jù)測量模式邏輯計算最后所有的控件的所需寬高,最后setMeasuredDimension()保存一下。

/ 繪制流程相關(guān)知識點 /

View的繪制流程相關(guān) 最基本的三個相關(guān)函數(shù) measure() ->layout()->draw()。

image

/ onMeasure()相關(guān)知識點 /

MeasureSpec

MeasureSpec是View的內(nèi)部類,它封裝了一個View的尺寸,在onMeasure()當中會根據(jù)這個MeasureSpec的值來確定View的寬高。MeasureSpec 的數(shù)據(jù)是int類型,有32位。高兩位表示模式,后面30位表示大小size。則MeasureSpec = mode+size三種模式分別為:EXACTLY,AT_MOST,UNSPECIFIED

  • EXACTLY: (match_parent或者 精確數(shù)據(jù)值)精確模式,對應(yīng)的數(shù)值就是MeasureSpec當中的size。

  • AT_MOST:(wrap_content)最大值模式,View的尺寸有一個最大值,View不超過MeasureSpec當中的Size值。

  • UNSPECIFIED:(一般系統(tǒng)使用)無限制模式,View設(shè)置多大就給他多大。


//獲取測量模式
 val widthMode = MeasureSpec.getMode(widthMeasureSpec)
//獲取測量大小 
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
//通過Mode和Size構(gòu)造MeasureSpec
val measureSpec = MeasureSpec.makeMeasureSpec(size, mode); 

View #onMeasure()源碼


protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    public static int getDefaultSize(int size, int measureSpec) {
        int result = 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;
    }
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    } 

setMeasuredDimension(int measuredWidth, int measuredHeight) :用來設(shè)置View的寬高,在我們自定義View保存寬高也會要用到。

getSuggestedMinimumWidth():當View沒有設(shè)置背景時,默認大小就是mMinWidth,這個值對應(yīng)Android:minWidth屬性,如果沒有設(shè)置時默認為0. 如果有設(shè)置背景,則默認大小為mMinWidth和mBackground.getMinimumWidth()當中的較大值。

getDefaultSize(int size, int measureSpec):用來獲取View默認的寬高,在getDefaultSize()中對MeasureSpec.AT_MOST,MeasureSpec.EXACTLY兩個的處理是一樣的,我們自定義View的時候 要對兩種模式進行處理。

ViewGroup中并沒有measure()也沒有onMeasure()

因為ViewGroup除了測量自身的寬高,還需要測量各個子View的寬高,不同的布局測量方式不同 (例如 LinearLayout跟RelativeLayout等布局),所以直接交由繼承者根據(jù)自己的需要去復寫。但是里面因為子View的測量是相對固定的,所以里面已經(jīng)提供了基本的measureChildren()以及measureChild()來幫助我們對子View進行測量。

/ onLayout相關(guān) /

View.java的onLayout方法是空實現(xiàn):因為子View的位置,是由其父控件的onLayout方法來確定的。onLayout(int l, int t, int r, int b)中的參數(shù)l、t、r、b都是相對于其父 控件的位置。自身的mLeft, mTop, mRight, mBottom都是相對于父控件的位置。

Android坐標系

image

內(nèi)部View坐標系跟點擊坐標

image

event.getX():表示的是觸摸的點距離自身左邊界的距離
event.getY():表示的是觸摸的點距離自身上邊界的距離
event.getRawX:表示的是觸摸點距離屏幕左邊界的距離
event.getRawY:表示的是觸摸點距離屏幕上邊界的距離
View.getWidth():表示的是當前控件的寬度,即getRight()-getLeft()
View.getHeight():表示的是當前控件的高度,即getBottom()-getTop()
View.getTop():子View的頂部到父View頂部的距離
View.getRight():子View的右邊界到父View的左邊界的距離
View.getBottom():子View的底部到父View的頂部的距離
View.getLeft():子View的左邊界到父View的左邊界的距離
View.getTranslationX()計算的是該View在X軸的偏移量。初始值為0,向左偏移值為負,向右偏移值為正。
View.getTranslationY()計算的是該View在Y軸的偏移量。初始值為0,向上偏移為負,向下偏移為證

看一下View#layout(int l, int t, int r, int b)源碼


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(changed, l, t, r, b);
   //   ....省略其它部分
    }
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
  protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
    // ....省略其它部分
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            invalidate(sizeChanged);
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                invalidateParentCaches();
            }
            mPrivateFlags |= drawn;
            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    } 

四個參數(shù)l、t、r、b分別代表View的左、上、右、下四個邊界相對于其父View的距離。在調(diào)用onLayout(changed, l, t, r, b);之前都會調(diào)用到setFrame()確定View在父容器當中的位置,賦值給mLeft,mTop,mRight,mBottom。在ViewGroup#onLayout()跟View#onLayout()都是空實現(xiàn),交給繼承者根據(jù)自身需求去定位。

部分零散知識點:

getMeasureWidth()與getWidth()getMeasureWidth()返回的是mMeasuredWidth,而該值是在setMeasureDimension()中的setMeasureDimensionRaw()中設(shè)置的。因此onMeasure()后的所有方法都能獲取到這個值。getWidth返回的是mRight-mLeft,這兩個值,是在layout()中的setFrame()中設(shè)置的.getMeasureWidthAndState中有一句:This should be used during measurement and layout calculations only. Use {@link #getWidth()} to see how wide a view is after layout.

總結(jié):只有在測量過程中和布局計算時,才用getMeasuredWidth()。在layout之后,用getWidth()來獲取寬度。

/ draw()繪畫過程 /

/*
         * 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)
         */ 

上面是draw()里面寫的繪畫順序。

  1. 繪制背景。

  2. 如果必要的話,保存當前canvas

  3. 繪制View的內(nèi)容

  4. 繪制子View

  5. 如果必要的話,繪畫邊緣重新保存圖層

  6. 畫裝飾(例如滾動條)

看一下View#draw()源碼的實現(xiàn)


public void draw(Canvas canvas) {
  // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
} 

由上面可以看到 先調(diào)用drawBackground(canvas) ->onDraw(canvas)->dispatchDraw(canvas)->onDrawForeground(canvas)越是后面繪畫的越是覆蓋在最上層。

drawBackground(canvas):畫背景,不可重寫

onDraw(canvas):畫主體

  • 代碼寫在super.onDraw()前:會被父類的onDraw覆蓋

  • 代碼寫在super.onDraw()后:不會被父類的onDraw覆蓋

dispatchDraw() :繪制子 View 的方法

  • 代碼寫在super.dispatchDraw(canvas)前:把繪制代碼寫在 super.dispatchDraw() 的上面,這段繪制就會在 onDraw() 之后、 super.dispatchDraw() 之前發(fā)生,也就是繪制內(nèi)容會出現(xiàn)在主體內(nèi)容和子 View 之間。而這個…… 其實和重寫 onDraw() 并把繪制代碼寫在 super.onDraw() 之后的做法,效果是一樣的。

  • 代碼寫在super.dispatchDraw(canvas)后:只要重寫 dispatchDraw(),并在 super.dispatchDraw() 的下面寫上你的繪制代碼,這段繪制代碼就會發(fā)生在子 View 的繪制之后,從而讓繪制內(nèi)容蓋住子 View 了。

onDrawForeground(canvas):包含了滑動邊緣漸變和滑動條跟前景。

一般來說,一個 View(或 ViewGroup)的繪制不會這幾項全都包含,但必然逃不出這幾項,并且一定會嚴格遵守這個順序。例如通常一個 LinearLayout 只有背景和子 View,那么它會先繪制背景再繪制子 View;一個 ImageView 有主體,有可能會再加上一層半透明的前景作為遮罩,那么它的前景也會在主體之后進行繪制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其實也有,不過只支持 FrameLayout,而直到 6.0 才把這個支持放進了 View 類里。

image

注意事項

在 ViewGroup 的子類中重寫除 dispatchDraw() 以外的繪制方法時,可能需要調(diào)用 setWillNotDraw(false);

出于效率的考慮,ViewGroup 默認會繞過 draw() 方法,換而直接執(zhí)行 dispatchDraw(),以此來簡化繪制流程。所以如果你自定義了某個 ViewGroup 的子類(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一個繪制方法內(nèi)繪制內(nèi)容,你可能會需要調(diào)用 View.setWillNotDraw(false) 這行代碼來切換到完整的繪制流程(是「可能」而不是「必須」的原因是,有些 ViewGroup 是已經(jīng)調(diào)用過 setWillNotDraw(false) 了的,例如 ScrollView)。

在重寫的方法有多個選擇時,優(yōu)先選擇 onDraw()

一段繪制代碼寫在不同的繪制方法中效果是一樣的,這時你可以選一個自己喜歡或者習慣的繪制方法來重寫。但有一個例外:如果繪制代碼既可以寫在 onDraw() 里,也可以寫在其他繪制方法里,那么優(yōu)先寫在 onDraw() ,因為 Android 有相關(guān)的優(yōu)化,可以在不需要重繪的時候自動跳過 onDraw() 的重復執(zhí)行,以提升開發(fā)效率。享受這種優(yōu)化的只有 onDraw() 一個方法。

/ 在Activity中獲取寬高 /

Activity獲取view的寬高, 在onCreate , onResume等方法中獲取到的都是0, 因為View的測量過程并不是和Activity的聲明周期同步執(zhí)行的。

view.postpost可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調(diào)用此runnable的時候, View也已經(jīng)初始化好了。


view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight(); 
            }
        }); 

ViewTreeObserver使用addOnGlobalLayoutListener接口, 當view樹的狀態(tài)發(fā)生改變或者View樹內(nèi)部的view的可見性發(fā)生改變時,onGlobalLayout都會被調(diào)用, 需要注意的是,onGlobalLayout方法可能被調(diào)用多次, 代碼如下:

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        }); 

onWindowFocusChanged這個方法的含義是View已經(jīng)初始化完畢了,在第一幀繪制完成后開始調(diào)用,這時 寬高已經(jīng)準備好了, 需要注意的就是這個方法可能會調(diào)用多次, 在Activity onResume和onPause的時候都會調(diào)用, 也會有多次調(diào)用的情況。

     @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if(hasWindowFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

最后編輯于
?著作權(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ù)。

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