自定义流式布局控件

时间:2022-06-27 17:03:26

效果图:

自定义流式布局控件

原理图:

自定义流式布局控件

代码:

/**
* Created by mChenys on 2015/12/5.
* 2016/09/28 修改,对没有子控件的情况,避免MyFlowLayout填充父容器的情况
*/
public class MyFlowLayout extends ViewGroup {
public static final int DEFAULT_SPACING = 20;
//水平间距和垂直间距
private int mHorizontalSpacing = DEFAULT_SPACING, mVertivalSpacing = DEFAULT_SPACING;
private Line mLine = null; //当前行
private int mUsedWidth = 0;//当前行已使用的宽度
private int maxLine = 100; //最大行数
private List<Line> mLineList = new ArrayList<>();

public MyFlowLayout(Context context) {
this(context, null);
}

public MyFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
int lines = mLineList.size();
//确定行之间的垂直位置
for (int i = 0; i < lines; i++) {
Line oneLine = mLineList.get(i);
oneLine.layoutView(left, top);
//更新每一行的top坐标
top += oneLine.mHeight + mVertivalSpacing;
}
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//子控件的最大宽高
int maxChildWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int maxChildHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
//父控件的宽高模式
int parentWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int parentHeightMode = MeasureSpec.getMode(heightMeasureSpec);
restoreLine();//每次进来onMeasure都需要重置一下数据

int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() == View.GONE) {
continue;
}
//确定子控件的宽高模式
int childWidthSpec = MeasureSpec.makeMeasureSpec(maxChildWidth,
parentWidthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : parentWidthMode);
int childHeightSpec = MeasureSpec.makeMeasureSpec(maxChildHeight,
parentHeightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : parentHeightMode);
//测量子控件
childView.measure(childWidthSpec, childHeightSpec);
if (mLine == null) {
mLine = new Line();
}
int width = childView.getMeasuredWidth();
mUsedWidth += width;
if (mUsedWidth <= maxChildWidth) {
//当前使用的宽度没有超过最大子控件的宽度,那么可以添加到行中
mLine.addView(childView);
mUsedWidth += mHorizontalSpacing;//加上水平间距
if (mUsedWidth >= maxChildWidth) {
//加上水平间距后大于了最大子控件宽度,那么需要换行
if (!newLine()) {
break;
}
}
} else {
//2种情况
//1.当前行没有控件,要添加的控件宽度大于最大宽度,强制添加,再换行
if (mLine.getViewCount() == 0) {
mLine.addView(childView);
if (!newLine()) {
break;
}
} else {
//2.当前行剩余空间不够存放,需要先换行,再添加
if (!newLine()) {
break;
}
mLine.addView(childView);
mUsedWidth += childView.getMeasuredWidth() + mHorizontalSpacing;//更新当前行使用的宽度
}
}
}
//确保最后一行有剩余宽度的情况,也要把这一行添加到集合
if (null != mLine && mLine.getViewCount() > 0 && !mLineList.contains(mLine)) {
mLineList.add(mLine);
}
//确定父控件宽高
int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
int totalHeight = 0;
int lines = mLineList.size();
for (int i = 0; i < lines; i++) {
Line oneLine = mLineList.get(i);
totalHeight += oneLine.mHeight;
}
if (lines > 0) {
totalHeight += mVertivalSpacing * (lines - 1) + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(totalWidth, totalHeight);
} else {
//没有子控件时避免该控件填充父容器
setMeasuredDimension(0, 0);
}

}

/**
* 创建新行
*
* @return
*/
private boolean newLine() {
//保存当前行
mLineList.add(mLine);
if (mLineList.size() > maxLine) {
return false;
}
//重置变量
mLine = new Line();
mUsedWidth = 0;
return true;
}

/**
* 还原所有数据
*/
private void restoreLine() {
mLineList.clear();
mLine = new Line();
mUsedWidth = 0;
}

