1. 程式人生 > >android自定義View之margin和padding的處理

android自定義View之margin和padding的處理

說起margin和padding,想必大家都不陌生。margin就是指子控制元件與外部控制元件的距離,俗稱外邊距;padding就是指控制元件內容與控制元件邊界的距離,俗稱內邊距。當我們使用系統自帶的控制元件時,margin和padding都是生效的,因為系統已經幫我們處理好了,但是當我們自定義View時,如果不單獨處理,那麼系統是不會識別,不生效的,下面我們來看下今天的例子。

padding的處理

自定義View中的處理

我們先定義一個自定義View,具體程式碼如下

/**
 * Created by bellnett on 17/2/9.
 */
public class CustomView
extends View {
private Paint mPaint; private int color; private int width; private int height; public CustomView(Context context, AttributeSet attrs) { super(context, attrs); init(context,attrs); } private void init(Context context, AttributeSet attrs) { TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView); color = array.getColor(R.styleable.CustomView_backgroundColor, Color.BLUE); array.recycle(); mPaint = new
Paint(); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(color); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); width = measureWidth(widthMeasureSpec); height = measureHeight(heightMeasureSpec); setMeasuredDimension(width,height); } @Override
protected void onDraw(Canvas canvas) { super.onDraw(canvas); Rect rect = new Rect(); rect.left = 0; rect.top = 0; rect.right = width; rect.bottom = height; canvas.drawRect(rect,mPaint); } private int measureWidth(int measureSpec) { int result = 200; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = specSize; break; case MeasureSpec.AT_MOST: result = Math.min(result, specSize); break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; } private int measureHeight(int measureSpec) { int result = 200; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = specSize; break; case MeasureSpec.AT_MOST: result = Math.min(result, specSize); break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; } }

功能非常簡單,就是繪製一個背景,其中單獨處理了wrap_content的情況,對於這個wrap_content的單獨處理我就不多說了,在上一篇《android View的工作原理》中已經進行了詳細說明。
下面看一下佈局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.bellnet.customview1.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:backgroundColor="@android:color/holo_blue_bright"/>

</RelativeLayout>

顯示結果也如我們預期的那樣
這裡寫圖片描述
下面我們給它加一下padding,在佈局檔案中加入以下程式碼

android:padding="10dp"

顯示結果
這裡寫圖片描述
沒有任何效果,那麼如何處理呢,很簡單,只要在過載的onDraw(Canvas canvas)方法中修改為以下程式碼即可

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paddingLeft = getPaddingLeft();
        paddingTop = getPaddingTop();
        paddingRight = getPaddingRight();
        paddingBottom = getPaddingBottom();
        Rect rect = new Rect();
        rect.left = 0 + paddingLeft;
        rect.top = 0 + paddingTop;
        rect.right = width - paddingRight;
        rect.bottom = height - paddingBottom;
        canvas.drawRect(rect,mPaint);
    }

這樣就加上了padding的處理,我們看下效果
這裡寫圖片描述
沒任何問題,padding屬性已經生效。

自定義ViewGroup中的處理

自定義ViewGroup處理padding和View有稍許不同,我們之前的文章也說過了,自定義ViewGroup是在dispatchView中遍歷子View來讓其自行繪製,自己需要繪製的情況比較少見,再結合padding的定義:padding是控制元件內容與控制元件邊界的距離。假設此時的控制元件內容就是我們上面的自定義View,那麼這個View的大小就是父控制元件的大小 - padding值,也就是父控制元件所能給予子控制元件的最大值,很明顯,這個操作我們可以放在父控制元件的onMeasure方法中。下面我們用程式碼來驗證。

首先是ViewGroup的實現。

/**
 * Created by bellnett on 17/2/9.
 */
public class CustomViewGroup extends ViewGroup {

