一、ViewGroup基础
ViewGroup是Android中所有布局的基类,它充当了容器的角色,可以容纳其他View。比如LinearLayout、RelativeLayout、FrameLayout、GridLayout等都是ViewGroup的子类。
一个ViewGroup可以有多个子View,这些子View可以以不同的方式排列,ViewGroup需要根据它的布局方式来决定每个子View的位置和大小。ViewGroup的布局方式通过它所处的布局文件的xml来定义,或者通过代码来设置。
二、自定义ViewGroup
1. 继承ViewGroup并重写onMeasure方法
public class CustomViewGroup extends ViewGroup{
public CustomViewGroup(Context context){
super(context);
}
public CustomViewGroup(Context context, AttributeSet attrs){
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 自定义测量代码
// ...
// 设置最终测量宽高
setMeasuredDimension(widthSize, heightSize);
}
}
在自定义ViewGroup时,我们需要实现onMeasure()方法来进行布局的测量。onMeasure()方法的运行原理是:系统会先调用setMeasuredDimension()方法来确定ViewGroup的测量宽高,然后再调用每个子View的measure()方法来测量它们的宽高,最后根据子View的测量结果来确定它们的位置和大小。
在onMeasure()方法中,我们需要参考父容器所传递进来的 widthMeasureSpec 和 heightMeasureSpec 参数,来计算出我们ViewGroup的宽高,然后再通过 setMeasuredDimension()方法来设置它们。widthMeasureSpec 和 heightMeasureSpec 的具体含义可以参考Android官方文档。
2. 重写onLayout方法
public class CustomViewGroup extends ViewGroup{
public CustomViewGroup(Context context){
super(context);
}
public CustomViewGroup(Context context, AttributeSet attrs){
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 自定义测量代码
// ...
// 设置最终测量宽高
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 自定义布局代码
// ...
}
}
ViewGroup的布局过程除了测量阶段之外,还有一个布局阶段。在测量阶段,ViewGroup会根据子View自身的大小来确定它们的位置和大小,但在布局阶段,ViewGroup则需要根据它的布局方式来决定子View的位置和大小。
我们需要实现onLayout()方法来进行布局的计算。onLayout()方法会在子View的测量阶段之后执行,这时我们已经知道了每个子View的大小和位置。我们只需要按照布局方式将它们放置到合适的位置即可。
三、实现一个简单的自定义ViewGroup
1. 布局方式
假设我们要实现一个自定义ViewGroup,它的布局方式为两行两列,每个子View大小相等。我们首先需要确定每个子View的位置和大小。我们将ViewGroup分成4份,然后将每个子View放置在相应的位置。
2. 代码实现
public class CustomViewGroup extends ViewGroup {
private int mChildWidth, mChildHeight;
public CustomViewGroup(Context context) {
this(context, null);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 计算每个子View的大小
int screenWidth = getResources().getDisplayMetrics().widthPixels;
mChildWidth = (int) (screenWidth * 0.4f);
mChildHeight = (int) (mChildWidth * 1.2f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 计算ViewGroup的总宽度
int width = MeasureSpec.getSize(widthMeasureSpec);
int totalPadding = getPaddingLeft() + getPaddingRight();
int availableWidth = width - totalPadding;
int columnCount = 2;
int childWidth = (availableWidth - (columnCount - 1) * mChildWidth) / columnCount;
int childHeight = (int) (childWidth * 1.2f);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
// 测量每个子View的大小
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// 计算ViewGroup的总高度
int rowCount = childCount % 2 == 0 ? childCount / 2 : childCount / 2 + 1;
int totalHeight = rowCount * childHeight + (rowCount - 1) * mChildHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, totalHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int width = r - l;
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int childTop = paddingTop;
// 遍历每个子View,设置它们的位置
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
int childLeft = paddingLeft + (mChildWidth + width - paddingLeft - getPaddingRight() - 2 * mChildWidth) * (i % 2);
if (i % 2 == 0) {
childTop += i == 0 ? 0 : (mChildHeight + childView.getMeasuredHeight());
}
childView.layout(childLeft, childTop, childLeft + mChildWidth, childTop + childView.getMeasuredHeight());
}
}
}
3. 使用
我们可以在xml布局文件中使用我们的自定义ViewGroup,如下所示:
<com.example.CustomViewGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<Button
android:text="Button 1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:text="Button 2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:text="Button 3"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:text="Button 4"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.example.CustomViewGroup>
注意到我们把ViewGroup的padding设置成了16dp,并为每个子View设置了match_parent作为它们在ViewGroup中的宽度,这样才能保证每个子View的大小一致。
四、结语
自定义ViewGroup是Android开发中很重要的一个方面。只要你理解了ViewGroup的测量和布局原理,就可以通过自定义ViewGroup达到各种复杂的布局效果。同时,ViewGroup也是自定义View中最常用、最基础的一部分。学会了自定义ViewGroup,你也就能够更好的理解自定义View的其他部分。