美文网首页android技术
Android 替身法实现折叠流式布局

Android 替身法实现折叠流式布局

作者: 坑逼的严 | 来源:发表于2021-10-25 18:07 被阅读0次
    1635156335588.gif

    之前写了一篇折叠流式布局,bug有点多,也不好改,究其原因就是写的逻辑太多,改起来不方便,毕竟主体逻辑不是自己写的,基于别人的改总是怪怪的。那么,我就想想这个东西的难点在哪?有什么简单的方法解决?

    难点

    我们的需求是:流式布局展示,当数量没超过两行,那么就不加入展开与收起按钮,如果超过两行但小于等于4行,在收起状态时加入展开按钮,在展开状态展示收起按钮,如果超过4行,在收起状态时加入展开按钮,在展开状态最大4行的最后展示收起按钮。

    折叠一个流式布局,在于加入一个子view的时候,要提前知道折叠的位置。

    比如流式布局在折叠状态时,加入一个子view后,我们要知道他有没有超过两行,超过了,我们需要知道第二行的最后一个index是多少,然后在这个位置插入向下按钮。

    再比如,流式布局不在折叠状态时,加入一个子view,我们要判断他是否在1行到4行之间,如果在,那么他后面一定要加一个向上按钮,因为他是展开的,一定要有一个收起按钮。如果超过4行,那么我们需要知道第4行最后一个按钮。

    当然开发中还发现一个问题,那就是加入一个子view后当前刚好是展开状态的第4行,那么加入收起按钮的时候,我们需要判断当前剩余的宽度够不够我们加入向上按钮,够的话,我们index插入 位置直接返回所有子view的大小,如果不能,那么我们返回所有所有子view的大小 -1 。因为如果我们还返回所有子view的大小的话,就会排到第5行。

    思路

    怎么提前知道要插入的位置呢?前一篇文章是用一个view,在onMeasure里面写了一大堆逻辑去写。现在换一个思路,我们做两个view,装在一个布局里面,一个view(A)是专门用于计算插入位置,另一个view(B)是专门展示数据。当A加入所有的子view后,我们能很快的知道我们需要的index,加入这个index为7,那么在B里面我们就只要装0到6的子view,最后7就变成收起或展开按钮。A就是我们的替身。缺点就是如果子view很大很大,那么就会超市或者很慢。

    代码

    包裹两个子view的布局FlowContentLayout

    public class FlowContentLayout extends RelativeLayout {
    
       private FlowLayout mBackFlowLayout;
    
       private int mLastIndex = 0;
       private FlowLayout mFontFlowLayout;
       private List<String> list = new ArrayList<>();
       private View upView;
       private View downView;
    
    
       public FlowContentLayout(Context context) {
           this(context,null);
       }
    
       public FlowContentLayout(Context context, AttributeSet attrs) {
           this(context, attrs,0);
       }
    
       public FlowContentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
           super(context, attrs, defStyleAttr);
           inflate(context, R.layout.flow_content_layout,this);
           upView = LayoutInflater.from(context).inflate(R.layout.view_item_fold_up, this, false);
           upView.setOnClickListener(new OnClickListener() {
               @Override
               public void onClick(View view) {
                   mBackFlowLayout.setFoldState(true);
                   mFontFlowLayout.setFoldState(true);
                   refreshViews();
               }
           });
           downView = LayoutInflater.from(context).inflate(R.layout.view_item_fold_down, this, false);
           downView.setOnClickListener(new OnClickListener() {
               @Override
               public void onClick(View view) {
                   mBackFlowLayout.setFoldState(false);
                   mFontFlowLayout.setFoldState(false);
                   refreshViews();
               }
           });
           mBackFlowLayout = findViewById(R.id.mFlowLayout);
           mBackFlowLayout.setFlowContentLayout(this);
           mFontFlowLayout = findViewById(R.id.mFontFlowLayout);
           mFontFlowLayout.setUpFoldView(upView);
           mFontFlowLayout.setDownFoldView(downView);
       }
    
    
       @Override
       protected void onDetachedFromWindow() {
           super.onDetachedFromWindow();
           mBackFlowLayout.setFlowContentLayout(null);
       }
    
       /**
        * 这里把隐藏的幕后计算布局加入view先计算
        * @param list
        */
       public void addViews(@NotNull List<String> list) {
           mLastIndex = 0;
           this.list.clear();
           this.list.addAll(list);
           mBackFlowLayout.addViews(list);
       }
    
       /**
        * 相同的数据重新刷新
        */
       private void refreshViews(){
           if(list != null && list.size() > 0){
               mLastIndex = 0;
               mBackFlowLayout.addViews(list);
           }
       }
    
       /**
        * 幕后布局计算后的最大折叠位置
        * @param foldState
        * @param index
        * @param flag 是否需要加入向上或者向下按钮
        * @param lineWidthUsed
        */
       public void foldIndex(boolean foldState, int index, boolean flag, int lineWidthUsed) {
           if(mLastIndex != index){//防止多次调用
               mLastIndex = index;
               //添加外部真正的布局
               if(flag){
                   List<String> list = new ArrayList<>();
                   for (int x = 0; x < index; x++) {
                       list.add(FlowContentLayout.this.list.get(x));
                   }
                   list.add("@@");
                   mFontFlowLayout.addViews(list);
               }else{
                   List<String> list = new ArrayList<>();
                   for (int x = 0; x < FlowContentLayout.this.list.size(); x++) {
                       list.add(FlowContentLayout.this.list.get(x));
                   }
                   mFontFlowLayout.addViews(list);
               }
           }
       }
    
       public int getUpViewWidth() {
           if(upView != null){
               return Utils.getViewWidth(upView);
           }
           return 0;
       }
    
       /**
        * 删除全部后转态恢复
        */
       public void releaseState(){
           mBackFlowLayout.setFoldState(true);
           mFontFlowLayout.setFoldState(true);
       }
    }
    
    

    流式布局FlowLayout

    public class FlowLayout extends ViewGroup {
    
        /**
         * 水平距离
         */
        private int mHorizontalSpacing = Utils.dp2px(8f);
    
        private static final int MAX_LINE = 3;//从0开始计数
        private static final int MIN_LINE = 1;//从0开始计数
        private FlowContentLayout mFlowContentLayout;
        private boolean foldState = true;
        private View upFoldView;
        private View downFoldView;
        private int mWidth;
        private int textViewHeight;
    
        public FlowLayout(Context context) {
            this(context, null);
        }
    
        public FlowLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public void setFlowContentLayout(FlowContentLayout mFlowContentLayout) {
            this.mFlowContentLayout = mFlowContentLayout;
        }
    
        public void setFoldState(boolean foldState) {
            this.foldState = foldState;
        }
    
        public void setUpFoldView(View upFoldView) {
            this.upFoldView = upFoldView;
        }
    
        public void setDownFoldView(View downFoldView) {
            this.downFoldView = downFoldView;
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mWidth = getWidth();
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            //获取mode 和 size
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            final int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight();
            //判断如果布局宽度抛去左右padding小于0,也不能处理了
            if (layoutWidth <= 0) {
                return;
            }
    
            //这里默认宽高默认值默认把左右,上下padding加上
            int width = getPaddingLeft() + getPaddingRight();
            int height = getPaddingTop() + getPaddingBottom();
    
            //初始一行的宽度
            int lineWidth = 0;
            //初始一行的高度
            int lineHeight = 0;
    
            //测量子View
            measureChildren(widthMeasureSpec, heightMeasureSpec);
    
            int[] wh = null;
            int childWidth, childHeight;
            //行数
            int line = 0;
            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                final View view = getChildAt(i);
                //这里需要先判断子view是否被设置了GONE
                if (view.getVisibility() == GONE) {
                    continue;
                }
                childWidth = view.getMeasuredWidth();
                childHeight = view.getMeasuredHeight();
    
                //第一行
                if (i == 0) {
                    lineWidth = getPaddingLeft() + getPaddingRight() + childWidth;
                    lineHeight = childHeight;
                } else {
                    //判断是否需要换行
                    //换行
                    if (lineWidth + mHorizontalSpacing + childWidth > widthSize) {
                        line++;//行数增加
                        // 取最大的宽度
                        width = Math.max(lineWidth, width);
                        //重新开启新行,开始记录
                        lineWidth = getPaddingLeft() + getPaddingRight() + childWidth;
                        //叠加当前高度,
                        height += lineHeight;
                        //开启记录下一行的高度
                        lineHeight = childHeight;
                        if(mFlowContentLayout != null){
                            if(foldState && line > MIN_LINE){
                                callBack(foldState,i-1, true,lineWidth);
                                break;
                            }else if(!foldState && line > MAX_LINE){
                                callBack(foldState,i-1, true,lineWidth);
                                break;
                            }
                        }
                    }
                    //不换行
                    else {
                        lineWidth = lineWidth + mHorizontalSpacing + childWidth;
                        lineHeight = Math.max(lineHeight, childHeight);
                    }
                }
                // 如果是最后一个,则将当前记录的最大宽度和当前lineWidth做比较
                if (i == count - 1) {
                    width = Math.max(width, lineWidth);
                    height += lineHeight;
                }
            }
            //根据计算的值重新设置
            if(mFlowContentLayout == null){
                setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
                        heightMode == MeasureSpec.EXACTLY ? heightSize : height);
            }else{
                setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
                        0);
            }
    
            if(foldState && (line >= 0 && line <= MIN_LINE)){
                callBack(foldState,getChildCount(),false,lineWidth);
            }
            if(!foldState && (line >= 0 && line <= MAX_LINE)){
                if(mFlowContentLayout != null){
                    int upViewWidth = mFlowContentLayout.getUpViewWidth() + mHorizontalSpacing;
                    if(lineWidth > (mWidth - upViewWidth) && line == MAX_LINE){
                        callBack(foldState,getChildCount() - 1,true,lineWidth);
                    }else{
                        callBack(foldState,getChildCount(),true,lineWidth);
                    }
                }else{
                    callBack(foldState,getChildCount(),true,lineWidth);
                }
    
            }
        }
    
        /**
         * 超过最大数的回调
         * @param foldState
         * @param index 最大数的位置。
         * @param b
         * @param lineWidthUsed
         */
        private void callBack(boolean foldState, int index, boolean b, int lineWidthUsed) {
            if(mFlowContentLayout != null){
                mFlowContentLayout.foldIndex(foldState,index,b,lineWidthUsed);
            }
        }
    
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            final int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            if (layoutWidth <= 0) {
                return;
            }
            int childWidth, childHeight;
            //需要加上top padding
            int top = getPaddingTop();
            final int[] wh = getMaxWidthHeight();
            int lineHeight = 0;
            int line = 0;
            //左对齐
            //左侧需要先加上左边的padding
            int left = getPaddingLeft();
            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                final View view = getChildAt(i);
                //这里一样判断下显示状态
                if (view.getVisibility() == GONE) {
                    continue;
                }
                //自适宽高
                childWidth = view.getMeasuredWidth();
                childHeight = view.getMeasuredHeight();
                //第一行开始摆放
                if (i == 0) {
                    view.layout(left, top, left + childWidth, top + childHeight);
                    lineHeight = childHeight;
                } else {
                    //判断是否需要换行
                    if (left + mHorizontalSpacing + childWidth > layoutWidth + getPaddingLeft()) {
                        line++;
                        //重新起行
                        left = getPaddingLeft();
                        top = top + lineHeight;
                        lineHeight = childHeight;
                    } else {
                        left = left + mHorizontalSpacing;
                        lineHeight = Math.max(lineHeight, childHeight);
                    }
                    view.layout(left, top, left + childWidth, top + childHeight);
                }
                //累加left
                left += childWidth;
            }
        }
    
        /**
         * 取最大的子view的宽度和高度
         *
         * @return
         */
        private int[] getMaxWidthHeight() {
            int maxWidth = 0;
            int maxHeight = 0;
            for (int i = 0, count = getChildCount(); i < count; i++) {
                final View view = getChildAt(i);
                if (view.getVisibility() == GONE) {
                    continue;
                }
                maxWidth = Math.max(maxWidth, view.getMeasuredWidth());
                maxHeight = Math.max(maxHeight, view.getMeasuredHeight());
            }
            return new int[]{maxWidth, maxHeight};
        }
    
        public void addViews(List<String> list){
            removeAllViews();
            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            for (int x = 0; x< list.size(); x++) {
                String s = list.get(x);
                if(TextUtils.equals("@@",s)){
                    if(foldState){
                        if(downFoldView != null){
                            Utils.removeFromParent(downFoldView);
                            addView(downFoldView,layoutParams);
                        }
                    }else{
                        if(upFoldView != null){
                            Utils.removeFromParent(upFoldView);
                            addView(upFoldView,layoutParams);
                        }
                    }
                }else{
                    addTextView(s,layoutParams);
                }
    
            }
        }
    
    
    
        private void addTextView(String s,LinearLayout.LayoutParams layoutParams){
            LinearLayout linearLayout = new LinearLayout(getContext());
            linearLayout.setPadding(0,Utils.dp2px(8f),0,0);
            linearLayout.setLayoutParams(layoutParams);
            TextView tv = new TextView(getContext());
            tv.setPadding(Utils.dp2px(12f), Utils.dp2px(8f), Utils.dp2px(12f), Utils.dp2px(8f));
            tv.setText(s);
            tv.setSingleLine();
            tv.setTextSize(TypedValue.COMPLEX_UNIT_SP,12);
            tv.setTextColor(getResources().getColor(R.color.ff666666));
            tv.setEllipsize(TextUtils.TruncateAt.END);
            tv.setBackgroundResource(R.drawable.search_tag_bg);
            linearLayout.addView(tv,new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
            addView(linearLayout,layoutParams);
            textViewHeight = Utils.getViewHeight(tv);
        }
    }
    
    
    

    最后activity里面只要往里面加入String集合就行

    mFlowContentLayout?.addViews(list)
    

    当需要清空所有数据,重新加入数据时不止String集合需要清空,也需要调用FlowContentLayout的releaseState方法还原他的收起展开状态。

    flow_content_layout布局代码填一下

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <com.laiyifen.search2.flowLayout.FlowLayout
            android:id="@+id/mFlowLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="invisible"/>
        <com.laiyifen.search2.flowLayout.FlowLayout
            android:id="@+id/mFontFlowLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </RelativeLayout>
    

    对了在实际运用中,我把这个布局放在了列表的头部,导致会调用他的detach方法,导致不能回调,所以注释掉下面的方法

    @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            mBackFlowLayout.setFlowContentLayout(null);
        }
    

    自己独立封装,在activity销毁时自己调用释放。

    相关文章

      网友评论

        本文标题:Android 替身法实现折叠流式布局

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