栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

手写一个淘宝、京东的搜索流式布局FlowLayout

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

手写一个淘宝、京东的搜索流式布局FlowLayout

目录
  • 一些叨叨
  • 继承ViewGrop 实现自定义控件
  • 重写构造器
  • 提供对外接口
  • 测量
  • 摆放
  • 使用方法
  • 完整代码

一些叨叨
  • 市面上所有的app只要有搜索功能,几乎都离不开流式布局,像淘宝、京东、小红书等等。暑假的时候写了一个类似淘宝的app,就用到了这个流式布局。

这个是自己的app实战效果

下面是测试效果

继承ViewGrop 实现自定义控件

自定义ViewGrop有几个关键点,其中测量 、摆放最重要。

第一步当然是继承ViewGroup了

public class FlowLayout extends ViewGroup {

}
重写构造器

继承 ViewGrop需要一些构造方法, 全部写调用自身不同的构造方法达到统一参数入口的目的,谷歌的TextView也是这样写的。这里getXXX就相当于在layout文件中获取定义过的量,没有定义就设置方法中的缺省值。

    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);
        //获取XML代码写的属性
        //xml可以设置一些子控件边距、颜色、点击效果、字体颜色、字体大小等等属性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        mHorizontalMargin = a.getDimension(R.styleable.FlowLayout_itemHorizontalMargin, DEFAULT_HORIZONTAL_MARGIN);
        mVerticalMargin = a.getDimension(R.styleable.FlowLayout_itemVerticalMargin, DEFAULT_VERTICAL_MARGIN);
        mTextMaxLength = a.getInt(R.styleable.FlowLayout_textMaxLength, DEFAULT_TEXT_MAX_LENGTH);
        if(mTextMaxLength!=-1&&mTextMaxLength<=0){
            throw new IllegalArgumentException("max length must not less than 0");
        }
        mMaxLine = a.getInt(R.styleable.FlowLayout_maxLine, DEFAULT_MAX_LINE);
        if(mMaxLine!=-1&&mMaxLine<=0){
            throw new IllegalArgumentException("max line must not less than 0");
        }
        mTextColor = a.getColor(R.styleable.FlowLayout_textColor, getResources().getColor(R.color.black));
        mBorderColor = a.getColor(R.styleable.FlowLayout_textBorderColor, getResources().getColor(R.color.black));
        mBorderRadius = a.getDimension(R.styleable.FlowLayout_borderRadius, DEFAULT_BORDER_RADIUS);
        Log.d(TAG, "FlowLayout: mHorizontalMargin" + mHorizontalMargin + "n" +
                "mVerticalMargin=" + mVerticalMargin + "n" +
                "mTextMaxLength=" + mTextMaxLength + "n" +
                "mTextColor=" + mTextColor + "n" +
                "mBorderColor=" + mBorderColor + "n" +
                "mBorderRadius=" + mBorderRadius);
        a.recycle();
    }

在value包下创建attrs.xml,写上自己想要的属性

    
        
        
        
        
        
        
        
    
提供对外接口

数据通过set方法传进来,内部需要维护一个链表。这里的泛型可以自定义,可以传一个实体类,这里简单起见,仅展示文本。

    public void setTextList(List list) {
        mData.clear();
        mData.addAll(list);
        setUpChildren();
    }

setUpChildren()主要用来更新TextView中展示的文本,以及提供点击事件。一个for循环遍历完所有数据,添加然后创建TextView,添加到ViewGrop中即可。

    private void setUpChildren() {
    	//移除ViewGrop中所有子View
        removeAllViews();
        for (String mDatum : mData) {
            TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.flow_item, this, false);
            textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(mTextMaxLength)});
            Log.d(TAG,"mDatum.length()---------------->"+mDatum.length());
            String finalMDatum = mDatum;
            textView.setText(mDatum);
            textView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onItemClickListener != null) {
                        onItemClickListener.OnItemClick(v, finalMDatum);
                    }
                }
            });
            //添加子View
            addView(textView);
        }
    }

内部维护一个点击事件

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener {
        void OnItemClick(View v, String text);
    }