/**
* 代表着一行,封装了一行所占高度,该行子View的集合,以及所有View的宽度总和
*/
private class Line {
private int mHeight;//行高
private int mWidth; //当前行子View的总宽度
private List<View> childViews = new ArrayList<>();

//添加子控件到行
public void addView(View view) {
childViews.add(view);
mHeight = mHeight > view.getMeasuredHeight() ? mHeight : view.getMeasuredHeight();
mWidth += view.getMeasuredWidth();
}

//获取当前行的子控件个数
public int getViewCount() {
return childViews.size();
}

public void layoutView(int left, int top) {
//1.有剩余宽度的情况
int viewCount = getViewCount();
int surplusWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() -
mWidth - mHorizontalSpacing * (viewCount - 1);
if (surplusWidth >= 0) {
//需要平均分配
int splitWidth = (int) (surplusWidth / viewCount + 0.5f);
for (int i = 0; i < viewCount; i++) {
View childView = childViews.get(i);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
//平分宽度的逻辑,如果不需要平分,可以注释掉
if (splitWidth > 0) {
width += splitWidth;
//重新测量控件
int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
childView.measure(widthSpec, heightSpec);
}
//高度低的要垂直居中显示
int topOffset = (int) ((mHeight - height) / 2 + 0.5f);
if (topOffset < 0) {
topOffset = 0;
}
//更新top值
top += topOffset;
//确定当前行子控件的位置
childView.layout(left, top, left + width, top + height);
//更新下一子控件的left坐标
left += width + mHorizontalSpacing;
}
} else {
//2.超出子控件的最大宽度,强制添加后的位置确定
if (getViewCount() == 1) {
View childView = childViews.get(0);
childView.layout(left, top, left + childView.getMeasuredWidth(), top + childView.getMeasuredHeight());
}
}
}
}
}


使用方式:

public class MainActivity extends AppCompatActivity {
String content = "['QQ','视频','放开那三国','电子书','酒店','单机','小说','斗地主','优酷','网游'," +
"'WIFI万能钥匙','播放器','捕鱼达人2','机票','游戏','熊出没之熊大快跑','美图秀秀','浏览器','" +
"单机游戏','我的世界','电影电视','QQ空间','旅游','免费游戏','2048','刀塔传奇','壁纸','节奏大师'," +
"'锁屏','装机必备','天天动听','备份','网盘','海淘网','大众点评','爱奇艺视频','腾讯手机管家','百度地图'," +
"'猎豹清理大师','谷歌地图','hao123上网导航','京东','youni有你','万年历-农历黄历','支付宝钱包']";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ScrollView scrollView = new ScrollView(this);
int padding = (int) (this.getResources().getDisplayMetrics().density * 10 + 0.5f);
scrollView.setPadding(padding, padding, padding, padding);
MyFlowLayout flowLayout = new MyFlowLayout(this);
Random random = new Random();
try {
JSONArray jsonArray = new JSONArray(content);
for (int i = 0; i < jsonArray.length(); i++) {
final String str = jsonArray.optString(i);
TextView textView = new TextView(this);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
textView.setText(str);
textView.setGravity(Gravity.CENTER);
textView.setPadding(padding, padding, padding, padding);
textView.setTextColor(Color.WHITE);
//随机创建颜色值
int r = 30 + random.nextInt(210);
int g = 30 + random.nextInt(210);
int b = 30 + random.nextInt(210);
//按下后的偏白的背景色
int pressColor = 0xffcecece;
int defaultColor = Color.rgb(r, g, b);
//创建圆角矩形背景选择器
StateListDrawable selector = DrawableUtils.getStateList(defaultColor, pressColor, 5);
textView.setBackgroundDrawable(selector);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
}
});
flowLayout.addView(textView);
}
} catch (JSONException e) {
e.printStackTrace();
}
scrollView.addView(flowLayout);
setContentView(scrollView);
}
}