Android使用RecyclerView实现日期分组以及时间轴显示

抽空介绍一下如何使用RecyclerView来实现分组列表以及时间轴的显示,先看下效果图:


Video_231202152515.gif

作为Android的小伙伴,在需求方面上,难免遇到实现类似的功能实现,实现起来有一定的难度,RecyclerView进行分组,和时间轴的显示。
重点讲下,RecyclerView如何进行分组,其实就是对数据源集合进行分组:
BuildListDataUtil.kt

object BuildListDataUtil {

    /**
     * 集合进行分组
     */
    fun buildListData(sourceList:MutableList<AppInfo>?): MutableList<AppInfo>{
        val tempData = ArrayList<AppInfo>()
        val appInfo = createEmptyObject(sourceList!![0])
        tempData.add(appInfo)
        tempData.add(sourceList[0])
        var preDate = DateUtils.getTimeStampConvertToDate(sourceList[0]
            .orderInfoApp?.publicFirstTime!!, DateUtils.PARAMETER_ALL_DATE_TYPE)
        for (index in 1 until sourceList.size) {
            val curDate = DateUtils.getTimeStampConvertToDate(sourceList[index]
                .orderInfoApp?.publicFirstTime!!, DateUtils.PARAMETER_ALL_DATE_TYPE)
            //日期一致的话,就添加至集合
            if (TextUtils.equals(preDate, curDate)) {
                tempData.add(sourceList[index])
            } else {
                // 日期不一致,则创建新的对象并添加到集合中
                val curAppInfo = createEmptyObject(sourceList[index])
                tempData.add(curAppInfo)
                tempData.add(sourceList[index])
                preDate = curDate
            }
        }
        return tempData
    }

    /**
     * 创建空对象
     */
    private fun createEmptyObject(appInfo: AppInfo): AppInfo {
        var tempInfo = AppInfo()
        var orderInfoApp = OrderInfoApp()
        orderInfoApp.publicFirstTime = appInfo.orderInfoApp?.publicFirstTime!!
        tempInfo.orderInfoApp = orderInfoApp
        return tempInfo
    }
}

我封装了BuildListDataUtil工具类,主要是对数据源集合逻辑进行分组处理,首先根据集合的索引值拿到第一个对象,进行创建空的对象,同时根据时间戳进行赋值,并返回空的对象作为用来展示标题布局的日期标题,并添加到临时的集合中,根据当前的对象的时间戳返回当前的preDate变量,接下来sourceList进行遍历,遍历下一个对象的时间戳返回当前的curDate 变量,和上一个preDate变量进行对比,如果日期一致,就添加到同一集合里面,否则就创建空的对象并添加到集合里面,并且把curDate值赋值给preDate,以此类推。
我打了断点,如下:


图片1.png

图片2.png

接下来就是把集合分好组的tempData变量返回给外部调用。
重点看下RecycleView的Adapter做了什么。
BuildGroupDateAdapter.kt

class BuildGroupDateAdapter constructor(datas:MutableList<AppInfo>?): Adapter<ViewHolder>() {
    //标题
    val ITEM_TITLE_TYPE = 1
    //内容
    val ITEM_CONTENT_TYPE = 2
    private var datas:MutableList<AppInfo>? = null
    private lateinit var itemTitleBing :ItemTitleLayoutBinding
    private lateinit var itemContentBinding: ItemContentLayoutBinding

    init {
        this.datas = datas
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
      var view :View
        return when(viewType) {
            ITEM_TITLE_TYPE -> {
                itemTitleBing = ItemTitleLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
                view = itemTitleBing.root
                TitleViewHolder(view)
            }
            else -> {
                itemContentBinding = ItemContentLayoutBinding
                    .inflate(LayoutInflater.from(parent.context), parent, false)
                view = itemContentBinding.root
                ContentViewHolder(view)
            }
        }
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        when(holder) {
            is TitleViewHolder -> {
                val titleDate = getTimeStampConvertToDate(
                    datas
                        ?.get(position)!!.orderInfoApp!!.publicFirstTime,
                    DateUtils.PARAMETER_ALL_DATE_TYPE
                )
                itemTitleBing.tvTitle.text = titleDate
                dealWithTitleTimeLine(position)
            }
            is ContentViewHolder -> {
                itemContentBinding.tvContent.text = datas!![position].name
                dealWithContentTimeLine(position)
            }
        }
    }

    /**
     * 标题时间轴
     */
    private fun dealWithTitleTimeLine(index: Int) {
        when(index) {
            0 -> {
                itemTitleBing.aboveLineTitle.visibility = View.INVISIBLE
                itemTitleBing.belowLineTitle.visibility = View.VISIBLE
            }
            else -> {
                itemTitleBing.aboveLineTitle.visibility = View.VISIBLE
                itemTitleBing.belowLineTitle.visibility = View.VISIBLE
            }
        }
    }

