1. 程式人生 > >View背後不為人知的勾當(一)--自定義控制元件和測量過程

View背後不為人知的勾當(一)--自定義控制元件和測量過程

本節首先講講自定義控制元件套路,自定義屬性,測量過程的問題,為了後面的幾節能專注於實現特效,而不受本文這些慣用套路的影響

  • 特效系列的目錄

    • 關於Draw
    • 關於動畫
    • 關於滑動
    • 關於Layout
  • 要實現介面特效,首先得掌握:

    • View的簡單原理
    • 自定義屬性
    • measure過程
    • 這算是UI特效的周邊技術,任何特效都得考慮這幾個問題
  • 安卓特效的實現,需要藉助以下四個技術點:

    • draw
    • 動畫
    • 滑動
    • layout

1 View的原理

  • 原理
    • 一個Activity包含一個Window物件,也就是PhoneWindow
    • 在onResume()方法裡,系統才會把DecoreView新增到PhoneWindow中
    • DecoreView將內容顯示在PhoneWindow上,所有View的監聽都通過WM接收,並通過Activity回撥響應的onClickListener
    • DecoreView包含一個TitleView,一個ContentView,ContentView的id是android.R.id.content,所以如果你的自定義佈局如SwipeBack需要將ToolBar包含在內,需要考慮把你的佈局新增到DecoreView下,但國內一般不用ToolBar,而是用自己定義的TitleBar,看情況
    • ContentView下才是setContentView設定的內容
    • requestWindowFeature()必須在setContentView之前呼叫

2 自定義屬性

宣告和使用自定義屬性:

定義attr:在values目錄下,attrs.xml

<declare-styleable name="TopBar">
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <attr name="titleBg"
format="reference|color" />
</declare-styleable> 使用attr:在任意佈局檔案裡 <xx.xx.xx.TopBar xmlns:custom="http://schemas.android.com/apk/res-auto" android:id="@+id/topbar" custom:title="title" custom:titleTextSize="15sp" custom:titleTextColor="#aaaaaa" custom:titleBg="@drawable/ic_launcher" />

處理自定義屬性

取出xml中設定的屬性

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
mTitle = ta.getString(R.styleable.TopBar_title);
mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitleBg = ta.getDrawable(R.styleable.TopBar_titleBg);
ta.recycle();

3 measure過程

  • 需要知道的

    • View的測量過程是最複雜的,對於view來說,得考慮內容寬高的計算,對於ViewGroup來說,得考慮具體佈局,padding,margin,weight等
    • 可能有時需要重複measure
    • MeasureSpec類:32位的int值,高2位是測量模式(最多表示4個模式),低30位是測量的大小,即寬或高的值
    • 3種測量模式
      • EXACTLY: 精確值
        • match_parent和dp,px都相當於指定具體數值
        • 特別注意一下match_parent,如果子View是match_parent, 父View是wrap_content,這裡就有衝突了
          • 子會被降級為AT_MOST
      • AT_MOST
        • wrap_content表示控制元件尺寸隨控制元件內容,但不能超過父控制元件給的最大尺寸
        • View預設是支援wrap_content的,很合理,一個空View是沒有內容的
        • 一般各種具體View的wrap_content會根據以下因素決定:
          • 背景drawable的寬高,也就是getInstinctWidth和getInstinctHeight
          • padding
          • 控制元件內容,如TextView的內容是text,ImageView的內容是src
          • minWidth和minHeight值(如果計算出的寬高小於min,則取min)
      • UNSPECIFIED
        • 不指定測量模式,View想多大就多大
        • 這個模式也比較合理,但我也不知道能用在哪兒
        • 能用在滾動控制元件?如果控制元件可以橫向滾動,則傳給子View的measureSpec就是不控制你寬高?
        • 可能意思就是:還是wrap_content,但不限制你最大值,所以和AT_MOST有區別,這是我猜的
  • 如何確定測量模式:

    • 首先注意:padding屬性影響測量過程,margin屬性影響佈局過程,也影響ViewGroup的測量過程

3.1 View的預設measure行為

View預設情況下,測量過程如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

//取minWidth和背景drawable的getMinimumWidth的最大值
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

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;         //minWidth屬性或者背景寬度:這個就可以認為是空白View的內容
        break;
    case MeasureSpec.AT_MOST:   //wrap_content,此時specSize是父控制元件給留的最大值
    case MeasureSpec.EXACTLY:   //match_parent或者具體值
        result = specSize;
        break;
    }
    return result;
}

可以看出,唯一沒處理的就是wrap_content的情況

3.2 一個View的測量模板

