流式布局可以说是在各种软件中的出场率都很高的一个布局方式,被广泛使用,像一些关键字搜索,标签等等的场景,更是随处可见,今天我们就来手把手打造一个FlowLayout。
FlowLayout由于是以一个容器的身份存在的,所以其需要继承的是ViewGroup而不是View,也就是说,我们今天所要做的,就是去自定义一个ViewGroup。
自定义ViewGroup和自定义View的套路基本都是一致的,但也有部分差异,下面,我们先来说说自定义ViewGroup的套路的几个关键点:
自定义属性(这个就不说了,在之前的文章中已经说了很多次了)
实现onMeasure()方法,通过测量每一个子View的宽高,来计算自身的宽高,最后达到测量自身宽高的目的。
实现onLayout()方法,该方法用于按照自己的想法来摆放可见(不为GONE)的子View。
实现onDraw()方法,默然情况下继承ViewGroup是不会调用该方法的,如果需要绘制一些界面,可以实现dispatchDraw()方法
注意点:在确定要自定义ViewGroup的时候,可以先考虑下先继承自一些已经封装好的ViewGroup,如LinearLayout等进行开发。
好了,下面,我们开始自定义FlowLayout
首先,自定义属性这一块,我们就直接忽略了,直接从onMeasure方法开始做起(判断view.visibility != GONE,之前忘加了):
/**
* 测量该控件的宽高
* 思路:通过测量每一个子view的宽高 来得到该layout的整体宽高
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 解决onMeasure被调用多次,导致在摆放子View的时候出现多次摆放的情况
*/
mChildViews.clear();
//获取子View的数量
int childCount = getChildCount();
//获取该layout的宽度 以方便控制后面TAG的摆放 和计算该Layout的高度
int width = MeasureSpec.getSize(widthMeasureSpec);
//初始化layout的高度
int height = getPaddingTop() + getPaddingBottom();
int lineWidth = getPaddingLeft();
//初始化一个每一行的高度 计算layout的总高度时,取每一行的最高
int maxHeight = 0;
ArrayList<View> views = new ArrayList<>();
mChildViews.add(views);
//for循环测量子View
for(int i = 0;i < childCount; i++){
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
ViewGroup.MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
//当叠加的宽度大于该Layout的总宽度时,则换行
if((lineWidth + childView.getMeasuredWidth() + params.leftMargin + params.rightMargin) > width){
maxHeight = Math.max(maxHeight,childView.getMeasuredHeight() + params.topMargin + params.bottomMargin);
height += maxHeight;
lineWidth = getPaddingLeft() + childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
views = new ArrayList<>();
mChildViews.add(views);
views.add(childView);
}else{
maxHeight = Math.max(maxHeight,childView.getMeasuredHeight() + params.topMargin + params.bottomMargin);
lineWidth += childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
views.add(childView);
}
}
height += maxHeight;
setMeasuredDimension(width,height);
}
在计算宽度时,我们需要获取到每个子View的margin值,但是由于ViewGroup本身是没有LayoutParams的,所以这里模仿了下LinearLayout的方法,来获取MarginLayoutParams:
/**
* 重写ViewGroup的该方法 获取到margin值(可以模仿LinearLayout)
* @param attrs
* @returnop
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
在onMeasure方法中,当几个子View的宽度总和大于FlowLayout的宽度时,这个时候就需要换行。其他的,注释应该都有了!
下一步,就是实现onLayout()方法,来摆放所有的子View(判断view.visibility != GONE,之前忘加了)
“`
/**
* 摆放子view
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int maxHeight,left,right,top,bottom;
top = getPaddingTop();
//循环遍历mChildViews
for(ArrayList<View> views : mChildViews){
left = getPaddingLeft();
maxHeight = 0;
for(int i = 0;i<views.size();i++){
View childView = views.get(i);
ViewGroup.MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
maxHeight = Math.max(maxHeight,childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin);
left += marginLayoutParams.leftMargin;
right = left + childView.getMeasuredWidth();
int childTop = top + marginLayoutParams.topMargin;
bottom = childTop + childView.getMeasuredHeight();
childView.layout(left,childTop,right,bottom);
left += marginLayoutParams.rightMargin + childView.getMeasuredWidth();
}
top += maxHeight;
}
}
到这里呢,基本是这个流式布局就完成了一大半了,下面要做的,就是往这个布局里面扔数据,测试其是否会按照我们既定的规则摆放。
在设置数据这一步,我们使用了Adapter的设计模式。下面来看具体的步骤:
1:定义一个抽象的BaseAdapter
/**
* Created by DELL on 2017/9/9.
* Description :
*/
public abstract class BaseAdapter {
//获取view的数量
public abstract int getCount();
//获取View
public abstract View getView(int position, ViewGroup parent);
}
这边暂时只定义了两个方法,一个是获取子View数量的,另外一个是获取到每一个子View的,暂时没有定义notifySetDataChanged等方法,这个可以根据自己的需求,给其设置一个观察者来处理。
下一步,需要在FloawLayout中去设置Adapter达到添加布局的效果
public void setAdapter(BaseAdapter adapter){
//当Adapter为空的时候,抛出异常
if(adapter == null){
throw new NullPointerException("adapter not null");
}
this.mAdapter = adapter;
int count = mAdapter.getCount();
for(int i = 0;i<count;i++){
View view = mAdapter.getView(i,this);
addView(view);
}
}
每一个子View的布局:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_textview"
android:padding="10dp"
android:layout_margin="5dp"
>
</TextView>
子view背景代码:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:color="@color/colorAccent" android:width="1dp"/>
<corners android:radius="8dp"/>
</shape>
下面,在Activity中设置Adapter,来测试下效果:
public class MainActivity extends AppCompatActivity {
private FlowLayout mFlowLayout;
private ArrayList<String> mStrList;
private LayoutInflater mLayoutInflater;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}
private void initData(){
mStrList = new ArrayList<>();
mStrList.add("JUSTH HELLO!");
mStrList.add("JUSTH HEJUSTH HELLO!LLO!");
mStrList.add("JUSTH LLO!");
mStrList.add("JUSTH HEJUSTH HELLO!LLO!");
mStrList.add("JUSTH HELLO!");
mStrList.add("JUSTLO!");
mStrList.add("JUSTH HELLO!");
mStrList.add("JUSTH HEJUSTH HELLO!LLO!");
mStrList.add("JUSTH HELO!");
mStrList.add("JUSTH HELLO!");
}
private void initView() {
mFlowLayout = (FlowLayout) findViewById(R.id.taglayout);
mLayoutInflater = LayoutInflater.from(this);
mFlowLayout.setAdapter(new BaseAdapter() {
@Override
public int getCount() {
return mStrList.size();
}
@Override
public View getView(int position, ViewGroup parent) {
View view = mLayoutInflater.inflate(R.layout.layout_textview,parent,false);
((TextView)view.findViewById(R.id.item_textview)).setText(mStrList.get(position));
return view;
}
});
}
}
最后,来一波效果图吧: