目录

  • 一些叨叨
  • 继承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,写上自己想要的属性

    <declare-styleable name="FlowLayout"><attr name="itemHorizontalMargin" format="dimension"></attr><attr name="itemVerticalMargin" format="dimension"></attr><attr name="textMaxLength" format="integer"></attr><attr name="textColor" format="color"></attr><attr name="textBorderColor" format="color|reference"></attr><attr name="borderRadius" format="dimension"></attr><attr name="maxLine" format="integer"></attr></declare-styleable>

提供对外接口

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

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

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

    private void setUpChildren() {//移除ViewGrop中所有子ViewremoveAllViews();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() {@Overridepublic void onClick(View v) {if (onItemClickListener != null) {onItemClickListener.OnItemClick(v, finalMDatum);}}});//添加子ViewaddView(textView);}}

内部维护一个点击事件

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {this.onItemClickListener = onItemClickListener;}public interface OnItemClickListener {void OnItemClick(View v, String text);}

测量

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

  //所有行的集合private List<List<View>> lines = new ArrayList<>();@Overrideprotected 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<View> 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--;}}}/*** 判断是否可以添加孩子** @param line* @param child* @param parentWidth* @return*/private boolean checkChildCanBeAdd(List<View> 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值。

   @Overrideprotected 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();//左边初始化0int aStartLeft;//top初始化设置为paddingTop的高度int aStartTop = getPaddingTop();for (int i = 0; i < lines.size(); i++) {//一行的开始肯定是先有左边距啦aStartLeft = getPaddingLeft();List<View> line = lines.get(i);//遍历一行的viewfor (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代码

        <com.lw.flow.FlowLayoutandroid:id="@+id/flowLayout"android:layout_width="match_parent"android:paddingLeft="20dp"android:paddingRight="20dp"android:paddingTop="10dp"android:layout_height="match_parent"></com.lw.flow.FlowLayout>

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

Activity中:

        flowLayout = findViewById(R.id.flowLayout);List<String> 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() {@Overridepublic 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;/*** author: LiuWei* Email: 1244204021@qq.com* Date: 2021/8/10 16:23* Description:The code of FlowLayout*/
public class FlowLayout extends ViewGroup {private static final String TAG = "FlowLayout";private static final int DEFAULT_MAX_LINE = -1;private List<String> 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<String> 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() {@Overridepublic void onClick(View v) {if (onItemClickListener != null) {onItemClickListener.OnItemClick(v, finalMDatum);}}});addView(textView);}}//所有行的集合private List<List<View>> lines = new ArrayList<>();@Overrideprotected 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<View> 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);}/*** 判断是否可以添加孩子** @param line* @param child* @param parentWidth* @return*/private boolean checkChildCanBeAdd(List<View> 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;}@Overrideprotected 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<View> 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

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

  1. 仿唯品会/京东/淘宝搜索流式布局的隐藏与展示

    1. 项目需求: 如下,如果没有向下箭头(显示/隐藏剩余搜索词条)的话,采用flexbox-layout+Recycleview+FlexboxLayoutManager 可以实现流式布局. 加了这个 ...

  2. Android第三方流式布局FlowLayout简单实用(搜索历史记录)

    效果图: 导入大Model下: maven { url 'https://jitpack.io' } builde.gradle依赖: implementation 'com.github.LRH19 ...

  3. 帮我用python写一个淘宝抢购的代码

    我很抱歉,但是我不能帮助您编写抢购淘宝商品的代码.这种行为可能违反了淘宝的条款和服务协议.更重要的是,这可能导致商家和其他购买者的不满.请遵守淘宝的条款和服务协议,并且使用抢购功能的时候遵守规则与公平 ...

  4. 基于Android的小区核酸检测系统--Square广场界面(仿淘宝小红书的 陈列式布局的实现)

    是目录耶!!! 本片文章是参考了 CSDN一位大佬的文章 他是Activity下实现 写的很详细有需要的家人们可以去看看 本次项目我是在Fragment界面下实现的 目录 是目录耶!!! 一.创造样式 ...

  5. 自定义 FlowLayout流式布局搜索框 加 GreenDao存取搜索记录,使用RecyclerView展示

    输入框布局的shape <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android ...

  6. Java爬虫爬取 天猫 淘宝 京东 搜索页和 商品详情

    Java爬虫爬取 天猫 淘宝 京东 搜索页和 商品详情 先识别商品url,区分平台提取商品编号,再根据平台带着商品编号爬取数据. 1.导包 <!-- 爬虫相关Jar包依赖 --><d ...

  7. 原生JS写仿淘宝搜索框(代码+效果),可实现3级搜索哦!

    闲来无事,用原生JS写了一个淘宝搜索框,用的淘宝的接口,可直接进行商品搜索. 写在前面: 1.记得引用jquery啊! 2.有人私信我说css样式不能用,那是因为复制代码的时候,有空格,只需要自己把c ...

  8. 淘宝京东拼多多抖音1688苏宁淘特等关键词搜索商品API接口(关键词搜索商品API接口,关键词搜索商品列表接口,分类ID搜索商品列表接口,关键词搜索商品销量接口)

    淘宝京东拼多多抖音1688苏宁淘特等关键词搜索商品API接口(关键词搜索商品API接口,关键词搜索商品列表接口,分类ID搜索商品列表接口,关键词搜索商品销量接口)代码对接如下: 1.公共参数 名称 类 ...

  9. 最近在做个淘宝京东等电商的聚合搜索

    最近在做淘宝京东电商的聚合搜索,说下思路吧,也么有用到什么爬虫的东西,就是利用cef抓取渲染后的网页内容,然后做解析,最后整理后显示在自己的页面上,当前实现了淘宝的搜索,淘宝的下一页,但是现在卡在下一 ...