一般具體帶內容的View,按下面的套路測量
* 思路也很簡單
* EXACTLY:就指定我多大,那我就多大,一般這時子控制元件就是match_parent,或者具體數值
* AT_MOST:specSize是我的最大值,我本身也帶個內容寬高,二者比較,取小的就是了,我儘量給父控制元件省地方
* 除非父控制元件放不下我了,那我就得按父控制元件尺寸來
* 至於calculateContentWidth和calculateContentHeight,可能會被padding,背景,內容等因素影響

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
            mearureWidth(widthMeasureSpec),
            mearureHeight(heightMeasureSpec));
}

private int mearureWidth(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentWidth();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

private int mearureHeight(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentHeight();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

///你自己測量寬度:寬度wrap_content時,會走這
private int calculateContentWidth(){
    return 200;
}

///你自己測量高度:寬度wrap_content時,會走這
private int calculateContentHeight(){
    return 200;
}
  • 自己處理wrap_content時應注意的問題

    • 內容如果是圖片,文字等可測量的,則內容本身才有個尺寸,其他的如你自己畫的一個圈,可能就需要你給個固定值或者自定義屬性什麼的了
    • padding:內容寬度 + 左右padding就是測量寬度
    • max和min相關屬性:也會影響你最終的測量值
  • padding影響還挺大

    • 你draw的時候,寬高是已知的,但是在這裡padding也是要考慮的

3.3 ViewGroup的測量模板

其實ViewGroup的match_parent和固定值也好說,就是EXACTLY,主要還是wrap_content的問題

ViewGroup的onMeasure直接繼承自View,所以沒有實現對子控制元件的處理,
但是測量子控制元件的方法已經提供了,下面給出一個ViewGroup測量的模板

在給出模板之前,需要說明的是,關於這一節的模板和下一節的原理,不要太糾結,
一般情況下,你通過RelativeLayout或者FrameLayout的margin就可以實現任何形式的佈局,
測量過程本身是很複雜的,如果不夠複雜,可能你考慮的情況不夠,參考LinearLayout的測量過程的程式碼,你就能知道為什麼自己一般不要過多的干擾佈局過程了

ViewGroup處理子控制元件的measure先不說
主要是ViewGroup在自己wrap_content時,寬高是隨著子控制元件來的,並且和具體的佈局方式還有關係,所以測量過程中計算ViewGroup自己本身寬高時,可能需要把佈局演算法先過一遍

模板1:不考慮子控制元件margin,也不考慮ViewGroup本身的wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    final int count = getChildCount();
    if (count > 0) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }
}


模板2:考慮margin,和考慮wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final int count = getChildCount();
    // 臨時ViewGroup大小值
    int viewGroupWidth = 0;
    int viewGroupHeight = 0;
    if (count > 0) {
        // 遍歷childView
        for (int i = 0; i < count; i++) {
            // childView
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //測量childView包含外邊距
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // 計算父容器的期望值,下面程式碼我們注掉,因為這裡的程式碼根據具體佈局來
            //viewGroupWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //viewGroupHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        }


        //根據子控制元件計算ViewGroup寬高,取決於具體佈局
        viewGroupWidth = calculateParentWidthBasedOnChilren();
        viewGroupHeight = calculateParentHeightBasedOnChilren();

        // ViewGroup內邊距
        viewGroupWidth += getPaddingLeft() + getPaddingRight();
        viewGroupHeight += getPaddingTop() + getPaddingBottom();

        //和建議最小值進行比較
        viewGroupWidth = Math.max(viewGroupWidth, getSuggestedMinimumWidth());
        viewGroupHeight = Math.max(viewGroupHeight, getSuggestedMinimumHeight());
    }
    setMeasuredDimension(resolveSize(viewGroupWidth, widthMeasureSpec), resolveSize(viewGroupHeight, heightMeasureSpec));
}


///你自己來實現,根據具體佈局和子控制元件們的資訊,算出ViewGroup的wrap_content寬度
private int calculateParentWidthBasedOnChilren(){
    int viewGroupWidth = 0;
    final int count = getChildCount();
    for (int i = 0; i < count; i++){
        View child = getChildAt(i);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //lp.leftMargin
        //lp.rightMargin
        //child.getMeausredWidth()
        ...看你怎麼算了
    }

    return viewGroupWidth;
}


///你自己來實現,根據具體佈局和子控制元件們的資訊,算出ViewGroup的wrap_content高度
private int calculateParentHeightBasedOnChilren(){
    int viewGroupHeight = 0;
    final int count = getChildCount();
    for (int i = 0; i < count; i++){
        View child = getChildAt(i);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //lp.topMargin
        //lp.bottomMargin
        //child.getMeausredHeight()
        ...看你怎麼算了
    }

    return viewGroupHeight;
}



以下程式碼是ViewGroup本身提供的