    /**
     * 内容时间轴
     */
    private fun dealWithContentTimeLine(index: Int) {
        when(index) {
            datas!!.size - 1  -> {
                itemContentBinding.timeLineContent.renderBg(R.color.time_line_color, true)
            }
            else -> {
                itemContentBinding.timeLineContent.renderBg(R.color.time_line_color, false)
            }
        }
    }

    override fun getItemCount(): Int {
       return datas!!.size
    }

    override fun getItemViewType(position: Int): Int {
        var id = datas!![position].id
        //id是空的 说明该对象是用来展示标题item布局的
        if(id.isNullOrEmpty()) {
          return ITEM_TITLE_TYPE
        }
        return ITEM_CONTENT_TYPE
    }

    class TitleViewHolder(view: View) : ViewHolder(view)

    class ContentViewHolder(view: View) : ViewHolder(view)
}

上面代码逻辑不是很复杂,其实就是创建了两个ViewHolder,一个是用来展示标题的ViewHolder,一个是来展示内容的ViewHolder。
重点看下这个,


图片3.png

根据position获取当前对象的id,id是空的用来展示标题item布局的。因为刚讲过,


图片4.png

创建空对象的时候,仅仅是把时间戳赋值给它,并没有赋值给id,因此id是空的话,是用来展示标题的type。
BuildGroupDateActivity.kt
class BuildGroupDateActivity : AppCompatActivity() {

    lateinit var adapter: BuildGroupDateAdapter
    lateinit var binding: ActivityMainBuildGroupDateBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBuildGroupDateBinding.inflate(layoutInflater)
        setContentView(binding.root)
        adapter = BuildGroupDateAdapter(getDatas())
        binding.baseRecyclerView.layoutManager = LinearLayoutManager(this)
        binding.baseRecyclerView.adapter = adapter
    }

    private fun getDatas() : MutableList<AppInfo> {
        return BuildListDataUtil.buildListData(TestBuildData.datas())
    }
}

运行此项目,界面上就可以正常展示RecyclerView日期分组了。
接下来,我们看下时间轴怎么实现。
在adapter里面创建了两个ViewHolder,每个ViewHolder创建了item布局,时间线的显示其实就是在布局里面做。
item_title_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="36dp"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        >
        <RelativeLayout
            android:layout_width="30dp"
            android:layout_height="match_parent"
            >
            <View
                android:id="@+id/above_line_title"
                android:layout_width="1dp"
                android:layout_height="18dp"
                android:background="@color/time_line_color"
                android:layout_centerHorizontal="true"
                android:layout_above="@+id/middle_line"
                />
            <View
                android:id="@+id/middle_line"
                android:layout_width="5dp"
                android:layout_height="5dp"
                android:background="@drawable/time_line_round_bg"
                android:layout_centerInParent="true"
                />

            <View
                android:id="@+id/below_line_title"
                android:layout_width="1dp"
                android:layout_height="18dp"
                android:layout_below="@id/middle_line"
                android:layout_centerHorizontal="true"
                android:background="@color/time_line_color" />
        </RelativeLayout>
    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text=""
        android:layout_gravity="center_vertical"
        />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

item_content_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        >
        <RelativeLayout
            android:layout_width="30dp"
            android:layout_height="match_parent"
            >
            <com.xilianke.mainapp_master.view.TimeLineView
                android:id="@+id/time_line_content"
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:layout_centerHorizontal="true"
                android:background="@color/teal_700" />
        </RelativeLayout>
        <TextView
            android:id="@+id/tvContent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""
            android:layout_gravity="center_vertical"
            />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

在该布局里面需要自定义一个View,也就是TimeLineView,需要处理时间线渐变。
TimeLineView.kt

class TimeLineView : View {

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attributeSet: AttributeSet?) :this (context, attributeSet, 0)

    constructor(context: Context, attributeSet: AttributeSet?, def: Int):super(context, attributeSet, def)

    fun renderBg(color: Int,lastItem:Boolean) {
        if (lastItem) {
          val gradient = gradientDrawable(color)
            background = gradient
        } else {
            background = context.resources.getDrawable(color)
        }
        post {
            invalidate()
        }
    }

    private fun gradientDrawable(arg: Int): GradientDrawable {
      val colors = intArrayOf(
//          arg and 0xFFFFFFFF.toInt(),//0%
//          arg and 0x7FFFFFFF,//50%
          arg and 0x33FFFFFF,//80%
          arg and 0x00FFFFFF,//100%
      )
        val gradientDrawable = GradientDrawable()
        gradientDrawable.colors = colors
        gradientDrawable.orientation = GradientDrawable
            .Orientation.TOP_BOTTOM//从上到下渐变
        return gradientDrawable
    }
}
图片5.png

运行此项目,最终的效果就是文章开头的效果图。
我把这次案例进行总结,方便后续可能会使用到,难免会使用RecyclerView分组实现,例如RecyclerView联系人分组实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容