流式布局--可展开、限制展示的行数

之前为了图方便,用过很多其他人写的流式布局,也基本满足了当时的要求。但是这次的要求有点多:可展开和收缩,限制行数、最初展示的最多为两行。接到需求第一步,网上找一个满足功能的FlowLayout,确实找到了几个但是都存在不一样的问题。以下就说一下遇到的问题,不知道你遇到过没有:
1.因为每个Item的TextViewwrap_content,所以当文本本身特别长的时候就会出现问题,尤其是当TextView设置了圆角背景的时候,你会发现TextView的右侧圆弧边没有了,因为子控件的实际宽度大于他的父控件。就算你设置ellipsize都没用。
2.数据显示会出现问题,比如展开的时候本应该显示4行,最终却只显示三行,当限制最多显示5行的时候,ImageView会出现单独占据第6行的情况。

废话不多说,直接上代码:
以下分别为两个布局文件

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="@dimen/dp_35"
    android:layout_margin="@dimen/dp_6"
    android:maxLines="1"
    android:ellipsize="end"
    android:background="@drawable/radio18_f8f8f8_bg"
    android:gravity="center_vertical"
    android:paddingLeft="@dimen/dp_12"
    android:paddingRight="@dimen/dp_12"
    android:textColor="@color/black"
    android:textSize="@dimen/sp_14"
    android:textStyle="bold">

</TextView>

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="@dimen/dp_35"
    android:layout_margin="@dimen/dp_4"
    android:background="@drawable/radio18_f8f8f8_bg"
    android:paddingLeft="@dimen/dp_12"
    android:paddingRight="@dimen/dp_12"
    android:src="@mipmap/search_open">

</ImageView>

/**
 * 1.设置最初可展示的行数,如果实际的View超过个行数的时候
 * 显示 “可展开View”, 如果不超过这个行数,则不显示 “可展开View”
 * 2.设置最大可展示的行数,超过的不再展示更多View,不超过的但是查过最
 * 初可展示的行数时,需要显示 “可回收View”
 * 3.两次测量,第一次测量,所有的View,并且知道所有的数据信息,包括一共的行数,每行的个数,这时候展示所有的
 * View,但是这一次不展示给用户看。第二次测量,根据第一次测量的信息,展示View
 */
public class FlowRecordLayout extends ViewGroup {

    private LayoutInflater mInflater;

    //最多可展示的View
    private List<List<View>> mRowViews = new ArrayList<>();

    //当前行的View
    private List<View> currentViewList = new ArrayList<>();

    //记录每行的行高
    private List<Integer> lineHeights = new ArrayList<>();

    /**
     * 图片
     */
    private ImageView imgExpand;

    /**
     * 是否有图片,只有一种情况没有图片
     * 分析几种状态
     * 1.初次测量,添加 {@link ImageView},测量宽度高度,方便计算展开时的位置和收缩时的位置。
     * 2.记录展开时
     * 3.记录收缩时
     * 4.记录不足的数量没有填充慢基础行 (只有这种情况下,才会没有图片)
     */
    private boolean mHasImg;

    /**
     * 是否为初次测量
     */
    private boolean mInitMeasure = true;

    //未展开的时候,ImageView显示的坐标---初步测量的时候,记录的行数超过基础行
    private int closeExpandIndex;

    //展开的时候,ImageView展示的坐标---初步测量的时候,记录超过最大行,当数据超过基础行,但是不超过最大行
    // 的时候这个数一直为 0.所以这个数值并不是指所有的展开时候的位置
    private int openExpandIndex;

    /**
     * 基础可展示的行数,也可以理解为回收状态下最大可展示的行数
     */
    private int mBaseLineCount;

    /**
     * 基础行数固定,也可以通过属性设置 看 mBaseLineCount 属性
     */
    private static final int BASE_LINE = 2;

    /**
     * 标签列表
     */
    private List<SearchRecord> recordList;

    /**
     * 是否超过基础行数
     */
    private boolean isOverBaseLine;

    /**
     * 最多可展示的数,设置为尽量打,则相当于不限制
     */
    private final int mMaxLineCount = 5;

    /**
     * 是否展开
     */
    private boolean expandable;


    public FlowRecordLayout(Context context) {
        this(context, null);
    }

