前面学习了 View 的绘制流程. 今天结合前面学的来自定义一个简单的标签布局.
看效果, 有点丑, 不过大概也算是弄懂了一些这个布局的基本原理. 下面来分析学习一下.
![](https://img.haomeiwen.com/i7115372/ce62b6920f70974f.png)
老规矩还是先分析:
- 首先在我们自定义的 ViewGroup 中, 要重写 onMeasure 和 onLayout,
- 在 onMeasure 需要针对所有的子 View 进行测量, 在测量子 View 的同时, 还要判断是否需要换行. 如果需要换行, 那么我们自定义 ViewGroup 的高度就需要累加. 我们在测量子 View 的时候, 就把每行的所有 View 保存起来. 还需要把每行的高度存起来. 在 onLayout 布局的时候, 可以直接使用.
- 在 onLayout 布局的时候对在 onMeasure 存放的所有行, 进行遍历, 遍历每一行中的每个子 View 对它进行布局. 需要考虑到, 摆放一个子 View 后, 下次摆放子 View 的 left 就需要向后移动.
- 无论是在 onMeasure 或者 onLayout 中, 都需要考虑到 margin 和 padding 的情况.
一般我们都不会直接继承自 ViewGroup, 在大部分情况下, 都是继承系统已经提供好的, 例如: linearLayout, RelativeLayout 等, 因为他们已经已经写好了一些 onLayout, touch 等等事件. 这里是为了学习, 所以直接继承自了 ViewGroup.
这里就不再分段讲解了, 直接上代码了, 分析的都很清楚.
activity_main 布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.zyq.view_day08.LabelLayout
android:id="@+id/label_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
定义一个简单的Adapter
因本章主要学习的是 ViewGroup 的测量与摆放, 所以就随便弄了一个Adapter.
public abstract class BaseLabelAdapter {
public abstract int getCount();
public abstract View getView(int position, ViewGroup parent);
}
自定义 ViewGroup LabelLayout
public class LabelLayout extends ViewGroup {
private BaseLabelAdapter mAdapter;
//存放所有行中所有View的集合
private List<List<View>> mAllLineViews = new ArrayList<>();
//存放
private List<Integer> mAllLineHeight = new ArrayList<>();
public LabelLayout(Context context) {
this(context, null);
}
public LabelLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LabelLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mAllLineViews.clear();
mAllLineHeight.clear();
//先拿到用户设置的宽高模式和大小.需要对 warp_content 做处理
//获取大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//获取模式
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
//判断模式是不是warp_content
if (modeWidth == MeasureSpec.AT_MOST) {
throw new RuntimeException("Error: 不允许设置 layout_width 为 wrap_content");
}
//获取父容器的宽度和高度, 但是高度需要计算(涉及到子 View 的换行. 换行一次累加一次)
int parentWidth = widthSize + getPaddingLeft() + getPaddingRight();
int parentHeight = getPaddingTop() + getPaddingBottom();
//当前行的宽度和高度
int currentLineWidth = 0;
int currentLineHeight = 0;
//获取子 View 的个数
int childCount = getChildCount();
//存放当前行的所有 View
ArrayList<View> currentLineViews = new ArrayList<>();
//测量所有的子 View
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//测量子 View, 传入子 View以及父容器的宽高模式.
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//测量完子 View 后, 就可以获取子 View 的宽度和高度 + margin 与 padding.
ViewGroup.MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + childView.getPaddingLeft() + childView.getPaddingRight() + params.leftMargin + params.rightMargin;
int childHeight = childView.getMeasuredHeight() + childView.getPaddingTop() + childView.getPaddingBottom() + params.topMargin + params.bottomMargin;
//这里需要考虑换行的问题, 如果一行可以满足, 那么不换行, 父容器的高度不累加, 这里使用一个变量来控制当前行宽.
//如果当前行宽 + 当前 View 的宽度 > 父容器的宽度, 那么就是需要换行了.也就是父容器的高度累加
//如果 < 父容器的宽度, 那么当前行宽就累加 当前 View 的宽度.
if (currentLineWidth + childWidth > parentWidth) {
//处理换行. 父容器高度累加
parentHeight += currentLineHeight;
//需要换行的情况下, 添加到集合
mAllLineViews.add(currentLineViews);
mAllLineHeight.add(currentLineHeight);
//换行后, 需要把当前View添加到下一行. 需要对当前行宽重新赋值
currentLineWidth = childWidth;
currentLineHeight = childHeight;
currentLineViews = new ArrayList<>();
currentLineViews.add(childView);
} else {
currentLineHeight = Math.max(childHeight, currentLineHeight);
currentLineWidth += childWidth;
//不换行就把当前 View 添加到当前行的集合中
currentLineViews.add(childView);
}
if (i == childCount - 1) {
parentHeight += currentLineHeight;
mAllLineHeight.add(currentLineHeight);
mAllLineViews.add(currentLineViews);
}
}
// 如果高度是wrap_content或者在ScrollView里面,就设置高度为计算的height值
setMeasuredDimension(parentWidth, modeHeight == MeasureSpec.AT_MOST || modeHeight == MeasureSpec.UNSPECIFIED ? parentHeight : heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//子View的左上右下,四个坐标
int left = getPaddingLeft();
int top = getPaddingTop();
//行数
int num = mAllLineViews.size();
for (int i = 0; i < num; i++) {
//获得第 i 行的所有 View
List<View> lineViews = mAllLineViews.get(i);
//获得第 i 行的高度
int lineHeigh = mAllLineHeight.get(i);
//遍历摆放第i行的所有View
for (View view : lineViews) {
if (view.getVisibility() == View.GONE) {
continue;
}
ViewGroup.MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
int childLeft = left + params.leftMargin;
int childRight = childLeft + view.getMeasuredWidth();
int childTop = top + params.topMargin;
int childBottom = childTop + view.getMeasuredHeight();
view.layout(childLeft, childTop, childRight, childBottom);
//每摆放一次, left坐标都要向右移动一个 View 的宽度.
left += view.getMeasuredWidth() + params.leftMargin + params.rightMargin;
}
//循环一行之后, 高度累加
top += lineHeigh;
//循环一行之后, left 恢复
left = getPaddingLeft();
}
}
public void setAdapter(BaseLabelAdapter adapter) {
if (adapter == null) {
throw new NullPointerException("Error: Adapter 为 null");
}
//先清空所有的子 View.
removeAllViews();
this.mAdapter = null;
this.mAdapter = adapter;
int childCount = this.mAdapter.getCount();
for (int i = 0; i < childCount; i++) {
//通过位置获取View
View childView = this.mAdapter.getView(i, this);
addView(childView);
}
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
LabelLayout mLayout;
List<String> mItems;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLayout = findViewById(R.id.label_layout);
mItems = new ArrayList<>();
mItems.add("1313131313131313131313131313131313131313131313131313131313131313131313131313131313131313");
mItems.add("1");
mItems.add("222222222222222222222");
mItems.add("333");
mItems.add("444444444");
mItems.add("5");
mItems.add("666666666666666666666666");
mItems.add("7777777777777");
mItems.add("8888");
mItems.add("9999999999");
mItems.add("1010101010101010");
mItems.add("111111111111");
mItems.add("12");
mItems.add("1313131313131313131313131313131313131313131313131313131313131313131313131313131313131313");
mItems.add("141414");
mItems.add("1313131313131313131313131313131313131313131313131313131313131313131313131313131313131313");
mLayout.setAdapter(new BaseLabelAdapter() {
@Override
public int getCount() {
return mItems.size();
}
@Override
public View getView(int position, ViewGroup parent) {
View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_label, parent, false);
TextView tv = view.findViewById(R.id.label_item);
tv.setText(mItems.get(position));
return view;
}
});
}
}
R.layout.item_label 布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/label_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/item_label_bg"
android:padding="5dp"
android:text="123" />
</LinearLayout>
大概就是这样, 大家最好还是先学习 View 的绘制流程 后, 再来分析这个, 相信会更加得心应手.
也会加深对 View 绘制流程中的 onMeasure 和 onLayout 的理解.
今天就到了这里了, 晚安, 小牛们.
明天继续努力 !!!
网友评论