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來佈局。