一些叨叨
- 市面上所有的app只要有搜索功能,几乎都离不开流式布局,像淘宝、京东、小红书等等。暑假的时候写了一个类似淘宝的app,就用到了这个流式布局。
这个是自己的app实战效果
下面是测试效果
继承ViewGrop 实现自定义控件
自定义ViewGrop 有几个关键点,其中测量 、摆放最重要。 第一步当然是继承ViewGroup 了
public class FlowLayout extends ViewGroup {
}
重写构造器
继承 ViewGrop 需要一些构造方法, 全部写调用自身不同的构造方法达到统一参数入口的目的,谷歌的TextView 也是这样写的。这里getXXX 就相当于在layout文件中获取定义过的量,没有定义就设置方法中的缺省值。
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
mHorizontalMargin = a.getDimension(R.styleable.FlowLayout_itemHorizontalMargin, DEFAULT_HORIZONTAL_MARGIN);
mVerticalMargin = a.getDimension(R.styleable.FlowLayout_itemVerticalMargin, DEFAULT_VERTICAL_MARGIN);
mTextMaxLength = a.getInt(R.styleable.FlowLayout_textMaxLength, DEFAULT_TEXT_MAX_LENGTH);
if(mTextMaxLength!=-1&&mTextMaxLength<=0){
throw new IllegalArgumentException("max length must not less than 0");
}
mMaxLine = a.getInt(R.styleable.FlowLayout_maxLine, DEFAULT_MAX_LINE);
if(mMaxLine!=-1&&mMaxLine<=0){
throw new IllegalArgumentException("max line must not less than 0");
}
mTextColor = a.getColor(R.styleable.FlowLayout_textColor, getResources().getColor(R.color.black));
mBorderColor = a.getColor(R.styleable.FlowLayout_textBorderColor, getResources().getColor(R.color.black));
mBorderRadius = a.getDimension(R.styleable.FlowLayout_borderRadius, DEFAULT_BORDER_RADIUS);
Log.d(TAG, "FlowLayout: mHorizontalMargin" + mHorizontalMargin + "\n" +
"mVerticalMargin=" + mVerticalMargin + "\n" +
"mTextMaxLength=" + mTextMaxLength + "\n" +
"mTextColor=" + mTextColor + "\n" +
"mBorderColor=" + mBorderColor + "\n" +
"mBorderRadius=" + mBorderRadius);
a.recycle();
}
在value 包下创建attrs.xml ,写上自己想要的属性
<declare-styleable name="FlowLayout">
<attr name="itemHorizontalMargin" format="dimension"></attr>
<attr name="itemVerticalMargin" format="dimension"></attr>
<attr name="textMaxLength" format="integer"></attr>
<attr name="textColor" format="color"></attr>
<attr name="textBorderColor" format="color|reference"></attr>
<attr name="borderRadius" format="dimension"></attr>
<attr name="maxLine" format="integer"></attr>
</declare-styleable>
提供对外接口
数据通过set方法传进来,内部需要维护一个链表。这里的泛型可以自定义,可以传一个实体类,这里简单起见,仅展示文本。
public void setTextList(List<String> list) {
mData.clear();
mData.addAll(list);
setUpChildren();
}
setUpChildren() 主要用来更新TextView 中展示的文本,以及提供点击事件。一个for循环遍历完所有数据,添加然后创建TextView ,添加到ViewGrop 中即可。
private void setUpChildren() {
removeAllViews();
for (String mDatum : mData) {
TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.flow_item, this, false);
textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(mTextMaxLength)});
Log.d(TAG,"mDatum.length()---------------->"+mDatum.length());
String finalMDatum = mDatum;
textView.setText(mDatum);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onItemClickListener != null) {
onItemClickListener.OnItemClick(v, finalMDatum);
}
}
});
addView(textView);
}
}
内部维护一个点击事件
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public interface OnItemClickListener {
void OnItemClick(View v, String text);
}
测量
测量已经注释已经说的很清楚啦。
private List<List<View>> lines = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, " in onMeasure");
int childCount = getChildCount();
Log.d(TAG, " childCount ========>" + childCount);
if (childCount == 0) {
return;
}
lines.clear();
List<View> line = new ArrayList<>();
lines.add(line);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
int childMeasureSpaceWidth = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
int childMeasureSpaceHeight = MeasureSpec.makeMeasureSpec(parentHeight, MeasureSpec.AT_MOST);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != VISIBLE) {
continue;
}
measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);
if(mMaxLine!=-1&&lines.size()>mMaxLine){
return;
}
if (line.size() == 0) {
line.add(child);
} else {
boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
Log.d(TAG, "onMeasure: canBeAdd-------------》" + canBeAdd);
if (canBeAdd) {
line.add(child);
} else {
line = new ArrayList<>();
lines.add(line);
i--;
}
}
}
private boolean checkChildCanBeAdd(List<View> line, View child, int parentWidth) {
int totalSize = getPaddingLeft();
totalSize += child.getMeasuredWidth();
for (View view : line) {
totalSize += view.getMeasuredWidth();
totalSize += (int) mHorizontalMargin;
}
totalSize += getPaddingRight();
return totalSize <= parentWidth;
}
摆放
摆放也算一个简单的算法了吧,对于做过好多算法的你们来说肯定不难理解。 这里直接也看着上面的图,注意,这里开始的时候垂直高度要加paddingTop ,同样底边也可以加一下paddingBottom ,前面的图应为只需要计算这个控件在哪一行,哪个集合里,所以不需要加垂直方向的padding 值。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.d(TAG, "in onLayout-------------->");
if (lines.size() == 0) {
return;
}
View firstChild = getChildAt(0);
int aChildHeight = firstChild.getMeasuredHeight();
int aStartLeft;
int aStartTop = getPaddingTop();
for (int i = 0; i < lines.size(); i++) {
aStartLeft = getPaddingLeft();
List<View> line = lines.get(i);
for (View view : line) {
view.layout(aStartLeft, aStartTop, aStartLeft + view.getMeasuredWidth(),
aStartTop + view.getMeasuredHeight());
aStartLeft += (int) mHorizontalMargin;
aStartLeft += view.getMeasuredWidth();
}
aStartTop += aChildHeight;
aStartTop += (int) mVerticalMargin;
}
}
使用方法
xml 代码
<com.lw.flow.FlowLayout
android:id="@+id/flowLayout"
android:layout_width="match_parent"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="10dp"
android:layout_height="match_parent">
</com.lw.flow.FlowLayout>
这里其他属性就自己写到代码里啦 Activity 中:
flowLayout = findViewById(R.id.flowLayout);
List<String> list = new ArrayList<>();
list.add("这是个关键");
list.add("iPad");
list.add("Android");
list.add("数码摄像机");
list.add("耳机");
list.add("鼠标");
list.add("键盘");
for (int i = 0; i < 5; i++) {
list.add("关键字" + i);
}
flowLayout.setTextList(list);
flowLayout.setOnItemClickListener(new FlowLayout.OnItemClickListener() {
@Override
public void OnItemClick(View v, String text) {
Toast.makeText(getApplicationContext(),"点击了:"+text,Toast.LENGTH_SHORT).show();
}
});
效果如下:
整合到自己的业务之后就能有下面的效果啦:
完整代码
package com.lw.tiketunion.ui.custom;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.InputFilter;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.lw.tiketunion.R;
import com.lw.tiketunion.base.App;
import com.lw.tiketunion.utils.LogUtils;
import com.lw.tiketunion.utils.SizeUtils;
import java.util.ArrayList;
import java.util.List;
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
private static final int DEFAULT_MAX_LINE = -1;
private List<String> mData = new ArrayList<>();
public static final int DEFAULT_BORDER_RADIUS = SizeUtils.dip2px(App.getContext(), 5);
public static final int DEFAULT_TEXT_MAX_LENGTH = 5;
private static final int DEFAULT_HORIZONTAL_MARGIN = SizeUtils.dip2px(App.getContext(), 10);
private static final int DEFAULT_VERTICAL_MARGIN = SizeUtils.dip2px(App.getContext(), 10);
private final int mTextColor;
private float mHorizontalMargin;
private float mVerticalMargin;
private int mTextMaxLength;
private int mBorderColor;
private float mBorderRadius;
private OnItemClickListener onItemClickListener;
private int mMaxLine;
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
mHorizontalMargin = a.getDimension(R.styleable.FlowLayout_itemHorizontalMargin, DEFAULT_HORIZONTAL_MARGIN);
mVerticalMargin = a.getDimension(R.styleable.FlowLayout_itemVerticalMargin, DEFAULT_VERTICAL_MARGIN);
mTextMaxLength = a.getInt(R.styleable.FlowLayout_textMaxLength, DEFAULT_TEXT_MAX_LENGTH);
if (mTextMaxLength != -1 && mTextMaxLength <= 0) {
throw new IllegalArgumentException("max length must not less than 0");
}
mMaxLine = a.getInt(R.styleable.FlowLayout_maxLine, DEFAULT_MAX_LINE);
if (mMaxLine != -1 && mMaxLine <= 0) {
throw new IllegalArgumentException("max line must not less than 0");
}
mTextColor = a.getColor(R.styleable.FlowLayout_textColor, getResources().getColor(R.color.black));
mBorderColor = a.getColor(R.styleable.FlowLayout_textBorderColor, getResources().getColor(R.color.black));
mBorderRadius = a.getDimension(R.styleable.FlowLayout_borderRadius, DEFAULT_BORDER_RADIUS);
Log.d(TAG, "FlowLayout: mHorizontalMargin" + mHorizontalMargin + "\n" +
"mVerticalMargin=" + mVerticalMargin + "\n" +
"mTextMaxLength=" + mTextMaxLength + "\n" +
"mTextColor=" + mTextColor + "\n" +
"mBorderColor=" + mBorderColor + "\n" +
"mBorderRadius=" + mBorderRadius);
a.recycle();
}
public void setTextList(List<String> list) {
mData.clear();
mData.addAll(list);
setUpChildren();
}
public void deleteAllList() {
mData.clear();
removeAllViews();
TextView textView = new TextView(getContext());
textView.setText("暂无历史记录");
textView.setTextColor(Color.BLACK);
addView(textView);
invalidate();
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public interface OnItemClickListener {
void OnItemClick(View v, String text);
}
private void setUpChildren() {
removeAllViews();
for (String mDatum : mData) {
TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.item_flow, this, false);
textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(mTextMaxLength)});
Log.d(TAG, "mDatum.length()---------------->" + mDatum.length());
String finalMDatum = mDatum;
textView.setText(mDatum);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onItemClickListener != null) {
onItemClickListener.OnItemClick(v, finalMDatum);
}
}
});
addView(textView);
}
}
private List<List<View>> lines = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, " in onMeasure");
int childCount = getChildCount();
Log.d(TAG, " childCount ========>" + childCount);
if (childCount == 0) {
return;
}
lines.clear();
List<View> line = new ArrayList<>();
lines.add(line);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
int childMeasureSpaceWidth = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
int childMeasureSpaceHeight = MeasureSpec.makeMeasureSpec(parentHeight, MeasureSpec.AT_MOST);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != VISIBLE) {
continue;
}
measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);
if (mMaxLine != -1 && lines.size() > mMaxLine) {
return;
}
if (line.size() == 0) {
line.add(child);
} else {
boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
Log.d(TAG, "onMeasure: canBeAdd-------------》" + canBeAdd);
if (canBeAdd) {
line.add(child);
} else {
line = new ArrayList<>();
lines.add(line);
i--;
}
}
}
int finalParentHeight;
View child = getChildAt(0);
int measuredHeight = child.getMeasuredHeight();
Log.d(TAG, "onMeasure:lines.size()--------> " + lines.size());
finalParentHeight = lines.size() * (measuredHeight + (int) mVerticalMargin);
setMeasuredDimension(parentWidth, finalParentHeight);
}
private boolean checkChildCanBeAdd(List<View> line, View child, int parentWidth) {
int totalSize = getPaddingLeft();
totalSize += child.getMeasuredWidth();
for (View view : line) {
totalSize += view.getMeasuredWidth();
totalSize += (int) mHorizontalMargin;
}
totalSize += getPaddingRight();
return totalSize <= parentWidth;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.d(TAG, "in onLayout-------------->");
if (lines.size() == 0) {
return;
}
View firstChild = getChildAt(0);
int aChildHeight = firstChild.getMeasuredHeight();
int aStartLeft;
int aStartTop = getPaddingTop();
for (int i = 0; i < lines.size(); i++) {
aStartLeft = getPaddingLeft();
List<View> line = lines.get(i);
for (View view : line) {
view.layout(aStartLeft, aStartTop, aStartLeft + view.getMeasuredWidth(),
aStartTop + view.getMeasuredHeight());
aStartLeft += (int) mHorizontalMargin;
aStartLeft += view.getMeasuredWidth();
}
aStartTop += aChildHeight;
aStartTop += (int) mVerticalMargin;
}
}
}
推荐学习资料:
|