    private int paddingLeft;
    private int paddingRight;
    private int paddingTop;
    private int paddingBottom;
    private int viewsWidth;//所有子View的寬度之和(在該例子中僅代表寬度最大的那個子View的寬度)
    private int viewsHeight;//所有子View的高度之和
    private int viewGroupWidth = 0;//ViewGroup算上padding之後的寬度
    private int viewGroupHeight = 0;//ViewGroup算上padding之後的高度

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        viewsWidth = 0;
        viewsHeight = 0;
        paddingLeft = getPaddingLeft();
        paddingTop = getPaddingTop();
        paddingRight = getPaddingRight();
        paddingBottom = getPaddingBottom();
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            viewsHeight += childView.getMeasuredHeight();
            viewsWidth = Math.max(viewsWidth, childView.getMeasuredWidth());
        }
        /* 用於處理ViewGroup的wrap_content情況 */
        viewGroupWidth = paddingLeft + viewsWidth + paddingRight;
        viewGroupHeight = paddingTop + viewsHeight + paddingBottom;
        setMeasuredDimension(measureWidth(widthMeasureSpec, viewGroupWidth), measureHeight
                (heightMeasureSpec, viewGroupHeight));
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        if (b) {
            int childCount = getChildCount();
            int mTop = paddingTop;
            for (int j = 0; j < childCount; j++) {
                View childView = getChildAt(j);
                int mLeft = paddingLeft;
                childView.layout(mLeft, mTop, mLeft + childView.getMeasuredWidth(), mTop + childView.getMeasuredHeight());
                mTop += childView.getMeasuredHeight();
            }
        }
    }

    private int measureWidth(int measureSpec, int viewGroupWidth) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = specSize;
                break;
            case MeasureSpec.AT_MOST:
                /* 將剩餘寬度和所有子View + padding的值進行比較,取小的作為ViewGroup的寬度 */
                result = Math.min(viewGroupWidth, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    private int measureHeight(int measureSpec, int viewGroupHeight) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = specSize;
                break;
            case MeasureSpec.AT_MOST:
                /* 將剩餘高度和所有子View + padding的值進行比較,取小的作為ViewGroup的高度 */
                result = Math.min(viewGroupHeight, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }
}

程式碼已經寫得很清楚了,其中要注意的有兩點:
1. 在onMeasure方法中處理ViewGroup的layout_width和layout_height為wrap_content的情況。本例模擬的是類似LinearLayout垂直佈局的情景,因此在寬度方面,選擇子View中寬度最大的那一個再加上padding值作為ViewGroup預設的寬度;在高度方面,算上所有子View的高度再加上padding值作為ViewGroup預設的高度,最後將預設值與剩餘空間進行比較,選擇值最小的作為ViewGroup測量之後的值。
2. 在onLayout中處理padding,只需要在佈置子View的時候加上padding的值即可。

然後是佈局檔案

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.bellnet.customview1.CustomViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_red_light"
        android:padding="10dp">

        <com.bellnet.customview1.CustomView
            android:layout_width="250dp"
            android:layout_height="wrap_content"
            app:backgroundColor="@android:color/holo_blue_bright"/>

        <com.bellnet.customview1.CustomView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:backgroundColor="@android:color/holo_green_dark"/>

    </com.bellnet.customview1.CustomViewGroup>


</RelativeLayout>

其中我將ViewGroup的寬高都設定為wrap_content,大家也可以設定指定的值或者match_parent來自行嘗試。
最後我們看下效果:
這裡寫圖片描述

margin的處理

自定義ViewGroup中的處理

margin的處理方式和自定義ViewGroup中padding的處理方式有點類似,在ViewGroup測量時,算上子View與ViewGroup的margin;在給子View佈局時,算上margin即可。

下面我們直接上程式碼

/**
 * Created by bellnett on 17/2/9.
 */
public class CustomViewGroup extends ViewGroup {

