流式布局
又到了一年一度的高考,回想起来自己参加高考已是八年前的事情了。有时候还是在梦中梦到高中生活,真的是,流光容易把人抛,红了樱桃,绿了芭蕉。
项目中有需求要用到流式布局,自己就自定义了一个FlowLayout,感觉还是有必要记录一下的。网上也有人写这种布局,不过连最基本的子控件的margin,viewgroup的wrap_content都没有处理,所以还是自己重新写了一遍。
以下的代码是从项目里抽出来的,已经去掉了不相关的代码,如果有需要的话可以自己扩展。先看下最终效果吧。
图中背景黄色部分就是我们的自定义ViewGroup,所谓流式布局最重要的一点就是可以换行。下面一起学习下怎么去实现这种布局吧。
实现过程
首先先思考一下如果要实现这种布局需要考虑什么,需要考虑viewgroup的宽高,如果是wrap_content的话就需要我们自己去测量出它的宽高。还要考虑子控件的margin值和viewgroup的padding值。这两个值在测量和摆放时候都需要加上。
整体思路就是在onMeasure时候去计算每行的子布局,并且记录下来,用一个List记录。然后根据这个List去摆放每一个子控件。这个List是二维的,它的每个item也是一个list,item的list是记录的每一行的view。下面看代码吧:
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class FlowLayout extends ViewGroup {
//子布局的数量
private int childCount = 0;
//记录所有的子view 每一项就是每一行view的集合
private List<List<View>> childList = new ArrayList<>();
//每一行的view
private List<View> lineList = new ArrayList<>();
//记录每一行的高度 只记录这一行最高的控件
private List<Integer> heightList = new ArrayList<>();
//FlowLayout的padding值
private int paddingLeft = 0;
private int paddingRight = 0;
private int paddingTop = 0;
private int paddingBottom = 0;
//标志位 不要重复测量
private boolean isMeasure = false;
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);
}
/***
* xml布局被解析成View对象的过程中,viewGroup.addView(view, params)传入的
* params就是通过viewGroup.generateLayoutParams(attrs)获得的,参数attrs
* 里包装的就是这个view在xml中的属性,所以如果我们不重写generateLayoutParams()
* 方法,那这个viewGroup里的子view就不支持margin设置了。
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取自身宽度的测量规则和父容器允许的宽度大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//获取自身高度的测量规则和父容器允许的高度大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//得到子布局个数
childCount = getChildCount();
//获取自身设置的Padding值
paddingLeft = getPaddingLeft();
paddingRight = getPaddingRight();
paddingTop = getPaddingTop();
paddingBottom = getPaddingBottom();
//测量的宽度 记录实际的宽高 wrap_content才用得到
int measureWidth = paddingLeft + paddingRight;
int measureHeight = paddingTop + paddingBottom;
//记录一行的宽度
int lineWidth = paddingRight + paddingLeft;
//开始测量子控件
measureChildren(widthMeasureSpec, heightMeasureSpec);
//子view
View view;
//MarginLayoutParams 用来获取每个子控件的margin值
MarginLayoutParams params;
//记录每行最大的高度值
int lineHeight = 0;
if (!isMeasure) {
isMeasure = true;
} else {
//遍历每个子view
for (int i = 0; i < childCount; i++) {
view = getChildAt(i);
params = (MarginLayoutParams) view.getLayoutParams();
//lineWidth代表当前已经添加view的总宽度 如果当前已添加的宽度加上这个子view的宽度加margin超过的最大宽度,就换行
if ((lineWidth + params.leftMargin + params.rightMargin + view.getMeasuredWidth()) > widthSize) {
//换行 将上一行的list记录加到总的list中
childList.add(lineList);
//重置行宽 初始值为当前行第一个view的宽度加上margin再加上viewgroup的padding值
lineWidth = view.getMeasuredWidth() + params.leftMargin + params.rightMargin+paddingRight + paddingLeft;
//新建一个list来记录当前行的view
lineList = new ArrayList<>();
//将当前view加到lineList中 当前view是每行的第一个view
lineList.add(view);
//记录当前行的高度
heightList.add(lineHeight);
//记录总的高度
measureHeight += lineHeight;
//记录当前子view的高度
lineHeight = view.getMeasuredHeight() + params.topMargin + params.bottomMargin;
} else {
//如果当前行能放下一个子view 则将该view加到lineList中 并记录行宽行高
lineWidth += params.leftMargin + params.rightMargin + view.getMeasuredWidth();
lineList.add(view);
//高度取遍历本行的过程中子view最大的高
lineHeight = Math.max(lineHeight, view.getMeasuredHeight() + params.topMargin + params.bottomMargin);
}
//宽度取最宽的那一行的值
measureWidth = Math.max(measureWidth, lineWidth);
//因为最后一个子view不会触发换行,而换行才会将上一行的list添加到总的list中
//所以当遍历到最后一个子view后需要手动再添加一次,并且计算总高度
if (i == childCount - 1) {
childList.add(lineList);
heightList.add(lineHeight);
measureHeight += lineHeight;
}
}
}
//如果宽是wrap_content 宽就是测量出来的宽
if (widthMode == MeasureSpec.AT_MOST) {
widthSize = measureWidth;
}
//如果高是wrap_content 高是测量出来的高
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = measureHeight;
}
setMeasuredDimension(widthSize, heightSize);
}
//摆放子view
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left, right, top, bottom;
MarginLayoutParams marginLayoutParams;
top = paddingTop;
//记录总的行高
int allHeight = paddingTop;
//指针 从heightList中取出每行的高度
int i = 0;
for (List<View> list : childList) {
//每行都从viewgroup的paddingleft处摆放
left = paddingLeft;
for (View view : list) {
marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
//子view的左边需要加上自身的leftMargin值
left += marginLayoutParams.leftMargin;
//子view的上边需要加上自身的topmargin
top += marginLayoutParams.topMargin;
//右边就是左边加上自身宽度
right = left + view.getMeasuredWidth();
//底部就是顶部加上自身高度
bottom = top + view.getMeasuredHeight();
//开始摆放
view.layout(left, top, right, bottom);
//一行一行的摆放 所以left需要累加 以供它右边的子view使用
left += view.getMeasuredWidth() + marginLayoutParams.rightMargin;
//重置top值 因为每个子view的topmargin可能不一样 每次都有重新计算top 注意top不需要累加
top = allHeight;
}
//记录当前行的高度
allHeight += heightList.get(i++);
//初始化行高
top = allHeight;
}
}
//这里不需要重写onDraw 子控件具体的绘制由每个子控件完成
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
//动态addView(View)需要重写该方法
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
}
代码已经加了详细注释,最好静下心来看,或者自己撸一遍印象才更加深刻。下面是在xml中的使用:
<com.honeywell.flowlayout.FlowLayout
android:id="@+id/flowlayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFF000"
android:paddingLeft="20dp"
android:paddingTop="15dp"
android:paddingBottom="20dp"
android:paddingRight="10dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:text="陆军三十八集团军"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:layout_marginBottom="10dp"
android:textSize="14sp"
android:text="空军航空兵第二师"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:text="海军南海舰队"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:text="第二炮兵"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:textSize="20sp"
android:layout_marginBottom="10dp"
android:text="武警机动师187师"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:layout_marginBottom="9dp"
android:text="解放军信息工程大学"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:layout_marginBottom="5dp"
android:text="大连舰艇学院"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:text="武警海警总队"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:text="陆军第20集团军"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:text="海军陆战队164旅"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:text="济南军区"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:textSize="20sp"
android:text="空军95357部队"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#61DF33"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:padding="5dp"
android:text="空军空降兵15军"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="陆军航空兵"/>
</com.honeywell.flowlayout.FlowLayout>
下面代码是在java中动态给FlowLayout添加子view:
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
FlowLayout flowLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
flowLayout=findViewById(R.id.flowlayout);
TextView textView = new TextView(this);
textView.setText("武警黄金部队");
textView.setBackgroundColor(Color.GREEN);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.setMargins(20,30,0,0);
textView.setLayoutParams(layoutParams);
flowLayout.addView(textView,layoutParams);
}
}
总结
其实总体来说不是很复杂,最多就是每行宽高的计算,摆放时候的左上右下的处理。源码下载:源码,不过还是希望大家可以自己去实现一遍。