Android 中自动换行的标签实现
前言
最近看自定义控件时,看到了这种控件就搞了一个研究一下。先看图
就是根据每行剩余的位置和要显示的标签进行比对换行的控件。
实现
像这种布局我们一般都是集成ViewGroup然后再覆写它的onMeasure和onLayout方法。
public class FlowViewGroup extends ViewGroup {
private int mPaddingTop;
private int mPaddingLeft;
private int mPaddingRight;
private int mPaddingBottom;
private ArrayList<String> mList = new ArrayList<>();
private Context mContext;
public FlowViewGroup(Context context) {
super(context);
mContext = context;
}
public FlowViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
public FlowViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int left;
int lineHeight = 0;
for (int i = 0; i < lines.size(); i++) {
left = 0;
top += lineHeight;
lineHeight = 0;
ArrayList<View> views = lines.get(i);
for (View view : views) {
if (view.getVisibility() == GONE) {
continue;
}
MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
view.layout(left + layoutParams.leftMargin + mPaddingLeft, top + layoutParams.topMargin + mPaddingTop, left + view.getMeasuredWidth() + layoutParams.leftMargin + mPaddingLeft, top + view.getMeasuredHeight() + layoutParams.topMargin + mPaddingTop);
lineHeight = Math.max(lineHeight, view.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin);
left = left + view.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
}
}
}
//计算后要显示行的数据的集合
private ArrayList<ArrayList<View>> lines = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
lines.clear();
//自身的padding
mPaddingTop = getPaddingTop();
mPaddingLeft = getPaddingLeft();
mPaddingRight = getPaddingRight();
mPaddingBottom = getPaddingBottom();
int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
int widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
//通过计算获取的总行高(包括ViewGroup的padding和子View的margin)
int linesHeight = 0;
//最宽行的宽度
int widthMax = 0;
//当前行已占用的宽度
int lineEmployWidth = 0;
//计算时当前行的最大高度
int currentLineHeightMax = 0;
//每一行中View的数据集
ArrayList<View> lineInfo = new ArrayList<>();
//获取子View的个数
int childCount = getChildCount();
//遍历子View对其进行测算
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//判断子View的显示状态 gone就不进行测算
if (childView.getVisibility() == GONE) {
continue;
}
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = 0;
int childHeight = 0;
//获取view的测量宽度
childWidth += childView.getMeasuredWidth();
//每行的第一个添加父布局的paddingLeft
if (0 == i) {
childWidth += mPaddingLeft;
}
//获取子View自身的margin属性
childWidth += (layoutParams.leftMargin + layoutParams.rightMargin);
//当前的行高
childHeight = childHeight + (childView.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin);
//当前行放不下时,重起一行显示
if (lineEmployWidth + childWidth > widthMeasureSize - mPaddingRight) {
//初始当前行的宽度
lineEmployWidth = childWidth + mPaddingLeft;
//添加一次行高
linesHeight += currentLineHeightMax;
//初始化行高
currentLineHeightMax = childHeight;
lines.add(lineInfo);
lineInfo = new ArrayList<>();
lineInfo.add(childView);
} else {//当前行可以显示时
lineInfo.add(childView);
//增加当前行已显示的宽度
lineEmployWidth += childWidth;
//为了显示最大的行高
currentLineHeightMax = Math.max(currentLineHeightMax, childHeight);
//显示中最大的行宽
widthMax = Math.max(widthMax, lineEmployWidth);
}
}
lines.add(lineInfo);
linesHeight += (mPaddingTop + mPaddingBottom + currentLineHeightMax);
setMeasuredDimension((widthMeasureMode == MeasureSpec.EXACTLY) ? widthMeasureSize : widthMax + mPaddingRight, (heightMeasureMode == MeasureSpec.EXACTLY) ? heightMeasureSize : linesHeight);
}
/**
* 重写该方法是为了使用MarginLayoutParams获取子View的margin值
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
/**
*当使用adapter添加数据时使用。
*/
@Override
protected LayoutParams generateDefaultLayoutParams() {
return super.generateDefaultLayoutParams();
}
private TagFlowAdapter mAdapter;
public void setAdapter(TagFlowAdapter adapter) {
if (null == adapter) {
throw new NullPointerException("TagFlowAdapter is null, please check setAdapter(TagFlowAdapter adapter)...");
}
mAdapter = adapter;
adapter.setOnNotifyDataSetChangedListener(new TagFlowAdapter.OnNotifyDataSetChangedListener() {
@Override
public void OnNotifyDataSetChanged() {
notifyDataSetChanged();
}
});
adapter.notifyDataSetChanged();
}
private void notifyDataSetChanged() {
removeAllViews();
if (mAdapter == null || mAdapter.getCount() == 0) {
return;
}
MarginLayoutParams layoutParams = new MarginLayoutParams(generateDefaultLayoutParams());
for (int i = 0; i < mAdapter.getCount(); i++) {
View view = mAdapter.getView(i);
if (view == null) {
throw new NullPointerException("item layout is null, please check getView()...");
}
addView(view, i, layoutParams);
}
}
}
我们先看一下onMeasure方法。我们使用getPadding的方法获取父布局的内边距,再通过MeasureSpec的getMode和getSize方法获取测量模式和测量的宽高。遍历子View通过getMeasureWidth和getMeasureHeight来获取其宽高(该值包括子view的padding值),然后根据子View的MarginLayoutParams来获取它的margin值(需要注意的是在布局中直接添加子View的话要重写public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs);}方法。不然会抛类转换异常)。现在我们需要的数值都已经获取到了剩下就好办了。我们先定义一个ArrayList < ArrayList < View > > lines 来储存有多少行的数据。每开始一行 我们就new一个ArrayList lineInfo来储存这一行的要绘制的View。具体的测算方法看上面的代码就行了。
接下来我们再看一下onLayout的方法。在该方法中我们要摆放子view位置。我们在测算过程中已经将子View分行处理。所以我们可以直接遍历lines设置每个字View的位置就行了。具体的看代码就行了。
我们的标签个数、样式等不可能是固定的。所以我们仿照adapter的模式来为该view动态设置数据。我们看一下notifyDataSetChanged方法,我们遍历通过adapter添加的数据,使用addView进行添加。因为我们在测量方法中用到了MarginLayoutParams所以我们添加View的时候要加上一个MarginLayoutParams防止测算方法中抛异常。
再看一下我们定义的adapter。我们定义一个类继承adapter。就可以设置数据了
public abstract class TagFlowAdapter {
public abstract int getCount();
public abstract Object getItem(int position);
public abstract long getItemId(int position);
public abstract View getView(int position);
public void notifyDataSetChanged(){
if(null != mOnNotifyDataSetChangedListener){
mOnNotifyDataSetChangedListener.OnNotifyDataSetChanged();
}
}
/**
* 释放一个接口 串联adapter与view中间的数据刷新
*/
public interface OnNotifyDataSetChangedListener{
void OnNotifyDataSetChanged();
}
private OnNotifyDataSetChangedListener mOnNotifyDataSetChangedListener;
public void setOnNotifyDataSetChangedListener(OnNotifyDataSetChangedListener listener){
mOnNotifyDataSetChangedListener = listener;
}
}
具体的调用代码。
Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FlowViewGroup group = (FlowViewGroup) findViewById(R.id.group);
MyTagAdapter myTagAdapter = new MyTagAdapter(this);
group.setAdapter(myTagAdapter);
ArrayList<String> list = new ArrayList<>();
list.add("上班族");
list.add("程序员");
list.add("喜欢美食");
list.add("懒得健身");
list.add("没事就喜欢LOL");
list.add("宅");
list.add("美女");
list.add("帅哥");
list.add("尸兄");
list.add("不修边幅");
list.add("德玛西亚");
myTagAdapter.setData(list);
myTagAdapter.notifyDataSetChanged();
}
}
activity_main资源文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.tianfb.text.myflowlayout.FlowViewGroup
android:id="@+id/group"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="5dp"
>
</com.tianfb.text.myflowlayout.FlowViewGroup>
</RelativeLayout>
adapter的代码
public class MyTagAdapter extends TagFlowAdapter {
private ArrayList<String> mList = new ArrayList<>();
private Context mContext;
public MyTagAdapter(Context context){
mContext = context;
}
public void setData(ArrayList<String> list){
mList.clear();
mList.addAll(list);
notifyDataSetChanged();
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position) {
View view = View.inflate(mContext, R.layout.item, null);
TextView tv = (TextView) view.findViewById(R.id.tv);
tv.setText(mList.get(position));
return view;
}
}
item资源文件
<?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">
<TextView
style="@style/text_flag_01"
android:id="@+id/tv"/>
</LinearLayout>
style代码
<style name="text_flag_01">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_margin">5dp</item>
<item name="android:background">@drawable/flag_01</item>
<item name="android:textColor">#ffffff</item>
</style>
flag_01代码
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#7690A5" >
</solid>
<corners android:radius="5dp"/>
<padding
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="2dp" />
</shape>
好了搞定了。
上一篇: js简单表单验证(非弹出框)