测量

测量已经注释已经说的很清楚啦。

  //所有行的集合
    private List> lines = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, " in onMeasure");
        int childCount = getChildCount();
        Log.d(TAG, " childCount ========>" + childCount);
        if (childCount == 0) {
            return;
        }
        lines.clear();
        //一行中所有View的集合
        List line = new ArrayList<>();
        //lines持有line的引用,后面操作会直接添加到lines里
        lines.add(line);

        //该控件的父类控件的值
        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
        int childMeasureSpaceWidth = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
        int childMeasureSpaceHeight = MeasureSpec.makeMeasureSpec(parentHeight, MeasureSpec.AT_MOST);

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != VISIBLE) {
                //不可见就进行下一个循环
                continue;
            }
            //测量孩子
            measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);

            //根据xml自定义的属性判断是否需要继续添加行
            if(mMaxLine!=-1&&lines.size()>mMaxLine){
                return;
            }
            if (line.size() == 0) {
                //先添加一个孩子
                line.add(child);
            } else {
                //第二个孩子添加之前需要判断一下是否可以添加
                boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
                Log.d(TAG, "onMeasure: canBeAdd-------------》" + canBeAdd);
                if (canBeAdd) {
                    //可以添加
                    line.add(child);
                } else {
                    //不能添加,重新开一个内存
                    //这里也是一样,line提前被添加到lines中了
                    line = new ArrayList<>();
                    lines.add(line);
                    //当前的孩子还需要添加到下一行
                    i--;
                }
            }
        }

    
    private boolean checkChildCanBeAdd(List line, View child, int parentWidth) {
        //应为line里一定有至少一个TextView
        //先加一个外部定义的paddingleft的值
        int totalSize = getPaddingLeft();
        //再添加一个外部传来TextView的宽度
        totalSize += child.getMeasuredWidth();
        for (View view : line) {
            //这里计算line里所有已经有的TextView宽度
            //一个TextView真实宽度=(外部设置的margin值(两个TextView之间的间距)+自身原本TextView宽度)
            totalSize += view.getMeasuredWidth();
            totalSize += (int) mHorizontalMargin;
        }
        //最后需要加上右边距
        totalSize += getPaddingRight();
        //返回计算好的总宽度totalSize是否小于父亲的宽度
        return totalSize <= parentWidth;
    }
摆放

摆放也算一个简单的算法了吧,对于做过好多算法的你们来说肯定不难理解。
这里直接也看着上面的图,注意,这里开始的时候垂直高度要加paddingTop,同样底边也可以加一下paddingBottom,前面的图应为只需要计算这个控件在哪一行,哪个集合里,所以不需要加垂直方向的padding值。

   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "in onLayout-------------->");
        if (lines.size() == 0) {
            return;
        }
        View firstChild = getChildAt(0);
        //这里定义好一个child的高度
        int aChildHeight = firstChild.getMeasuredHeight();
        //左边初始化0
        int aStartLeft;
        //top初始化设置为paddingTop的高度
        int aStartTop = getPaddingTop();
        
        for (int i = 0; i < lines.size(); i++) {
            //一行的开始肯定是先有左边距啦
            aStartLeft = getPaddingLeft();
            List line = lines.get(i);
            //遍历一行的view
            for (View view : line) {
                //左边位置=aStartLeft
                //上边位置=aStartTop
                //右边位置=aStartLeft+view的宽度
                //下边位置=aStartTop+view的高度
                view.layout(aStartLeft, aStartTop, aStartLeft + view.getMeasuredWidth(),
                        aStartTop + view.getMeasuredHeight());
                //应为存在水平边距,这里右边位置要加上这个边距才是下一个控件起始位置
                aStartLeft += (int) mHorizontalMargin;
                //aStartLeft还没改值
                aStartLeft += view.getMeasuredWidth();
            }
            //处理下一行
            //高度=子控件高度加上Margin值
            aStartTop += aChildHeight;
            aStartTop += (int) mVerticalMargin;
        }
    }
使用方法

xml代码

        
        

