自定義VIEW

HFW發表於2019-03-27

前言

上篇文章複習總結了Android中常見的佈局和佈局引數,這篇文章就來複習總結下自定義View(當然只是簡單的)。那麼什麼時候需要使用自定義View? 當現有的元件無法滿足我們的需要的我們就可能得使用自定義View。

一、View的工作流程

view的工作流程指的是View的三大方法measure、layout、draw。其中measure用來測量View的寬和高,layout用來決定View的位置,draw用於繪製View,下面先從入口開始說起

入口

既然View顯示在Activity內,那麼先從Activity啟動說起,這裡省略前面的相關步驟直接從handleLaunchActivity開始

// ActivityThread
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
	Activity a = performLaunchActivity(r, customIntent);
	handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
    Context appContext = createBaseContextForActivity(r, activity);
    activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window);
    mInstrumentation.callActivityOnCreate(activity, r.state);
}
final void handleResumeActivity(IBinder token,
                                    boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    r = performResumeActivity(token, clearHide, reason);
    // wm為WindowManagerImpl例項,decor為DecorView例項
    wm.addView(decor, l);
}
// WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    // mGlobal為WindowMangerGlobal例項
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// WindowMangerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    root = new ViewRootImpl(view.getContext(), display);
    root.setView(view, wparams, panelParentView);
}
// ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    requestLayout();
    // 設定Activity的decorView的parent為ViewRootImpl例項
    view.assignParent(this);
}
public void requestLayout() {
    scheduleTraversals();
}
void scheduleTraversals() {
    // 在螢幕重新整理訊號到來以後會呼叫mTraversalRunnable.run()
    mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
void doTraversal() {
    // 該方法內部真正進行View的三大流程
    performTraversals();
}
private void performTraversals() {
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();
}
複製程式碼

知道了入口了以後,我們首先來看看View的measure過程吧

Measure

View的測量從DecorView開始,一層層的進行遞迴直到呼叫了所有View的onMeasure方法,繼續從performTraversals開始

private void performTraversals() {
    // 對於DecorView來說其onMeasure的兩個引數由視窗大小和WindowManger.LayoutParams決定
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼

performMeasure需要兩個引數,都是通過getRootMeasureSpec獲取的

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
        switch (rootDimension) {
            case ViewGroup.LayoutParams.MATCH_PARENT:
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
        }
        return measureSpec;
}
複製程式碼

根據視窗的大小和佈局引數決定,繼續看看performMeasure

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    // 這裡的mView就是DecorView
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製程式碼

裡面呼叫了onMeasure, 對於View只需要測量自身即可,但是對於ViewGroup需要測量所有的子View,首先看看View的onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 呼叫了該方法以後該View的大小就被測量完了
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
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;
}
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);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
複製程式碼

由此可見以下兩點

  • setMeasureDimension就是用來設定mMeasuredWidthmMeasuredHeight的,預設View的onMeasure實現測量模式為MeasureSpec.AT_MOSTMeasureSpec.EXACTLY時取的大小是一樣的,也就是說在佈局檔案中設定為wrap_contentmatch_parent效果是一樣的
  • 當測量模式為MeasureSpec.UNSPECIFIED時View沒有設定背景就返回自動最小寬/高,不然返回背景的最小寬/高和自身最小寬/高直接的最大值

下面再看看ViewGroup由於其需要測量所有子View,並根據自己的規則決定最後需要多少尺寸,而且每個ViewGroup的規則都不盡相同因此ViewGroup並沒有重寫onMeasure,但是定義了一個measureChildren

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        // 如果View的Visibility不是Gone就measureChild
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    // 根據父View的measureSpec和子View的LayoutParams,以及對應方向的padding來決定子View的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
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) {
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製程式碼

我們通過一張表格來歸納下getChildMeasureSpec的結果

父View的measureSpec 子View的LayoutParams 結果
測量模式:MeasureSpec.EXACTLY 尺寸:A 固定值B 測量模式:MeasureSpec.EXACTLY 尺寸:固定值B
測量模式:MeasureSpec.EXACTLY 尺寸:A MATCH_PARENT 測量模式:MeasureSpec.EXACTLY 尺寸:A-padding
測量模式:MeasureSpec.EXACTLY 尺寸:A WRAP_CONTENT 測量模式:MeasureSpec.AT_MOST 尺寸:A-padding
測量模式:MeasureSpec.AT_MOST 尺寸:A 固定值B 測量模式:MeasureSpec.EXACTLY 尺寸:固定值B
測量模式:MeasureSpec.AT_MOST 尺寸:A MATCH_PARENT 測量模式:MeasureSpec.AT_MOST 尺寸:A-padding
測量模式:MeasureSpec.AT_MOST 尺寸:A WRAP_CONTENT 測量模式:MeasureSpec.AT_MOST 尺寸:A-padding
測量模式:MeasureSpec.UNSPECIFIED 尺寸:A 固定值B 測量模式:MeasureSpec.EXACTLY 尺寸:固定值B
測量模式:MeasureSpec.UNSPECIFIED 尺寸:A MATCH_PARENT 測量模式:MeasureSpec.UNSPECIFIED 尺寸:A-padding
測量模式:MeasureSpec.UNSPECIFIED 尺寸:A WRAP_CONTENT 測量模式:MeasureSpec.UNSPECIFIED 尺寸:A-padding

Layout

Layout方法的作用是為了確定元素的位置,接著看看performLayout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    // host就是decorView
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
複製程式碼

該方法的作用就是拿到decorView測量完的長/寬然後給出DecorView在螢幕中的位置要求其為它的子View進行定位

public void layout(int l, int t, int r, int b) {
    onLayout(changed, l, t, r, b);
}
複製程式碼

onLayout方法在View裡面是一個空實現,因為每個ViewGroup都有其自己的佈局方式

Draw

Draw方法的作用是用來繪製UI,接著看看performDraw

private void performDraw() {
    draw(fullRedrawNeeded);
}
private void draw(boolean fullRedrawNeeded) {
    drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
    mView.draw(canvas);
}
複製程式碼

這裡又呼叫了View的draw方法

public void draw(Canvas canvas) {
    /*
     *      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)
     */
    drawBackground(canvas);
    onDraw(canvas);
    // 去呼叫子View的draw方法
    dispatchDraw(canvas);
    onDrawForeground(canvas);
}
複製程式碼

這裡主要就是繪製背景、呼叫onDraw、呼叫子View的draw、繪製前景色

二、自定義View基本流程

最基本的自定義View需要進行以下兩個步驟

1. 繼承

首先,自定義View的時候我們一般會選擇繼承自現有的View的子類或者直接繼承View,在繼承的時候得注意一定要有兩個引數(Context、AttributeSet)的構造方法除非這個View不在xml裡面使用,因為當LayoutInflate在解析xml的時候會通過反射呼叫兩個引數的構造器來建立View,如果找不到該構造器將導致程式crash

2. 自定義屬性

我們可以在values目錄下面新建一個declare-styleable來定義屬性,然後在佈局檔案中使用注意需要引用以下名稱空間,然後在自定義View的構造器中通過obtainStyledAttributes獲取屬性值

xmlns:app="http://schemas.android.com/apk/res-auto"
複製程式碼

3. 重寫

其次,我們需要重寫幾個方法,一般我們自定義View時需要重寫onMeasure()onDraw(),自定義ViewGroup則是需要重寫onMeasureonLayout,三個方法的作用如下所示

  • onMeasure 用來測量View的寬和高
  • onLayout 用來確定View的位置
  • onDraw 用來繪製View

三、例項

詳見以前寫的一個自定義ViewPager和TabLayout

相關文章