///遍歷測量所有子控制元件
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];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            //原始碼中是measureChild,注意還有個measureChildWithMargins
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}


public static int resolveSize(int size, int measureSpec) {
    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}


3.4 測量過程相關原始碼和流程簡陋分析

參考開發藝術

再看ViewGroup的測量過程

  • 思路:

    • match_parent和具體值好說
    • 如果ViewGroup本身是wrap_content,那就需要根據所有子View來確定自己的寬高
    • 如果一個豎直方向的LinearLayout高度是wrap_content,讓你測量,你怎麼測量?
      • 對於每一個View,取其高度和margin
        • 如果子View是wrap_content或具體數值,還好說
        • 如果子View是match_parent,而咱這個LinearLayout又是個wrap_content
          • 那就把子View當wrap_content來處理
  • 注意:

    • ViewGroup提供了measureChildren, measureChildWithMargins, getChildMeasureSpec方法
    • 但沒有提供具體的onMeasure實現,因為也沒法提供
    • 再注意LinearLayout的vertical模式下,測量過程需要遍歷子控制元件,see how tall everyone is, also remember max width
對於頂級View,即DecoreView,measure過程和普通View有點不同

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;

    if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
            "Measuring " + host + " in display " + desiredWindowWidth
            + "x" + desiredWindowHeight + "...");

    boolean goodMeasure = false;
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        // On large screens, we don't want to allow dialogs to just
        // stretch to fill the entire width of the screen to display
        // one line of text.  First try doing the layout at a smaller
        // size to see if it will fit.
        final DisplayMetrics packageMetrics = res.getDisplayMetrics();
        res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
        int baseSize = 0;
        if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
            baseSize = (int)mTmpValue.getDimension(packageMetrics);
        }
        if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
        if (baseSize != 0 && desiredWindowWidth > baseSize) {
            childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                    + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                goodMeasure = true;
            } else {
                // Didn't fit in that size... try expanding a bit.
                baseSize = (baseSize+desiredWindowWidth)/2;
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
                        + baseSize);
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    if (DEBUG_DIALOG) Log.v(TAG, "Good!");
                    goodMeasure = true;
                }
            }
        }
    }

    if (!goodMeasure) {
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }

    if (DBG) {
        System.out.println("======================================");
        System.out.println("performTraversals -- after measure");
        host.debug();
    }

    return windowSizeMayChange;
}

看這段
if (!goodMeasure) {
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
        windowSizeMayChange = true;
    }
}

desire就是螢幕寬高

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

上面產生的就是DecoreView的measureSpec

注意一個問題,這個也曾經出現在阿里面試題裡:
父是AT_MOST時,高度其實是根據子View來,
但如果此時子是match_parent,所以子也只能是AT_MOST了
Child wants to be our size, but our size is not fixed. Constrain child to not be bigger than us.

3.5 直接從RelativeLayout或者FrameLayout寫自己的佈局

實戰:BlockLayout

現在需要一個BlockLayout,其子控制元件可以是任何控制元件,但不論其寬度指定成什麼,
每一行都必須只能放3個子控制元件,而高度不論指定成什麼,最後顯示出來的都是個正方形
並且,每一行的中間那個控制元件,必須距左右各10dp的margin
行與行之間,也是10dp的margin

這估計是個最簡單的Layout了

CubeSdk裡的BlockLayout是繼承RelativeLayout,實現比較簡單比較巧妙,
利用了Relativelayout子控制元件的margin來控制,規避了自己measuue和layout,
我們應該學習

package in.srain.cube.views.block;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;

public class BlockListView extends RelativeLayout {

    public interface OnItemClickListener {
        void onItemClick(View v, int position);
    }

    private static final int INDEX_TAG = 0x04 << 24;

    private BlockListAdapter<?> mBlockListAdapter;

    private LayoutInflater mLayoutInflater;

    private OnItemClickListener mOnItemClickListener;

    public BlockListView(Context context) {
        this(context, null, 0);
    }