这里其他属性就自己写到代码里啦

Activity中:

        flowLayout = findViewById(R.id.flowLayout);
        List list = new ArrayList<>();
        list.add("这是个关键");
        list.add("iPad");
        list.add("Android");
        list.add("数码摄像机");
        list.add("耳机");
        list.add("鼠标");
        list.add("键盘");

        for (int i = 0; i < 5; i++) {
            list.add("关键字" + i);
        }
        flowLayout.setTextList(list);
  
        flowLayout.setOnItemClickListener(new FlowLayout.OnItemClickListener() {
            @Override
            public void OnItemClick(View v, String text) {
                Toast.makeText(getApplicationContext(),"点击了:"+text,Toast.LENGTH_SHORT).show();
            }
        });

效果如下:

整合到自己的业务之后就能有下面的效果啦:

  • 将链表序列化后缓存到SharedPreference中,就能本地保存了。

  • 这里点击一个文字,该文字自动跳到第一个也很好写,直接Collections.reverse(lists)将链表倒置就行啦,

完整代码
package com.lw.tiketunion.ui.custom;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.InputFilter;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.lw.tiketunion.R;
import com.lw.tiketunion.base.App;
import com.lw.tiketunion.utils.LogUtils;
import com.lw.tiketunion.utils.SizeUtils;

import java.util.ArrayList;
import java.util.List;


public class FlowLayout extends ViewGroup {
    private static final String TAG = "FlowLayout";
    private static final int DEFAULT_MAX_LINE = -1;
    private List mData = new ArrayList<>();

    public static final int DEFAULT_BORDER_RADIUS = SizeUtils.dip2px(App.getContext(), 5);
    public static final int DEFAULT_TEXT_MAX_LENGTH = 5;
    //一行中每个View的间距
    private static final int DEFAULT_HORIZONTAL_MARGIN = SizeUtils.dip2px(App.getContext(), 10);
    //每行间距
    private static final int DEFAULT_VERTICAL_MARGIN = SizeUtils.dip2px(App.getContext(), 10);

