自定义View-使用Adapter设计模式打造流式布局

概述

最近在写公司项目需求,有一个流式布局FlowLayout需要进行修改,这个一般用于显示标签信息,看了一下之前的代码,感觉可拓展性太差,要实现这次的效果,有点麻烦,索性自己打造一个流式布局,这次决定像ListView,RecycleView一样,采用Adapter设计模式进行实现,这样具体对某个子View进行修改的话,可以直接在Adapter中进行修改。

效果

分析

1.继承自谁?我们可以看到内部包含许多View,因此这是一个自定义ViewGroup

2.测量 onMeasure(),这里我们需要知道FlowLayout的高度可以随便给,但是宽度不可以是wrap_content,因此我们需要在对wrap_conent时抛出异常提醒,具体怎么判断宽度是否为wrap_content? 这里需要根据测量模式进行处理,简单可以理解为:

2.1 EXACTLY:表示设置了精确值,一般当设置其宽、高为dp或是match_parent时,会将其测量模式设置为EXACTLY; 2.2 AT_MOST:一般当设置其宽、高为wrap_content时,会将其测量设置为AT_MOST; 2.3 UNSPECIFIED:表示子布局想要多大就多大,此种模式比较少见。一般会在系统的View中,例如ScrollView…

3.摆放 onLayout:这里我们需要对子View按照顺序进行换行或摆放,定位

实现

1.首先需要处理的是onMeasure()方法

1.1 需要处理什么时候换行?

当前已摆放View的宽度+即将要摆放View的宽度>该FlowLayout的宽度+PaddingLeft+PaddingRight

1.2 高度计算

FlowLayout的高度 = 所有行的最大高度+PaddingTop+PaddingBottom

1.3 当View设置为GONE则不进行测量

注意⚠️:这里我们设置了两个List集合,一个是我们所有的集合按照我们每一行的View进行储存,为了避免代码臃肿,在onlayout在写一遍,这个集合我们在这里进行储存,我们在摆放时需要使用

下面看下具体的实现代码

private List<List<View>> mChildViews = new ArrayList<>();
private ArrayList<View> childViews = new ArrayList<>();//每一行的View
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //判断用户是否设置宽度为wrap_content,如果是wrap_content则抛出异常
    if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
        throw new RuntimeException("FlowLayout does not allow setting layout_width to wrap_content");
    }
    mCurrentLines = 0;
    //获取View个数
    int childCount = getChildCount();
    //获取宽度
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    //计算高度
    int heightSize = getPaddingTop() + getPaddingBottom();
    int lineWidth = 0;
    int maxLineHeight = 0;
    mChildViews.clear();
    childViews.clear();
    //循环所有的View计算高度  注意处理Gone,padding margin
    for (int i = 0; i < childCount; i++) {
        //获取子View
        View mChildView = getChildAt(i);
        if (mChildView.getVisibility() == GONE) continue;
        //这句话执行完毕之后  就可以获取子View的宽高了 因为这句话会调用子View 的measure方法
        measureChild(mChildView, widthMeasureSpec, heightMeasureSpec);
        //获取layoutParams  计算最大高度
        MarginLayoutParams layoutParams = (MarginLayoutParams) mChildView.getLayoutParams();
        maxLineHeight = Math.max(maxLineHeight, mChildView.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin);
        int mChildWidth = mChildView.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;

        if (lineWidth + mChildWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
            // 需要换行
            heightSize += maxLineHeight;
            maxLineHeight = 0;
            lineWidth = mChildWidth;
            mChildViews.add(childViews);
            childViews = new ArrayList<>();
            mCurrentLines++;
            if (lines != 0 && mCurrentLines == lines) break;
        } else {
            //未满一行不需要换行
            lineWidth += mChildWidth;
        }

        childViews.add(mChildView);
        if (i == (childCount - 1)) {
            heightSize += maxLineHeight;
            mChildViews.add(childViews);
        }
    }
    //设置自己的宽高
    setMeasuredDimension(widthSize, heightSize);
}
  1. onLayout摆放方法,这里我们需要注意的是处理好子View的Margin和FlowLayout的Padding,这里拿到我们在onMeasure中赋值的集合,在这里进行循环摆放,
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int left, top = this.getPaddingTop(), right, bottom;
    int maxLineHeight = 0;
    //循环所有的
    for (List<View> mViews : mChildViews) {
        //新的一行开始
        left = getPaddingLeft();
        for (View mView : mViews) {
            MarginLayoutParams layoutParams = (MarginLayoutParams) mView.getLayoutParams();
            maxLineHeight = Math.max(maxLineHeight, mView.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin);
            left += layoutParams.leftMargin;
            right = left + mView.getMeasuredWidth();
            bottom = top + layoutParams.topMargin + mView.getMeasuredHeight();
            mView.layout(left, top, bottom);
            left += mView.getMeasuredWidth() + layoutParams.rightMargin;
        }
        top += maxLineHeight;
    }
}