    private int paddingLeft;
    private int paddingRight;
    private int paddingTop;
    private int paddingBottom;
    private int viewsWidth;//所有子View的寬度之和(在該例子中僅代表寬度最大的那個子View的寬度)
    private int viewsHeight;//所有子View的高度之和
    private int viewGroupWidth = 0;//ViewGroup算上padding之後的寬度
    private int viewGroupHeight = 0;//ViewGroup算上padding之後的高度
    private int marginLeft
    private int marginTop;
    private int marginRight;
    private int marginBottom;

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        viewsWidth = 0;
        viewsHeight = 0;
        marginLeft = 0;
        marginTop = 0;
        marginRight = 0;
        marginBottom = 0;
        paddingLeft = getPaddingLeft();
        paddingTop = getPaddingTop();
        paddingRight = getPaddingRight();
        paddingBottom = getPaddingBottom();
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            viewsHeight += childView.getMeasuredHeight();
            viewsWidth = Math.max(viewsWidth, childView.getMeasuredWidth());
            marginLeft = Math.max(0,lp.leftMargin);//在本例中找出最大的左邊距
            marginTop += lp.topMargin;//在本例中求出所有的上邊距之和
            marginRight = Math.max(0,lp.rightMargin);//在本例中找出最大的右邊距
            marginBottom += lp.bottomMargin;//在本例中求出所有的下邊距之和
        }
        /* 用於處理ViewGroup的wrap_content情況 */
        viewGroupWidth = paddingLeft + viewsWidth + paddingRight + marginLeft + marginRight;
        viewGroupHeight = paddingTop + viewsHeight + paddingBottom + marginTop + marginBottom;
        setMeasuredDimension(measureWidth(widthMeasureSpec, viewGroupWidth), measureHeight
                (heightMeasureSpec, viewGroupHeight));
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        if (b) {
            int childCount = getChildCount();
            int mTop = paddingTop;
            for (int j = 0; j < childCount; j++) {
                View childView = getChildAt(j);
                MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams();
                int mLeft = paddingLeft + lp.leftMargin;
                mTop += lp.topMargin;
                childView.layout(mLeft, mTop, mLeft + childView.getMeasuredWidth(), mTop + childView.getMeasuredHeight());
                mTop += (childView.getMeasuredHeight() + lp.bottomMargin);
            }
        }
    }

    private int measureWidth(int measureSpec, int viewGroupWidth) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = specSize;
                break;
            case MeasureSpec.AT_MOST:
                /* 將剩餘寬度和所有子View + padding的值進行比較,取小的作為ViewGroup的寬度 */
                result = Math.min(viewGroupWidth, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    private int measureHeight(int measureSpec, int viewGroupHeight) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = specSize;
                break;
            case MeasureSpec.AT_MOST:
                /* 將剩餘高度和所有子View + padding的值進行比較,取小的作為ViewGroup的高度 */
                result = Math.min(viewGroupHeight, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }
}

以上程式碼需要注意的是:
1. onMeasure方法可能會呼叫多次,因此在onMeasure中累加得做好初始化準備
2. 直接使用MarginLayoutParams類會報錯,因此得在ViewGroup中過載generateLayoutParams方法並且返回一個新的MarginLayoutParams物件,才能獲取到margin值

然後是佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.bellnet.customview1.CustomViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_red_light">

        <com.bellnet.customview1.CustomView
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="15dp"
            android:layout_marginTop="5dp"
            app:backgroundColor="@android:color/holo_blue_bright"/>

        <com.bellnet.customview1.CustomView
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:layout_marginBottom="5dp"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="20dp"
            android:layout_marginTop="5dp"
            app:backgroundColor="@android:color/holo_green_dark"/>

    </com.bellnet.customview1.CustomViewGroup>

</LinearLayout>

最後是效果圖:
這裡寫圖片描述

總結一下:

  • 在自定義View中處理padding,只需要在onDraw()中處理,別忘記處理佈局為wrap_content的情況。
  • 在自定義ViewGroup中處理padding,只需要在onLayout()中,給子View佈局時算上padding的值即可,也別忘記處理佈局為wrap_content的情況。
  • 自定義View無需處理margin,在自定義ViewGroup中處理margin時,需要在onMeasure()中根據margin計算ViewGroup的寬、高,同時在onLayout中佈局子View時也別忘記根據margin來佈局。