    private final int mTextColor;
    private float mHorizontalMargin;
    private float mVerticalMargin;
    private int mTextMaxLength;
    private int mBorderColor;
    private float mBorderRadius;
    private OnItemClickListener onItemClickListener;
    private int mMaxLine;

    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);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        mHorizontalMargin = a.getDimension(R.styleable.FlowLayout_itemHorizontalMargin, DEFAULT_HORIZONTAL_MARGIN);
        mVerticalMargin = a.getDimension(R.styleable.FlowLayout_itemVerticalMargin, DEFAULT_VERTICAL_MARGIN);
        mTextMaxLength = a.getInt(R.styleable.FlowLayout_textMaxLength, DEFAULT_TEXT_MAX_LENGTH);
        if (mTextMaxLength != -1 && mTextMaxLength <= 0) {
            throw new IllegalArgumentException("max length must not less than 0");
        }
        mMaxLine = a.getInt(R.styleable.FlowLayout_maxLine, DEFAULT_MAX_LINE);
        if (mMaxLine != -1 && mMaxLine <= 0) {
            throw new IllegalArgumentException("max line must not less than 0");
        }
        mTextColor = a.getColor(R.styleable.FlowLayout_textColor, getResources().getColor(R.color.black));
        mBorderColor = a.getColor(R.styleable.FlowLayout_textBorderColor, getResources().getColor(R.color.black));
        mBorderRadius = a.getDimension(R.styleable.FlowLayout_borderRadius, DEFAULT_BORDER_RADIUS);
        Log.d(TAG, "FlowLayout: mHorizontalMargin" + mHorizontalMargin + "n" +
                "mVerticalMargin=" + mVerticalMargin + "n" +
                "mTextMaxLength=" + mTextMaxLength + "n" +
                "mTextColor=" + mTextColor + "n" +
                "mBorderColor=" + mBorderColor + "n" +
                "mBorderRadius=" + mBorderRadius);
        a.recycle();
    }

    public void setTextList(List list) {
        mData.clear();
        mData.addAll(list);
        setUpChildren();
    }

    public void deleteAllList() {
        mData.clear();
        removeAllViews();
        TextView textView = new TextView(getContext());
        textView.setText("暂无历史记录");
        textView.setTextColor(Color.BLACK);
        addView(textView);
        invalidate();
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener {
        void OnItemClick(View v, String text);
    }

    private void setUpChildren() {
        removeAllViews();
        for (String mDatum : mData) {
            TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.item_flow, this, false);
            textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(mTextMaxLength)});
            Log.d(TAG, "mDatum.length()---------------->" + mDatum.length());
            String finalMDatum = mDatum;
            textView.setText(mDatum);
            textView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onItemClickListener != null) {
                        onItemClickListener.OnItemClick(v, finalMDatum);
                    }
                }
            });
            addView(textView);
        }
    }

    //所有行的集合
    private List> lines = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, " in onMeasure");
        int childCount = getChildCount();
        Log.d(TAG, " childCount ========>" + childCount);
        if (childCount == 0) {
            return;
        }
        lines.clear();
        //一行中所有View的集合
        List line = new ArrayList<>();
        lines.add(line);

        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);

        int childMeasureSpaceWidth = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
        int childMeasureSpaceHeight = MeasureSpec.makeMeasureSpec(parentHeight, MeasureSpec.AT_MOST);

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != VISIBLE) {
                //不可见就进行下一个循环
                continue;
            }
            //测量孩子
            measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);

            if (mMaxLine != -1 && lines.size() > mMaxLine) {
                return;
            }
            if (line.size() == 0) {
                //先添加一个孩子
                line.add(child);
            } else {
                //第二个孩子添加之前需要判断一下是否可以添加
                boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
                Log.d(TAG, "onMeasure: canBeAdd-------------》" + canBeAdd);
                if (canBeAdd) {
                    //可以添加
                    line.add(child);
                } else {
                    line = new ArrayList<>();
                    lines.add(line);
                    i--;
                }
            }
        }
        int finalParentHeight;
        View child = getChildAt(0);
        int measuredHeight = child.getMeasuredHeight();
        Log.d(TAG, "onMeasure:lines.size()--------> " + lines.size());
        finalParentHeight = lines.size() * (measuredHeight + (int) mVerticalMargin);
        setMeasuredDimension(parentWidth, finalParentHeight);
    }

    
    private boolean checkChildCanBeAdd(List line, View child, int parentWidth) {
        int totalSize = getPaddingLeft();
        totalSize += child.getMeasuredWidth();
        for (View view : line) {
            totalSize += view.getMeasuredWidth();
            totalSize += (int) mHorizontalMargin;
        }
        totalSize += getPaddingRight();
        return totalSize <= parentWidth;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "in onLayout-------------->");
        if (lines.size() == 0) {
            return;
        }
        View firstChild = getChildAt(0);
        int aChildHeight = firstChild.getMeasuredHeight();
        int aStartLeft;
        int aStartTop = getPaddingTop();
        for (int i = 0; i < lines.size(); i++) {
            aStartLeft = getPaddingLeft();
            List line = lines.get(i);
            for (View view : line) {
                view.layout(aStartLeft, aStartTop, aStartLeft + view.getMeasuredWidth(),
                        aStartTop + view.getMeasuredHeight());
                aStartLeft += (int) mHorizontalMargin;
                aStartLeft += view.getMeasuredWidth();
            }
            aStartTop += aChildHeight;
            aStartTop += (int) mVerticalMargin;
        }
    }
}

推荐学习资料:

  • 视频:
    https://www.bilibili.com/video/BV1oa4y1E7Fb?spm_id_from=333.999.0.0
    这个up主自定义控件真的讲解的很好,通俗易懂。
  • 其他:
    功能更强大的FlowLayout:https://github.com/jhwsx/BlogCodes/tree/master/FlowLayout
    参考资料
    https://blog.csdn.net/lmj623565791/article/details/38339817
    https://mp.weixin.qq.com/s/jNdy0ol-oB2nQugptEK5wQ
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/572146.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号