IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 手写一个淘宝、京东的搜索流式布局FlowLayout -> 正文阅读

[移动开发]手写一个淘宝、京东的搜索流式布局FlowLayout

作者:token keyword

一些叨叨

  • 市面上所有的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中所有子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<List<View>> 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<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值。
请添加图片描述

   @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<View> 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代码

        <com.lw.flow.FlowLayout
            android: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() {
            @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;

/**
 * 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() {
                @Override
                public void onClick(View v) {
                    if (onItemClickListener != null) {
                        onItemClickListener.OnItemClick(v, finalMDatum);
                    }
                }
            });
            addView(textView);
        }
    }

    //所有行的集合
    private List<List<View>> 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<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;
    }

    @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<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;
        }
    }
}

推荐学习资料:

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-11-20 18:30:36  更:2021-11-20 18:30:53 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:35:53-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码