继承ViewGroup,例子:FlowLayout
FlowLayout(流式布局),标签控件(如TextView)根据FlowLayout的宽度,自动地往右添加,如果当前行剩余空间不足,则自动添加到下一行。
关键点:
1.需要能够识别margin属性,布局参数类使用MarginLayoutParams;
2.在onMeasure方法中对自己及所有的子元素进行测量,要考虑自己的padding及子元素的margin;
3.在onLayout方法中对所有的子元素进行布局,要考虑自己的padding及子元素的margin。
//自定义布局控件FlowLayout,继承ViewGroup
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs); //使用MarginLayoutParams
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int parentWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int parentHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int parentHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
Logger.d("父容器, 宽度 MeasureSpec: " + MeasureSpec.toString(widthMeasureSpec));
Logger.d("父容器, 高度 MeasureSpec: " + MeasureSpec.toString(heightMeasureSpec));
int curLineUsedWidth = getPaddingLeft() + getPaddingRight(); //当前行已使用的宽度,要考虑父容器padding
int linesUsedWidthMax = curLineUsedWidth; //所有行中已使用的宽度的最大值
int curLineUsedHeightMax = 0; //当前行已使用的高度的最大值
int linesUsedHeightTotal = getPaddingTop() + getPaddingBottom(); //所有行已使用的高度的总和,要考虑父容器padding
Logger.d("当前行已使用的宽度: " + curLineUsedWidth);
Logger.d("所有行中已使用的宽度的最大值: " + linesUsedWidthMax);
Logger.d("当前行已使用的高度的最大值: " + curLineUsedHeightMax);
Logger.d("所有行已使用的高度的总和: " + linesUsedHeightTotal);
int childCounts = getChildCount();
for(int i = 0; i < childCounts; i++) {
Logger.d("子元素: " + i);
View child = getChildAt(i);
if(child.getVisibility() == View.GONE) //如果子元素不可见则不进行测量
continue;
int widthUsed = curLineUsedWidth - getPaddingLeft() - getPaddingRight(); //已使用的宽度,不包含父容器padding
int heightUsed = linesUsedHeightTotal - getPaddingTop() - getPaddingBottom(); //已使用的高度,不包含父容器padding
Logger.d("已使用的宽度,不包含父容器padding: " + widthUsed + ", 已使用的高度,不包含父容器padding: " + heightUsed);
//如果使用measureChildWithMargins方法,当ViewGroup宽度的剩余空间小于子元素在无限制条件下设置wrap_content时的大小时(当然还需要加上leftMargin跟rightMargin),子元素的最终测量尺寸(当然还需要加上leftMargin跟rightMargin)会被限制为ViewGroup宽度的剩余空间。
//因为子元素的SpecMode为AT_MOST的情况下,子元素的SpecSize会被设置为ViewGroup宽度的剩余空间,而子元素最终的测量尺寸是不能够超过SpecSize的。
//measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); //调用系统方法测量子元素,要考虑父容器padding及子元素margin
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
Logger.d("子元素: " + i + ", 测量尺寸 width: " + child.getMeasuredWidth() + ", height: " + child.getMeasuredHeight());
int childWidthWithMargin = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; //子元素的宽度加上margin
int childHeightWithMargin = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //子元素的高度加上margin
if(curLineUsedWidth + childWidthWithMargin > parentWidthSpecSize) {
Logger.d("宽度不够,换行");
curLineUsedWidth = getPaddingLeft() + getPaddingRight() + childWidthWithMargin;
Logger.d("子元素: " + i + ",当前行已使用的宽度: " + curLineUsedWidth);
linesUsedHeightTotal += curLineUsedHeightMax;
curLineUsedHeightMax = childHeightWithMargin;
} else {
Logger.d("宽度足够,不用换行");
curLineUsedWidth += childWidthWithMargin;
Logger.d("子元素: " + i + ",当前行已使用的宽度: " + curLineUsedWidth);
linesUsedWidthMax = Math.max(linesUsedWidthMax, curLineUsedWidth);
curLineUsedHeightMax = Math.max(curLineUsedHeightMax, childHeightWithMargin);
}
if(i == childCounts - 1) { //如果是最后一个子元素
Logger.d("最后一个子元素");
linesUsedHeightTotal += curLineUsedHeightMax;
}
}
Logger.d("linesUsedWidthMax: " + linesUsedWidthMax);
Logger.d("linesUsedHeightTotal: " + linesUsedHeightTotal);
setMeasuredDimension((parentWidthSpecMode == MeasureSpec.EXACTLY) ? parentWidthSpecSize
: linesUsedWidthMax, (parentHeightSpecMode == MeasureSpec.EXACTLY) ? parentHeightSpecSize
: linesUsedHeightTotal);
Logger.d("父容器, 测量尺寸 width: " + getMeasuredWidth() + ", height: " + getMeasuredHeight());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentWidth = getMeasuredWidth();
int parentHeight = getMeasuredHeight();
int childCounts = getChildCount();
int curLineUsedWidth = getPaddingLeft() + getPaddingRight(); //当前行已使用的宽度,要考虑父容器padding
int curLineUsedHeightMax = 0; //当前行已使用的高度的最大值
int linesUsedHeightTotal = getPaddingTop() + getPaddingBottom(); //所有行已使用的高度的总和,要考虑父容器padding
Logger.d("当前行已使用的宽度: " + curLineUsedWidth);
Logger.d("当前行已使用的高度的最大值: " + curLineUsedHeightMax);
Logger.d("所有行已使用的高度的总和: " + linesUsedHeightTotal);
for(int i = 0; i < childCounts; i++) {
Logger.d("子元素: " + i);
View child = getChildAt(i);
if(child.getVisibility() == View.GONE)
continue;
MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
int childWidthWithMargin = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; //子元素的宽度加上margin
int childHeightWithMargin = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //子元素的高度加上margin
int left, top, right, bottom;
if(curLineUsedWidth + childWidthWithMargin > parentWidth) {
Logger.d("宽度不够,换行");
left = getPaddingLeft() + lp.leftMargin;
top = linesUsedHeightTotal + curLineUsedHeightMax - getPaddingBottom() + lp.topMargin;
right = left + child.getMeasuredWidth();
bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
curLineUsedWidth = getPaddingLeft() + getPaddingRight() + childWidthWithMargin;
Logger.d("子元素: " + i + ",当前行已使用的宽度: " + curLineUsedWidth);
linesUsedHeightTotal += curLineUsedHeightMax;
curLineUsedHeightMax = childHeightWithMargin;
} else {
Logger.d("宽度足够,不用换行");
left = curLineUsedWidth - getPaddingRight() + lp.leftMargin;
top = linesUsedHeightTotal - getPaddingBottom() + lp.topMargin;
right = left + child.getMeasuredWidth();
bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
curLineUsedWidth += childWidthWithMargin;
Logger.d("子元素: " + i + ",当前行已使用的宽度: " + curLineUsedWidth);
curLineUsedHeightMax = Math.max(curLineUsedHeightMax, childHeightWithMargin);
}
Logger.d("子元素: " + i + ",left: " + left + ", top: " + top + ", right: " + right + ", bottom: " + bottom);
}
}
}
//布局
<com.tomorrow.androidtest3.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20px"
android:background="#808080">
<TextView
style="@style/flowLayoutStyle"
android:text="搭建知识框架" />
<TextView
style="@style/flowLayoutStyle"
android:text="Android" />
<TextView
style="@style/flowLayoutStyle"
android:text="Java" />
<TextView
style="@style/flowLayoutStyle"
android:text="设计模式" />
<TextView
style="@style/flowLayoutStyle"
android:text="架构模式" />
<TextView
style="@style/flowLayoutStyle"
android:text="App后台" />
<TextView
style="@style/flowLayoutStyle"
android:text="性能优化" />
<TextView
style="@style/flowLayoutStyle"
android:text="打通任督二脉" />
</com.tomorrow.androidtest3.FlowLayout>
//风格,styles.xml
<style name="flowLayoutStyle">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_margin">20px</item>
<item name="android:background">@drawable/background</item>
<item name="android:textColor">#ffffff</item>
</style>
//背景,background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="@color/colorPrimary" />
<corners
android:radius="30dp" />
<padding
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="2dp" />
</shape>
//MainActivity
setContentView(R.layout.activity_main);