到这里,一个流式布局大致完成,不过这样的流式布局,使用及后续的修改比较麻烦,再者就是在添加数据时,一个数据的多样性,想String[],List都比较麻烦,下面我们仿照ListView,RecycleView一样使用Adapter设计模式来解决这些耦合度问题,如果数据变化的话,我们也按照ListView,RecycleView一样采用观察者模式,进行刷新.下面就来看下具体的实现吧

  1. 首先我们需要定义一个抽象的类,我们关心的条目个数,及View显示需要定义为抽象方法,至于其他的公共使用的,我们在该类中实现,
public abstract class FlowLayoutAdapter {
    private final DataSetObservable mDataSetObservable = new DataSetObservable();
    // 子view的集合
    private final ArrayList<View> views = new ArrayList<>();

    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

    // 得到条目数
    public abstract int getItemCount();

    // 根据位置得到子View布局
    public abstract View getItemView(int position, ViewGroup parent);

    // 将子view布局添加到总的list里面
    public void addViewToList(ViewGroup parent) {
        views.clear();
        int counts = getItemCount();
        if (counts == 0) return;

        for (int i = 0; i < counts; i++) {
            views.add(getItemView(i, parent));
        }
    }

    //得到列表里面的所有子view
    public ArrayList<View> getViewList() {
        return views;
    }
}

2.在FlowLayout中设置setAdapter()方法添加View

public void setAdapter(FlowLayoutAdapter adapter) {
    if (mAdapter != null && mDataSetObserver != null) {
        //注销观察者
        mAdapter.unregisterDataSetObserver(mDataSetObserver);
        mAdapter = null;
    }

    if (adapter == null) throw new NullPointerException("adapter does not allow is null");

    this.mAdapter = adapter;
    //刷新数据
    mDataSetObserver = new DataSetObserver() {
        @Override
        public void onChanged() {
            resetLayout();
        }
    };
    //注册观察者
    mAdapter.registerDataSetObserver(mDataSetObserver);
    resetLayout();
}

protected final void resetLayout() {
    this.removeAllViews();
    int counts = mAdapter.getItemCount();
    mAdapter.addViewToList(this);
    ArrayList<View> views = mAdapter.getViewList();
    for (int i = 0; i < counts; i++) {
        this.addView(views.get(i));
    }
}
  1. 下面我们看一下具体使用实例

3.1 在Activity中:

class FlowLayoutActivity : AppCompatActivity() {
    companion object {
        fun start(mContext: Context, title: String) {
            val intent = Intent()
            intent.setClass(mContext, FlowLayoutActivity::class.java)
            intent.putExtra("title", title)
            mContext.startActivity(intent)
        }
    }

    private val datas: ArrayList<String> = arrayListOf(...)

    private val mFlowLayout: FlowLayout by lazy { findViewById(R.id.flowLayout) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_flow_layout)
        mFlowLayout.setAdapter(DataFlowAdapter(this@FlowLayoutActivity, datas))
    }
}

3.2 在Adapter中

public class DataFlowAdapter extends FlowLayoutAdapter {
    private Context mContext;
    private List<String> dataList;

    public DataFlowAdapter(Context mContext, List<String> dataList) {
        this.mContext = mContext;
        this.dataList = dataList;
    }

    @Override
    public int getItemCount() {
        return dataList == null ? 0 : dataList.size();
    }

    @Override
    public View getItemView(int position, ViewGroup parent) {
        AppCompatTextView mView = (AppCompatTextView) LayoutInflater.from(mContext).inflate(R.layout.item_flow_view, parent, false);
        mView.setText(dataList.get(position));
        return mView;
    }
}

至此,整个流式布局到这里也就完成了

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具