自定義控件—自定義ViewGroup
ViewGroup的基本概念
ViewGroup繪製流程分爲三部分:測量、佈局、繪製。分別對應 onMeasure 、onLayout 、 onDraw 函數。
這三個函數的作用分別如下;
- onMeasure : 測量控件大小,爲正式佈局提供建議。至於用不用還要看onLayout函數
- onLayout : 對所有子控件進行佈局
- onDraw : 根據佈局的位置繪圖。
onMeasure 函數與 MeasureSpec
onMeasure 函數的聲明如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
其中 widthMeasureSpec 和 heightMeasureSpec 是由mode + size組成,其中前兩位代表模式,後30位代表數值。
模式的分類
MeasureSpec.UNSPECIFIED (未指定 ):父元素不對子元素施加任何束縛,子元素可以得到任意想要的大小
MeasureSpec.AT_MOST (wrap_content):根據子控件確定自身大小
MeasureSpec.EXACTLY (精確值:100dp、match_parent):子元素至多達到指定的大小
他們的二進制值分別是:
MeasureSpec.UNSPECIFIED:32個0
MeasureSpec.EXACTLY:01+30個0
MeasureSpec.AT_MOST :10+30個0
前面兩位代表模式,他們分別代表十進制的0、1、2
模式提取
widthMeasureSpec、heightMeasureSpec是由模式和大小組成,前兩位代表模式。下面看看如何提取模式和大小;
首先我們想到的是和Mask運算,去掉不需要的部分。
上面是我們自己實現的,實際Android已經爲我們實現了提取模式和大小的實現。
例如:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
另外,模式的取值爲:
- MeasureSpec.UNSPECIFIED
- MeasureSpec.AT_MOST
- MeasureSpec.EXACTLY
模式的用處及對應關係
實際上在xml中定義的寬高,就是在這些模式中的一個。下面爲模式的對應關係
wrap_content -> MeasureSpec.AT_MOST
match_parent 和 具體的數值 -> MeasureSpec.EXACTLY
當用戶指定值爲:wrap_content 的時候我們需要計算控件的值,否則應該尊重,選用用戶的值。
getMeasuredxxx和getxxx(xxx代表寬高)
兩者主要有如下區別:
- getMeasuredHeight():在measure過程結束後才能拿到數值。而getHight()需要在layout過程後才能拿到數值
- getMeasuredHeight():數值通過setMeasuredDimension()指定的。而getHeight是通過layout()指定的
讓子控件支持margin值
如果想要支持子控件的layout_margin屬性,則自定義的ViewGroup必須重寫generateLayoutParams()函數
因爲默認的layoutParams值只會獲取寬高,不會設置margin屬性值。所以,我們需要一個MarginLayoutParams類來讓子控件獲取margin值。詳細請看MarginLayoutParams源碼。
添加如下代碼,讓你的ViewGroup支持margin
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
計算ViewGroup大小示例:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
height += childHeight;
width = Math.max(childWidth, width);
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
擺放子控件位置:onLayout
在onLayout中設置子控件的layout屬性,進而控制子控件位置擺放。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}
自定義LinearLayout實現完整代碼:
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
height += childHeight;
width = Math.max(childWidth, width);
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}
}
自定義RadioGroup(實現指定列)
本示例實現指定列的佈局測量和擺放方法。
實現思路:
自定義佈局,主要實現的方法有:測量和佈局。
測量高度:測量每行的最大高度值進行累加。
測量寬度:本示例爲實現
佈局行:每隔指定列 top值進行累加。
實現代碼:
public class YfTypeLayout extends RadioGroup {
private int rowNum = 3;
public YfTypeLayout(Context context) {
super(context);
}
public YfTypeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
int rowMaxHeight = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
rowMaxHeight = Math.max(rowMaxHeight, childHeight);
if (i % rowNum == rowNum - 1 || i == count - 1) {
height += rowMaxHeight;
rowMaxHeight = 0;
}
width = Math.max(childWidth, width);
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int left;
int parentWidth = getMeasuredWidth();
int count = getChildCount();
int rowMaxHeight = 0;
int region = parentWidth / 3;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
if (i % rowNum == 0) {
left = region / 2 - childWidth / 2;
top += rowMaxHeight;
} else {
left = region * (i % rowNum) + region / 2 - childWidth / 2;
}
rowMaxHeight = rowMaxHeight > childHeight ? rowMaxHeight : childHeight;
child.layout(left, top, left + childWidth, top + childHeight);
}
}
}