TagFlowLayout(新手搬运完整版) 简单的多选tag布局
先上一张效果图:类似淘宝选择款式时候的多选框。上网搜索到了TagFlowLayout这个东西,而且大多数都是鸿洋大神的博客出处http://blog.****.net/lmj623565791/article/details/48393217。看了下博客,只是关键步骤零碎的东西 并不完整 对于下载Demo无法运行的我来说整理了好久才终于整理出了这个东西的完整版。这里为了给自己以后般运用,具体每步目的没有明确标出,如果想学习原理的人请附加鸿洋大神的博客一起观看,否则的话直接把各个类粘贴到自己的项目中修改下选中和未选中的状态就能用了。
1.首先我们需要一个FlowLayout这个类继承自ViewGroup
public class FlowLayout extends ViewGroup { private static final String TAG = "FlowLayout"; private static final int LEFT = -1; private static final int CENTER = 0; private static final int RIGHT = 1; protected List<List<View>> mAllViews = new ArrayList<List<View>>(); protected List<Integer> mLineHeight = new ArrayList<Integer>(); protected List<Integer> mLineWidth = new ArrayList<Integer>(); private int mGravity; private List<View> lineViews = new ArrayList<>(); public FlowLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout); mGravity = ta.getInt(R.styleable.TagFlowLayout_gravity,LEFT); ta.recycle(); } public FlowLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FlowLayout(Context context) { this(context, null); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); // wrap_content int width = 0; int height = 0; int lineWidth = 0; int lineHeight = 0; int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { View child = getChildAt(i); if (child.getVisibility() == View.GONE) { if (i == cCount - 1) { width = Math.max(lineWidth, width); height += lineHeight; } continue; } measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) { width = Math.max(width, lineWidth); lineWidth = childWidth; height += lineHeight; lineHeight = childHeight; } else { lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); } if (i == cCount - 1) { width = Math.max(lineWidth, width); height += lineHeight; } } setMeasuredDimension( // modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(), modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()// ); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mAllViews.clear(); mLineHeight.clear(); mLineWidth.clear(); lineViews.clear(); int width = getWidth(); int lineWidth = 0; int lineHeight = 0; int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { View child = getChildAt(i); if (child.getVisibility() == View.GONE) continue; MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) { mLineHeight.add(lineHeight); mAllViews.add(lineViews); mLineWidth.add(lineWidth); lineWidth = 0; lineHeight = childHeight + lp.topMargin + lp.bottomMargin; lineViews = new ArrayList<View>(); } lineWidth += childWidth + lp.leftMargin + lp.rightMargin; lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin); lineViews.add(child); } mLineHeight.add(lineHeight); mLineWidth.add(lineWidth); mAllViews.add(lineViews); int left = getPaddingLeft(); int top = getPaddingTop(); int lineNum = mAllViews.size(); for (int i = 0; i < lineNum; i++) { lineViews = mAllViews.get(i); lineHeight = mLineHeight.get(i); // set gravity int currentLineWidth = this.mLineWidth.get(i); switch (this.mGravity){ case LEFT: left = getPaddingLeft(); break; case CENTER: left = (width - currentLineWidth)/2+getPaddingLeft(); break; case RIGHT: left = width - currentLineWidth + getPaddingLeft(); break; } for (int j = 0; j < lineViews.size(); j++) { View child = lineViews.get(j); if (child.getVisibility() == View.GONE) { continue; } MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth(); int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); left += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; } top += lineHeight; } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } }
2.然后需要一个TagView来实现checked选中的这么一个功能
public class TagView extends FrameLayout implements Checkable { private boolean isChecked; private static final int[] CHECK_STATE = new int[]{android.R.attr.state_checked}; public TagView(Context context) { super(context); } public View getTagView() { return getChildAt(0); } @Override public int[] onCreateDrawableState(int extraSpace) { int[] states = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) { mergeDrawableStates(states, CHECK_STATE); } return states; } /** * Change the checked state of the view * * @param checked The new checked state */ @Override public void setChecked(boolean checked) { if (this.isChecked != checked) { this.isChecked = checked; refreshDrawableState(); } } /** * @return The current checked state of the view */ @Override public boolean isChecked() { return isChecked; } /** * Change the checked state of the view to the inverse of its current state */ @Override public void toggle() { setChecked(!isChecked); } }
3.我们还需要一个TagAdapter
public abstract class TagAdapter<T>
{
private List<T> mTagDatas;
private OnDataChangedListener mOnDataChangedListener;
private HashSet<Integer> mCheckedPosList = new HashSet<Integer>();
public TagAdapter(List<T> datas)
{
mTagDatas = datas;
}
public TagAdapter(T[] datas)
{
mTagDatas = new ArrayList<T>(Arrays.asList(datas));
}
interface OnDataChangedListener
{
void onChanged();
}
void setOnDataChangedListener(OnDataChangedListener listener)
{
mOnDataChangedListener = listener;
}
public void setSelectedList(int... poses)
{
Set<Integer> set = new HashSet<>();
for (int pos : poses)
{
set.add(pos);
}
setSelectedList(set);
}
public void setSelectedList(Set<Integer> set)
{
mCheckedPosList.clear();
if (set != null)
mCheckedPosList.addAll(set);
notifyDataChanged();
}
HashSet<Integer> getPreCheckedList()
{
return mCheckedPosList;
}
public int getCount()
{
return mTagDatas == null ? 0 : mTagDatas.size();
}
public void notifyDataChanged()
{
mOnDataChangedListener.onChanged();
}
public T getItem(int position)
{
return mTagDatas.get(position);
}
public abstract View getView(FlowLayout parent, int position, T t);
public boolean setSelected(int position, T t)
{
return false;
}
}
4.最后就是我们的TagFlowLayout了
public class TagFlowLayout extends FlowLayout implements TagAdapter.OnDataChangedListener { private TagAdapter mTagAdapter; private boolean mAutoSelectEffect = true; private int mSelectedMax = -1;//-1为不限制数量 private static final String TAG = "TagFlowLayout"; private MotionEvent mMotionEvent; private Set<Integer> mSelectedView = new HashSet<Integer>(); public TagFlowLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout); mAutoSelectEffect = ta.getBoolean(R.styleable.TagFlowLayout_auto_select_effect, true); mSelectedMax = ta.getInt(R.styleable.TagFlowLayout_max_select, -1); ta.recycle(); if (mAutoSelectEffect) { setClickable(true); } } public TagFlowLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TagFlowLayout(Context context) { this(context, null); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { TagView tagView = (TagView) getChildAt(i); if (tagView.getVisibility() == View.GONE) continue; if (tagView.getTagView().getVisibility() == View.GONE) { tagView.setVisibility(View.GONE); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } public interface OnSelectListener { void onSelected(Set<Integer> selectPosSet); } private OnSelectListener mOnSelectListener; public void setOnSelectListener(OnSelectListener onSelectListener) { mOnSelectListener = onSelectListener; if (mOnSelectListener != null) setClickable(true); } public interface OnTagClickListener { boolean onTagClick(View view, int position, FlowLayout parent); } private OnTagClickListener mOnTagClickListener; public void setOnTagClickListener(OnTagClickListener onTagClickListener) { mOnTagClickListener = onTagClickListener; if (onTagClickListener != null) setClickable(true); } public void setAdapter(TagAdapter adapter) { mTagAdapter = adapter; mTagAdapter.setOnDataChangedListener(this); mSelectedView.clear(); changeAdapter(); } private void changeAdapter() { removeAllViews(); TagAdapter adapter = mTagAdapter; TagView tagViewContainer = null; HashSet preCheckedList = mTagAdapter.getPreCheckedList(); for (int i = 0; i < adapter.getCount(); i++) { View tagView = adapter.getView(this, i, adapter.getItem(i)); tagViewContainer = new TagView(getContext()); // ViewGroup.MarginLayoutParams clp = (ViewGroup.MarginLayoutParams) tagView.getLayoutParams(); // ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(clp); // lp.width = ViewGroup.LayoutParams.WRAP_CONTENT; // lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; // lp.topMargin = clp.topMargin; // lp.bottomMargin = clp.bottomMargin; // lp.leftMargin = clp.leftMargin; // lp.rightMargin = clp.rightMargin; tagView.setDuplicateParentStateEnabled(true); if (tagView.getLayoutParams() != null) { tagViewContainer.setLayoutParams(tagView.getLayoutParams()); } else { ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.setMargins(dip2px(getContext(), 5), dip2px(getContext(), 5), dip2px(getContext(), 5), dip2px(getContext(), 5)); tagViewContainer.setLayoutParams(lp); } tagViewContainer.addView(tagView); addView(tagViewContainer); if (preCheckedList.contains(i)) { tagViewContainer.setChecked(true); } if (mTagAdapter.setSelected(i, adapter.getItem(i))) { mSelectedView.add(i); tagViewContainer.setChecked(true); } } mSelectedView.addAll(preCheckedList); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { mMotionEvent = MotionEvent.obtain(event); } return super.onTouchEvent(event); } @Override public boolean performClick() { if (mMotionEvent == null) return super.performClick(); int x = (int) mMotionEvent.getX(); int y = (int) mMotionEvent.getY(); mMotionEvent = null; TagView child = findChild(x, y); int pos = findPosByView(child); if (child != null) { doSelect(child, pos); if (mOnTagClickListener != null) { return mOnTagClickListener.onTagClick(child.getTagView(), pos, this); } } return true; } public void setMaxSelectCount(int count) { if (mSelectedView.size() > count) { Log.w(TAG, "you has already select more than " + count + " views , so it will be clear ."); mSelectedView.clear(); } mSelectedMax = count; } public Set<Integer> getSelectedList() { return new HashSet<Integer>(mSelectedView); } private void doSelect(TagView child, int position) { if (mAutoSelectEffect) { if (!child.isChecked()) { //处理max_select=1的情况 if (mSelectedMax == 1 && mSelectedView.size() == 1) { Iterator<Integer> iterator = mSelectedView.iterator(); Integer preIndex = iterator.next(); TagView pre = (TagView) getChildAt(preIndex); pre.setChecked(false); child.setChecked(true); mSelectedView.remove(preIndex); mSelectedView.add(position); } else { if (mSelectedMax > 0 && mSelectedView.size() >= mSelectedMax) return; child.setChecked(true); mSelectedView.add(position); } } else { child.setChecked(false); mSelectedView.remove(position); } if (mOnSelectListener != null) { mOnSelectListener.onSelected(new HashSet<Integer>(mSelectedView)); } } } public TagAdapter getAdapter() { return mTagAdapter; } private static final String KEY_CHOOSE_POS = "key_choose_pos"; private static final String KEY_DEFAULT = "key_default"; @Override protected Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable(KEY_DEFAULT, super.onSaveInstanceState()); String selectPos = ""; if (mSelectedView.size() > 0) { for (int key : mSelectedView) { selectPos += key + "|"; } selectPos = selectPos.substring(0, selectPos.length() - 1); } bundle.putString(KEY_CHOOSE_POS, selectPos); return bundle; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; String mSelectPos = bundle.getString(KEY_CHOOSE_POS); if (!TextUtils.isEmpty(mSelectPos)) { String[] split = mSelectPos.split("\\|"); for (String pos : split) { int index = Integer.parseInt(pos); mSelectedView.add(index); TagView tagView = (TagView) getChildAt(index); if (tagView != null) tagView.setChecked(true); } } super.onRestoreInstanceState(bundle.getParcelable(KEY_DEFAULT)); return; } super.onRestoreInstanceState(state); } private int findPosByView(View child) { final int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { View v = getChildAt(i); if (v == child) return i; } return -1; } private TagView findChild(int x, int y) { final int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { TagView v = (TagView) getChildAt(i); if (v.getVisibility() == View.GONE) continue; Rect outRect = new Rect(); v.getHitRect(outRect); if (outRect.contains(x, y)) { return v; } } return null; } @Override public void onChanged() { mSelectedView.clear(); changeAdapter(); } public static int dip2px(Context context, float dpValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } }
在需要调用的activity中
public class MainActivity extends AppCompatActivity {
private TagFlowLayout flowLayout;
private ArrayList<String> city = new ArrayList<>();
private TagAdapter<String> mAdapterFive;
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
flowLayout = (TagFlowLayout) findViewById(R.id.flowLayout);
city.add("1");
city.add("2");
city.add("3");
city.add("哥谭市");
city.add("纽约");
city.add("一叶知秋");
city.add("该换行了");
flowLayout.setAdapter(mAdapterFive = new TagAdapter<String>(city){
@Override
public View getView(FlowLayout parent, int position, String s)
{
tv = new TextView(MainActivity.this);
tv.setTextSize(13);
tv.setPadding(34,18,34,18);
tv.setBackgroundResource(R.drawable.tag);
tv.setText(s);
return tv;
}
});
flowLayout.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() {
@Override
public boolean onTagClick(View view, int position, FlowLayout parent) {
Toast.makeText(MainActivity.this, city.get(position), Toast.LENGTH_SHORT).show();
return true;
}
});
}
}
其中tag为drawable下xml文件 分别在创建两个xml文件为选中和未选中状态下的背景
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/checked_bg"
android:state_checked="true"></item>
<item android:drawable="@drawable/normal_bg"></item>
</selector>
比如其中未选中时
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="30dp"/>
<solid android:color="#ffffff"/>
<stroke android:width="0.5dp" android:color="#dddddd" />
</shape>
在activity的xml中XXXX自己的包名
<com.XXXX.XXXX.XXXXX.TagFlowLayout
android:id="@+id/flowLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingBottom="20dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="10dp"
></com.XXXX.XXXX.XXXXX.TagFlowLayout>
这样一个完整的tagFlowLayout就完成了 此处为多选状态下的效果,如果不需要多选 只需要把drawable下文件状态取消,在onTag的点击事件中完成你想要完成的功能,跳转页面等。