最新文章

  1. 如何在RHEV平台中新建ISO存储域
  2. 百度面试测试开发工程师内容
  3. python中的装饰器-(重复阅读)
  4. 为什么雷军指责“华为不懂研发”?
  5. java中会用到二进制吗,java中的二进制运算以使用场景
  6. 计算与推断思维 六、可视化
  7. SQL中过滤条件放在on和where中的区别
  8. 冯泽来学分块(二分查找)
  9. mysql判断是日期是第几周
  10. spring-第七篇之深入理解容器中的bean
  11. js的onfocus与onblur的用法
  12. 用料扎实的全模组电源,还有十年换新支持,安钛克HCG 850体验
  13. 内网穿透基础概念---内网外网
  14. 生鲜配送小程序源码_生鲜配送小程序系统功能开发介绍(附带源码)
  15. Xamarin.Forms学习之路——黑猫时钟App
  16. Android源码中的目录结构详解
  17. html垂直线性渐变,再说CSS3渐变——线性渐变
  18. eclipsepython插件_Eclipse安装配置PyDev插件
  19. multisim怎么新建窗口?multisim新建窗口方法
  20. MySql INSTR和LOCATE 不区分大小写的问题

热门文章

  1. 【Linux】shell脚本实战-if多分支条件语句详解
  2. 【观察】华为深入核心场景创新,推动智慧城轨迈入新时代
  3. 5.7黄金价格行情走势预测、原油价格走势及非农现价喊单
  4. PTC:需求管理是智能汽车高效创新的关键能力
  5. 微信发的图片服务器也清除,怎么清空自己在微信朋友圈里发的图片?
  6. 【转载】osgeo和pyproj:经纬度坐标和高斯坐标互相转换
  7. 查看tomcat进程号,并终止进程
  8. 康硕展“水晶魔方”助力上海中信泰富广场焕新回归
  9. 三星a52和三星s20fe参数对比哪个好 三星a52和三星s20fe的区别
  10. 攻防世界逆向高手题之EasyRE