android流式布局实现与源码分析
版权声明:本文为博主原创文章,转载请注明出处。
项目地址: https://github.com/hongyangAndroid/FlowLayout
废话不多少,我们先来看一张效果图:
image.png要实现图片中的布局效果,我们该如何操作呢?我们直接看代码:
final String[] tags = new String[]{"item1", "item2", "item3",
"item4", "item5", "item6", "item7", "item8",
"item9", "item10", "item11", "item12", "item13",
"item14", "item15", "item16", "item17", "item18",
"item19", "item20", "item21", "item22", "item23",
"item24", "item25", "item26", "item27", "item28",
"item29", "item30", "item31", "item32", "item33"
};
private TagFlowLayout mFlowLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final LayoutInflater mInflater = LayoutInflater.from(this);
mFlowLayout = (TagFlowLayout) findViewById(R.id.id_flowlayout);
mFlowLayout.setAdapter(new TagAdapter<String>(tags) {
@Override
public View getView(com.zhy.view.flowlayout.FlowLayout parent, int position, String s) {
TextView tv =(TextView)mInflater.inflate(R.layout.tag_item,
mFlowLayout, false);
tv.setText(s);
return tv;
}
// 为标签设置预点击内容(就是一开始就处于点击状态的标签)
@Override
public boolean setSelected(int position, String s) {
return s.equals("item10");
}
});
// 为点击标签设置点击事件.
mFlowLayout.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() {
@Override
public boolean onTagClick(View view, int position, com.zhy.view.flowlayout.FlowLayout parent) {
Toast.makeText(MainActivity.this, tags[position], Toast.LENGTH_SHORT).show();
return true;
}
});
// 点击标签时,回传所有已选中标签
mFlowLayout.setOnSelectListener(new TagFlowLayout.OnSelectListener() {
@Override
public void onSelected(Set<Integer> selectPosSet) {
Log.e("jacky", "choose:" + selectPosSet.toString());
}
});
}
有没有熟悉的感觉,对,其实跟我们平时使用的布局设置适配器达到的显示效果差不多,只不过把以前的列表效果换成了流式布局,将对itemClick的点击效果替换成了tagClick而已,使用起来很简单。至于选中后的效果,当然我们可以自己在XML文件中定义选中状态,相信这个不会难倒大家的。
想必到这里大家基本上已经知道怎么使用了,接下来我们来看看源码里面是怎么实现的。首先从我们布局开始看起,而TagFlowLayout又是继承自FlowLayout,那我们先来看看FlowLayout中是如何实现的:
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);}}
从代码中我们可以看到,该控件是一个继承自ViewGroup的自定义view,而自定义View的流程这里不做深究,这里主要实现了onMeasure()与onLayout()函数,而在onMeasure中,首先通过
循环对width = Math.max(lineWidth, width)不断求最大值:
lineWidth为lineWidth += childWidth;当需要换行是,即lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()时,lineWidth = childWidth(即该行第一个view的宽度),而height 也类似,那么到这里我们知道,lineWidth保存了当前行所有child view占父view(当前view)的最大宽度,所以可以得出width始终为最大宽度,而height为子view的占父的最大高度,从最后一行代码可以看到,setMeasuredDimension(
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()
);到这里是不是恍然大悟,是的,其实作者目的很明确,就是为了根据mode模式设置当前view的宽和高的。
接下来我们看一下onLayout中的实现:
首先看到在循环中有这么一条判断语句:
if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) {
...
}
从代码中我们很容易知道,childWidth表示child view的宽度,lineWidth又表示什么呢?
我们不妨向下继续看:
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
到这里我们大概知道了,lineWidth表示每个子view以及它的margin累加,所以这是一个不断计算并累加view宽度的值(暂且认为是计算行宽度的值),然后回过头来看,就很容易知道if里面判断的是累加之后的lineWidth再加上childWidth + lp.leftMargin + lp.rightMargin与
width - getPaddingLeft() - getPaddingRight()的判断,我们很容易知道这是计算当前view(父view)的宽度的值(也可理解为所能容纳子view的最大宽度);当子view宽度大于这个值时,当然就要换行啦,到这里我们就差不多可以知道,mLineHeight保存了每行的高度,而mLineWidth保存了每行的宽度,lineViews存储了每行的所有view,而mAllViews存储的是每个lineViews对象,那么这几个关键变量我们就搞清楚了,我们继续向下看:
首先是一个for (int i = 0; i < lineNum; i++)很明显是按行遍历:
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;
}
结合mLineWidth和mGravity参数可以得出结论,这里主要是为了得到view的left值。
而for (int j = 0; j < lineViews.size(); j++)是遍历每行中的view,并通过
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;
并计算出每行中view的left,top,right与bottom信息,然后调用了child.layout(lc, tc, rc, bc);到这里我们基本上理清了,这个onlayout主要是为了计算每个child view的layout的坐标,并将child view放置在正确的位置上。
到这里我们基本上搞清楚了FlowLayout主要是完成流式布局相关的测量与child view位置计算的。
然后我们继续来看下FlowLayout子类TabFlowLayout中的实现:
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)
{
//if (mTagAdapter == adapter)
// return;
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());
tagView.setDuplicateParentStateEnabled(true);
if (tagView.getLayoutParams() != null)
{
tagViewContainer.setLayoutParams(tagView.getLayoutParams());
} else
{
MarginLayoutParams lp = new MarginLayoutParams(LayoutParams.WRAP_CONTENT, 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);
} }
当我们使用为TabFlowLayout设置adapter时,会调用到changeAdapter(),其实核心功能就这两句:
tagViewContainer.addView(tagView);
addView(tagViewContainer);
将adapter的getview中获取的view对象不断添加到TabFlowLayout中,并设置其选中状态,到这里基本上已经可以完成布局的流式显示了。最后让我们来分析下事件监听是如何处理的:
先来看下OnSelectListener事件,主要在doSelect()中处理的回调,而doSelect()是在performClick()调用的,那么就意味着只要有item的点击事件,就会回调OnSelectListener监听并将mSelectedView传回供开发者进行数据处理,同理performClick()也对mOnTagClickListener监听进行了处理,主要用来处理单个点击事件,到这里基本上完成了对FlowLayout的分析。
至于TagView我们需要知道的是TagView其实代表的是我们在布局中定义的item,作者在代码中通过TagAdapter的getview将item转化成了TagView,并通过checkable接口设置tagView的选中状态。
tagView与TagView的源码比较简单,读者可自行阅读TagView与TagAdapter源码。
到这里基本上完成了从FlowLayout的使用到源码解读的过程。
文章到此结束,感谢大家的阅读。