重拾Android自定義View

笑嘆人生發表於2020-04-18

 Android做了這麼久了,自定義view平常也用的不是特別多,可能自己做的專案太low了吧,有的知識放在一邊不用它,久了,也就容易生疏了,這裡記錄一篇部落格,記錄記錄一下,一個View的展示分為三個主要的步驟:onMeasure()、onLayout()、onDraw(),分別是測量,佈局,繪製,按照步驟依次說一下這三個方法

onMeasure()

 我們在xml佈局中其實也能設定佈局的寬高,直接寫死寬高,或者用match_parent、wrap_content之類的去限定view的寬高,用這些方法可能一些平常的view也就能滿足了,但是如果我要你根據一個這個view中的子view來去縮放當前view的大小,這個你就沒辦法去做了吧,這時候就需要我們重寫onMeasure方法了

@Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		//根據子view重新測量view
    	int n = getChildCount();
        for (int i = 0; i < n; i++)
           measureView(getChildAt(i));//具體實現方法
}
複製程式碼

 onMeasure方法重寫時提供了兩個引數,widthMeasureSpec與heightMeasureSpec,這兩個引數是根據測量規格和測量大小得到的,我們可以通過getMode()和getSize()得到specMode與specSize

 int mode = MeasureSpec.getMode(widthMeasureSpec);
 int size = MeasureSpec.getSize(widthMeasureSpec);
複製程式碼

 好了,說到specMode,那就不得不提一提這個specMode,這個關鍵呀,也是非常好理解的一個specMode,specMode有三個模式,分別是

  • MeasureSpec.EXACTLY : 這個模式我們把它對應成match_parent來理解,就好理解多了,就是把父控制元件給我們留下的那點多餘的位置給佔了,就比如一排五個座位,兩個位置被其他的人給坐了,我設定了這個模式,那我就把剩下的三個位置給佔了,我想怎麼坐就怎麼坐,我想怎麼躺就怎麼躺
  • MeasureSpec.AT_MOST: 這個模式我們把它對應成wrap_content來理解,就是子view能佔多少位置,那我就佔多大的位置,就比如穿緊身衣一樣,貼身就好
  • MeasureSpec.UNSPECIFIED: 這個模式我們把它對應成固定寫死寬高來理解,不干涉別人,比如一排五個位置,我說就佔兩個,如果我有三個人,那另一個就擠出去吧

 上面介紹了onMeasure方法中的mode,size,但是我們還有一點估計是懵逼的,就是widthMeasureSpec與heightMeasureSpec哪來的,他們是根據mode和size得到的,但是是誰給的呢?還有我為什麼說把那三個模式對應成match_parent來理解呢?

第一個問題:  我們要知道這兩個引數是怎麼來的,那我們就需要去View中測量方法中看看到底啥情況 測量方法中,我們重點關注呼叫的onMeasure方法,他主要是通過getDefaultSize獲取了View的寬高,再通過setMeasuredDimension去指定了view的寬高

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
複製程式碼
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製程式碼
    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;
    }
複製程式碼

第二個問題:  我們View的三個重要流程都是通過ViewRoot來完成的,而ViewRoot的對應類是ViewRootImpl

下面的方法是ViewRootImpl中根據視窗的佈局引數計算出根檢視的度量規範,這裡明顯就可以看出他們之間的對應關係了

  private static 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;
    }
複製程式碼

onLayout()

 我們要展示的View已經測量好了,知道了他應該展示的大小應該是多少了,接下來就是佈局了,將View佈局在介面上,在ViewRootImpl中也是在測量完成後,執行performLayout方法,performLayout方法中呼叫了View的layout方法,接收四個引數,就是四個方向,View的位置的擺放都是在layout方法中進行了,layout方法中還提供了一個onLayout的空方法,在老版本中該方法是抽象方法,每次自定義View的時候都需要重寫的,現在已經不需要了,已經改成了一個空方法,可以根據自己的需求去重寫了

 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);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }
複製程式碼

onDraw()

 View的大小、擺放位置都確定了,接下來就是將這個View畫出來了,根據上面提到的,都會走View的measure、layout,那麼接下來自然就是draw了,draw中比較多,我們只要關注onDraw()這個方法就行了,原始碼中也寫了註釋,繪製內容,這裡提供出了畫布,讓你自己愛咋畫咋畫

  public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

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

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

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

        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */

        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;

        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;

        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        // 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
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        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);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        // 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);
    }
複製程式碼

自定義屬性

 自定義的屬性我們放在res中的values中的attrs.xml檔案中(沒有自行建立)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="drawPen">
        <attr name="pen_color" format="color"/>
        <attr name="pen_icon" format="reference"/>
    </declare-styleable>
</resources>
複製程式碼

有多種format屬性型別可以選擇

  • reference:引用資源
  • string:字串
  • Color:顏色
  • boolean:布林值
  • dimension:尺寸值
  • float:浮點型
  • integer:整型
  • fraction:百分數
  • enum:列舉型別
  • flag:位或運算

使用方法: 在構造中去獲取屬性,這裡獲取到的屬性就是我們在xml中設定的屬性 這裡的app就是在根佈局加上名稱空間 xmlns:app="schemas.android.com/apk/res-aut…"

	<com.example.SignatureView
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		android:layout_below="@+id/topBar"
		android:id="@+id/signView"
		android:visibility="gone"
		app:pen_color="#FFF"/>
複製程式碼
    public SignatureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.drawPen);
        int color = typedArray.getColor(R.styleable.drawPen_pen_color, Color.BLUE);
        Drawable drawable = typedArray.getDrawable(R.styleable.drawPen_pen_icon);
    }
複製程式碼

相關文章