    public BlockListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BlockListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mLayoutInflater = LayoutInflater.from(context);
    }

    public void setAdapter(BlockListAdapter<?> adapter) {
        if (adapter == null) {
            throw new IllegalArgumentException("adapter should not be null");
        }
        mBlockListAdapter = adapter;
        adapter.registerView(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (null != mBlockListAdapter) {
            mBlockListAdapter.registerView(null);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (null != mBlockListAdapter) {
            mBlockListAdapter.registerView(this);
        }
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
    }

    OnClickListener mOnClickListener = new OnClickListener() {

        @Override
        public void onClick(View v) {
            int index = (Integer) v.getTag(INDEX_TAG);
            if (null != mOnItemClickListener) {
                mOnItemClickListener.onItemClick(v, index);
            }
        }
    };

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    public void onDataListChange() {

        removeAllViews();

        int len = mBlockListAdapter.getCount();
        int w = mBlockListAdapter.getBlockWidth();
        int h = mBlockListAdapter.getBlockHeight();
        int columnNum = mBlockListAdapter.getCloumnNum();

        int horizontalSpacing = mBlockListAdapter.getHorizontalSpacing();
        int verticalSpacing = mBlockListAdapter.getVerticalSpacing();

        boolean blockDescendant = getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS;

        for (int i = 0; i < len; i++) {

            RelativeLayout.LayoutParams lyp = new RelativeLayout.LayoutParams(w, h);
            int row = i / columnNum;
            int clo = i % columnNum;
            int left = 0;
            int top = 0;

            if (clo > 0) {
                left = (horizontalSpacing + w) * clo;
            }
            if (row > 0) {
                top = (verticalSpacing + h) * row;
            }
            lyp.setMargins(left, top, 0, 0);
            View view = mBlockListAdapter.getView(mLayoutInflater, i);
            if (!blockDescendant) {
                view.setOnClickListener(mOnClickListener);
            }
            view.setTag(INDEX_TAG, i);
            addView(view, lyp);
        }
        requestLayout();
    }
}

4 自定義控制元件的套路

  • 套路:

    • 繼承View,通過onDraw實現效果,需要考慮支援wrap_content和padding
      • 預設是不支援的wrap_content的(設為wrap,實際還是使用match_parent效果)
    • 繼承ViewGroup,需要合理處理measure和layout過程,得考慮padding和margin,否則這倆屬性就失效了,同時處理子元素的測量和佈局過程
      • 這種方法要想得到一個規範的Layout是很複雜的,看LinearLayout原始碼就知道
    • 對現有控制元件進行擴充套件,比較簡單,不需要考慮wrap_cotent和padding
    • 建立複合控制元件,比較簡單,也是最常見的
  • 幾個重要的回撥和注意點

    • onFinishInflate() 從xml載入元件完成
    • onSizeChanged() 元件大小改變
    • onAttachedToWindow
    • onDetachedFromWindow 動畫,handler,執行緒之類的,應該在這裡停止
    • View不可見時,也需要停止執行緒和動畫,否則可能造成記憶體洩漏
    • 滑動衝突需要考慮
    • onMeasure
    • onLayout
    • onTouchEvent()
  • 其他:

    • 知道怎麼自定義attr
    • 自定義Drawable也是個路子

5 自定義控制元件入門

在這裡先給出入門的例子,為後面幾節做準備

5.1 CircleView:版本1

效果就是畫個圓,不考慮wrap_content和padding

public class CircleView  extends View{
    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    private void init(){
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(width/2, height/2, radius, mPaint);
    }
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    >

    <org.ayo.ui.sample.view_learn.CircleView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000" />

</FrameLayout>
  • 效果:
    • 預設支援了margin,因為margin由父控制元件處理了
    • 但不支援padding,因為沒有在onDraw裡考慮padding
    • 也不支援wrap_content,而是當做match_parent處理
      • 要解決這個問題,這個例子裡,需要指定個預設寬高,例如都是200px

5.2 CircleView:版本2

新增padding支援

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    final int paddingLeft = getPaddingLeft();
    final int paddingRight = getPaddingRight();
    final int paddingTop = getPaddingTop();
    final int paddingBottom = getPaddingBottom();
    int width = getWidth() - paddingLeft - paddingRight;
    int height = getHeight() - paddingTop - paddingBottom;
    int radius = Math.min(width, height) / 2;
    canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, radius, mPaint);
}
<org.ayo.ui.sample.view_learn.CircleView
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:layout_margin="20dp"
    android:padding="20dp"
    android:background="#000000" />

5.3 CircleView:版本3

新增wrap_content支援

//===========================================
//為了讓控制元件支援wrap_content時,內容尺寸取200px,需要我們重寫measure過程
//===========================================
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
            mearureWidth(widthMeasureSpec),
            mearureHeight(heightMeasureSpec));
}

private int mearureWidth(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentWidth();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

private int mearureHeight(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentHeight();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

private int calculateContentWidth(){
    return 200;
}

private int calculateContentHeight(){
    return 200;
}
<org.ayo.ui.sample.view_learn.CircleView2
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="20dp"
    android:padding="20dp"
    android:background="#000000" />

其實上面這段java程式碼,可以簡化一下,參考開發藝術


///不考慮contentSize和specSize的大小關係,不考慮minWidth和minHeight
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(200, 200);
    }else if(widthSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(200, heightSize);
    }else if(heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(widthSize, 200);
    }
}