Android自定义View

android流式布局实现与源码分析

2017-08-01  本文已影响129人  骑着毛驴追宝马

版权声明:本文为博主原创文章,转载请注明出处。

项目地址: 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的使用到源码解读的过程。

文章到此结束,感谢大家的阅读。

上一篇下一篇

猜你喜欢

热点阅读