    public FlowRecordLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public FlowRecordLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }


    public void setRecordList(List<SearchRecord> recordList) {
        mRowViews.clear();
        mInitMeasure = true;
        closeExpandIndex = 0;
        openExpandIndex = 0;
        expandable = false;
        this.recordList = recordList;
        mInflater = LayoutInflater.from(getContext());
        removeAllViews();
        for (int i = 0; i < recordList.size(); i++) {
            SearchRecord record = recordList.get(i);
            TextView textChild = (TextView) mInflater.inflate(R.layout.text_search_tag_layout, this, false);
            textChild.setText(CommonUtils.setLimitLength(16, record.getName()));
            addView(textChild);
            int finalI = i;
            textChild.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnTagClickListener != null) {
                        mOnTagClickListener.onTagClick(textChild, finalI);
                    }
                }
            });
        }
        imgExpand = (ImageView) mInflater.inflate(R.layout.img_search_record_tag, this, false);
        addView(imgExpand);
        imgExpand.setImageResource(R.mipmap.search_open);
        imgExpand.setOnClickListener(v -> mOnTagClickListener.onExpandClick(expandable));
        requestLayout();
    }

    public void setRecordList(List<SearchRecord> recordList, boolean hasImg) {
        mInitMeasure = false;
        removeAllViews();
        this.mHasImg = hasImg;
        mInflater = LayoutInflater.from(getContext());
        this.recordList = recordList;
        int index = !hasImg ? recordList.size() : !expandable ? closeExpandIndex : (openExpandIndex > 0) ? openExpandIndex : recordList.size();
        for (int i = 0; i < index; i++) {
            SearchRecord record = recordList.get(i);
            TextView textChild = (TextView) mInflater.inflate(R.layout.text_search_tag_layout, this, false);
            textChild.setText(CommonUtils.setLimitLength(16, record.getName()));
            addView(textChild);
            int finalI = i;
            textChild.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnTagClickListener != null) {
                        mOnTagClickListener.onTagClick(textChild, finalI);
                    }
                }
            });
        }
        if (hasImg) {
            imgExpand = (ImageView) mInflater.inflate(R.layout.img_search_record_tag, this, false);
            addView(imgExpand);
            int resId = expandable ? R.mipmap.search_close : R.mipmap.search_open;
            imgExpand.setImageResource(resId);
            imgExpand.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    mOnTagClickListener.onExpandClick(expandable);
                }
            });

        }
        requestLayout();
    }

    private OnTagClickListener mOnTagClickListener;

    public void setOnTagClickListener(OnTagClickListener onTagClickListener) {
        mOnTagClickListener = onTagClickListener;
    }

    public interface OnTagClickListener {
        void onTagClick(View view, int position);

        void onExpandClick(boolean expandable);
    }


    /**
     * 是否展开
     *
     * @param expandable 是否展开
     */
    public void setExpandable(boolean expandable) {
        removeAllViews();
        mInflater = LayoutInflater.from(getContext());
        mInitMeasure = false;
        mHasImg = true;
        this.expandable = expandable;
        int index = !expandable ? closeExpandIndex : (openExpandIndex > 0) ? openExpandIndex : recordList.size();
        for (int i = 0; i < index; i++) {
            SearchRecord record = recordList.get(i);
            TextView textChild = (TextView) mInflater.inflate(R.layout.text_search_tag_layout, this, false);
            textChild.setText(CommonUtils.setLimitLength(16, record.getName()));
            addView(textChild);
            int finalI = i;
            textChild.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnTagClickListener != null) {
                        mOnTagClickListener.onTagClick(textChild, finalI);
                    }
                }
            });
        }
        imgExpand = (ImageView) mInflater.inflate(R.layout.img_search_record_tag, this, false);
        addView(imgExpand);
        int resId = expandable ? R.mipmap.search_close : R.mipmap.search_open;
        imgExpand.setImageResource(resId);
        imgExpand.setOnClickListener(v -> mOnTagClickListener.onExpandClick(expandable));
    }

    public boolean getExpandable() {
        return expandable;
    }

    public boolean isOverBaseLine() {
        return isOverBaseLine;
    }

    /**
     * 重新方法用来获取子view的margin值
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int top = 0;
        int left = 0;
        int cl, ct, cr, cb;
        for (int i = 0; i < mRowViews.size(); i++) {
            List<View> viewList = mRowViews.get(i);
            for (int j = 0; j < viewList.size(); j++) {
                View child = viewList.get(j);
                MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
                cl = left + params.leftMargin;
                ct = top + params.topMargin;
                cr = cl + child.getMeasuredWidth();
                cb = ct + child.getMeasuredHeight();
                child.layout(cl, ct, cr, cb);
                left = cr + params.rightMargin;
            }
            top += lineHeights.get(i);
            left = 0;
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mRowViews.clear();
        lineHeights.clear();
        currentViewList.clear();
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int modeW = MeasureSpec.getMode(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int modeH = MeasureSpec.getMode(heightMeasureSpec);
        int curLineWidth = 0;
        int curLineHeight = 0;
        int curLineCount = 1;
        int totalHeight = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            int leftMargin = params.leftMargin;
            int topMargin = params.topMargin;
            int rightMargin = params.rightMargin;
            int bottomMargin = params.bottomMargin;
            int childViewWidth = childWidth + leftMargin + rightMargin;
            int childViewHeight = childHeight + topMargin + bottomMargin;
            // TODO: 2021-08-26   判断子控件的宽是否大于父控件的宽,当大于父控件的宽度时,子控件、
            //  显示不完整 ,所以需要重新设置宽度,同时子控件的内容应该添加 android:ellipsize="end" 属性
            if (childViewWidth > width) {
                LayoutParams param = child.getLayoutParams();
                params.width = width - params.leftMargin - params.rightMargin;
                child.setLayoutParams(param);
                childViewWidth = params.width + params.leftMargin + params.rightMargin;
            }
            // TODO: 2021-08-27   测量基础行队尾添加 ImageView 的情况和限制行队尾添加 ImageView 的情况。
            if (curLineWidth + childViewWidth > width) {
                // TODO: 2021-08-27   1.标注特殊行   基础行、限制行
                if (curLineCount == BASE_LINE && mInitMeasure) {  //基础行并且处于第一次测量阶段
                    ImageView imgChild = (ImageView) getChildAt(childCount - 1);
                    measureChild(imgChild, widthMeasureSpec, heightMeasureSpec);
                    MarginLayoutParams imgParams = (MarginLayoutParams) child.getLayoutParams();
                    int imgSpaceWidth = imgChild.getMeasuredWidth() + imgParams.leftMargin + imgParams.rightMargin;
                    closeExpandIndex = imgSpaceWidth + curLineWidth > width ? i - 1 : i;
                } else if (curLineCount == mMaxLineCount && mInitMeasure) {  //限制行并且处于第一次测量阶段
                    ImageView imgChild = (ImageView) getChildAt(childCount - 1);
                    measureChild(imgChild, widthMeasureSpec, heightMeasureSpec);
                    MarginLayoutParams imgParams = (MarginLayoutParams) child.getLayoutParams();
                    int imgSpaceWidth = imgChild.getMeasuredWidth() + imgParams.leftMargin + imgParams.rightMargin;
                    openExpandIndex = imgSpaceWidth + curLineWidth > width ? i - 1 : i;
                }
                // TODO: 2021-08-27   换行
                totalHeight += curLineHeight;
                mRowViews.add(currentViewList);
                lineHeights.add(curLineHeight);
                currentViewList = new ArrayList<>();
                currentViewList.add(child);
                curLineHeight = childViewHeight;
                curLineWidth = childViewWidth;
                curLineCount++;
            } else {
                curLineHeight = Math.max(curLineHeight, childViewHeight);
                curLineWidth += childViewWidth;
                currentViewList.add(child);
            }
            if (i == childCount - 1) {
                totalHeight += curLineHeight;
                lineHeights.add(curLineHeight);
                mRowViews.add(currentViewList);
            }
        }
        mHasImg = mInitMeasure && mRowViews.size() > BASE_LINE && !(mRowViews.get(BASE_LINE).get(0) instanceof ImageView);
        isOverBaseLine = mHasImg;
        // 宽度可以不用考虑 主要考虑高度
        if (modeH == MeasureSpec.EXACTLY) {
            setMeasuredDimension(width, height);
        } else {
            setMeasuredDimension(width, totalHeight);
        }
    }
}


@BindView(R.id.flow_layout_search_record)
FlowRecordLayout mZFlowLayout;

private void initZFlowLayout() {
        mZFlowLayout.setRecordList(keyList);
        mZFlowLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mZFlowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                mZFlowLayout.setRecordList(keyList, mZFlowLayout.isOverBaseLine());
            }
        });
        mZFlowLayout.setOnTagClickListener(new FlowRecordLayout.OnTagClickListener() {
            @Override
            public void onTagClick(View view, int position) {
                EventBus.getDefault().postSticky(new SearchKeyChangeEvent(keyList.get(position).getName()));
            }

            @Override
            public void onExpandClick(boolean expandable) {
                mZFlowLayout.setExpandable(!expandable);
            }
        });
    }

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android中的流式布局是一种常用的布局方式,它可以根据内容的大小和数量自动调整控件的位置和大小,使得界面能够自适应屏幕的宽度。通常在需要展示多个标签、图片或文字等的场景下使用。 流式布局的特点是将内容按照先后顺序从左到右排列,当一行的宽度不足以容纳下一个控件时,会自动换行。这种布局方式能够节省空间,提高界面的可读性和美观性。 在Android中,可以使用FlowLayout这个第三方库来实现流式布局。使用FlowLayout的步骤如下:首先在项目的build.gradle文件中添加依赖,然后在布局文件中将根布局设置为FlowLayout,并在其中添加需要展示的控件,可以通过调整控件的属性来定义布局的样式和排列方式。 流式布局可以动态调整控件的位置和大小,可以通过设置权重来控制每个控件在水平方向上的占比,也可以设置边距来调整控件之间的间隔。另外,流式布局还可以为每个控件设置点击事件和长按事件,方便实现更丰富的交互效果。 总之,流式布局是一种灵活且强大的布局方式,可以有效地解决多个控件在界面上排列不下或排列不美观的问题,同时也能够提高界面的可读性和用户体验。在开发Android应用时,如果遇到需要展示多个标签、图片或文字等的场景,流式布局是一个很好的选择。 ### 回答2: Android流式布局是一种灵活的布局方式,用于在屏幕上动态自适应地显示一系列视图。它能够根据子视图的大小和屏幕大小自动调整子视图的位置和宽度。这种布局方式适用于显示不规则大小的子视图,尤其适用于显示标签、图片、标签云等。 Android流式布局可以通过使用LinearLayout或GridLayout来实现。在LinearLayout中,可以设置orientation属性为horizontal或vertical来实现水平或垂直流式布局。在GridLayout中,可以通过设置列数来控制每行显示的子视图数量。 Android流式布局的优点是可以根据屏幕的大小和方向自动调整子视图的布局,使得页面在不同设备上都能够良好地显示。同时,它也提供了更好的用户体验,因为用户可以在不同屏幕上以不同的方式查看和交互。 然而,Android流式布局也存在一些限制。由于其自适应特性,子视图的大小和位置可能会受到限制。此外,较复杂的布局可能会导致性能问题,因为在布局过程中需要进行多次测量和计算。因此,在使用流式布局时,需要谨慎处理子视图的大小和数量,以提高性能并避免布局过于复杂。 总结来说,Android流式布局是一种灵活而自适应的布局方式,适用于显示不规则大小的子视图。它可以根据屏幕的大小和方向自动调整子视图的布局,并提供更好的用户体验。然而,需要注意处理子视图的大小和数量,以提高性能并避免布局过于复杂。 ### 回答3: Android流式布局(Flow Layout)是一种动态适应屏幕宽度的布局方式,主要用于解决在屏幕上按行排列多个子视图的问题。 在传统的线性布局中,如果视图超出屏幕宽度,就会自动换行,但是每一行只会放置一个子视图。而在流式布局中,子视图会根据屏幕宽度自动换行,并且每一行可以放置多个子视图,适应屏幕不同宽度的设备。 流式布局的使用非常方便,只需要将子视图添加流式布局中即可。它提供了一些属性来控制子视图在布局中的排列方式,比如子视图之间的间距、子视图的对齐方式等。此外,流式布局还可以通过设置权重属性,实现子视图的均匀分布或者按比例分布。 流式布局在一些场景下非常有用,比如在标签云、瀑布流展示等需要动态调整子视图排列的情况下。相比于其他布局方式,流式布局可以更好地利用屏幕空间,提高用户体验。 总之,Android流式布局是一种动态适应屏幕宽度的布局方式,可以方便地排列多个子视图,并提供了一些属性来控制子视图的排列方式和样式。它的使用简单灵活,适用于多种场景,可以有效地提高用户界面的可用性和美观性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值