美文网首页Android自定义View
android流式布局实现与源码分析

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

作者: 骑着毛驴追宝马 | 来源:发表于2017-08-01 21:08 被阅读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的使用到源码解读的过程。

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

    相关文章

      网友评论

      本文标题:android流式布局实现与源码分析

      本文链接:https://www.haomeiwen.com/subject/nujdlxtx.html