【问题标题】:How to add date separators in recycler view using Paging Library?如何使用分页库在回收站视图中添加日期分隔符?
【发布时间】:2019-04-01 02:06:30
【问题描述】:

经过大量搜索,我知道可以使用常规适配器,但我不知道如何使用分页库来做到这一点。我不需要代码只是一个线索。

示例

【问题讨论】:

    标签: android android-recyclerview kotlin android-paging


    【解决方案1】:

    要添加分隔符,您基本上有 2 个选项:

    1. 基于视图,您明确地将分隔符作为“项目”包含在列表中,并为这些分隔符定义新的视图类型。允许列表重复使用分隔符视图,但这意味着您需要在定义数据时考虑分隔符。
    2. 基于数据,每个项目实际上都有一个分隔视图,但它只显示在特定项目上。根据某些标准,您可以在绑定视图持有者时显示或隐藏它。

    对于分页库,只有选项 2 是可行的,因为它只加载部分数据并且插入分隔符变得更加复杂。您只需要找出一种方法来检查项目 x 是否与项目 x-1 不同,并根据结果在视图中显示/隐藏日期部分。

    【讨论】:

    • 如果我必须使用选项 1,有什么想法吗?
    • 很好的答案,如果我们考虑到关注点的分离,选项2是唯一的方法。 DataSource 不应该知道 View 想要显示标题,它应该只获取数据。
    • 在 2 选项中存在几个问题: - 如果从数据库中删除具有“标题”的项目怎么办? - 如果新项目被添加到列表中间怎么办? - 如果包含“header”的项目中的日期字段发生变化怎么办?
    【解决方案2】:

    我和你在同一个地方,我想出了这个解决方案。

    一个重要的注意事项,为了实现这一点,我必须将我的日期转换器更改为数据库,从 long 更改为 string 以存储时间戳

    这些是我的转换器

    class DateConverter {
        companion object {
            @JvmStatic
            val formatter = SimpleDateFormat("yyyyMMddHHmmss", Locale.ENGLISH)
    
            @TypeConverter
            @JvmStatic
            fun toDate(text: String): Date = formatter.parse(text)
    
            @TypeConverter
            @JvmStatic
            fun toText(date: Date): String = formatter.format(date)
        }
    }
    

    虽然有一些起始信息,但我有一个我希望显示的报告标题列表,并且可以翻页并能够过滤

    它们由这个对象表示:

    data class ReportHeaderEntity(
    @ColumnInfo(name = "id") override val id: UUID
    , @ColumnInfo(name = "name") override val name: String
    , @ColumnInfo(name = "description") override val description: String
    , @ColumnInfo(name = "created") override val date: Date)
    

    我还想在列表中的项目之间添加分隔符以按日期显示它们

    我通过以下方式实现了这一点:

    我在这样的房间里创建了一个新查询

     @Query(
        "SELECT id, name, description,created " +
                "FROM   (SELECT id, name, description, created, created AS sort " +
                "        FROM   reports " +
                "        WHERE  :filter = '' " +
                "                OR name LIKE '%' || :filter || '%' " +
                "                OR description LIKE '%' || :filter || '%' " +
                "        UNION " +
                "        SELECT '00000000-0000-0000-0000-000000000000' as id, Substr(created, 0, 9) as name, '' as description, Substr(created, 0, 9) || '000000' AS created, Substr(created, 0, 9) || '256060' AS sort " +
                "        FROM   reports " +
                "        WHERE  :filter = '' " +
                "                OR name LIKE '%' || :filter || '%' " +
                "                OR description LIKE '%' || :filter || '%' " +
                "        GROUP  BY Substr(created, 0, 9)) " +
                "ORDER  BY sort DESC ")
    
    fun loadReportHeaders(filter: String = ""): DataSource.Factory<Int, ReportHeaderEntity>
    

    这基本上为我过滤过的所有项目创建了一个分隔线

    它还会创建一个用于排序的虚拟日期(时间为 25:60:60,以便它始终出现在其他报告的前面)

    然后我使用 union 将其与我的列表结合起来,并按虚拟日期对它们进行排序

    我必须从 long 更改为 string 的原因是因为在 sql 中使用字符串创建虚拟日期并将日期部分与整个日期时间分开会更容易

    上面创建了一个这样的列表:

    00000000-0000-0000-0000-000000000000    20190522        20190522000000
    e3b8fbe5-b8ce-4353-b85d-8a1160f51bac    name 16769  description 93396   20190522141926
    6779fbea-f840-4859-a9a1-b34b7e6520be    name 86082  description 21138   20190522141925
    00000000-0000-0000-0000-000000000000    20190521        20190521000000
    6efa201f-d618-4819-bae1-5a0e907ddcfb    name 9702   description 84139   20190521103247
    

    在我的 PagedListAdapter 中,我将其更改为 PagedListAdapter&lt;ReportHeader, RecyclerView.ViewHolder&gt; 的实现(不是特定的查看器)

    添加到伴生对象中:

    companion object {
        private val EMPTY_ID = UUID(0L,0L)
        private const val LABEL = 0
        private const val HEADER = 1
    }
    

    并像这样覆盖获取视图类型:

    override fun getItemViewType(position: Int): Int = if (getItem(position)?.id ?: EMPTY_ID == EMPTY_ID) LABEL else HEADER
    

    然后我创建了两个单独的视图持有者:

    class ReportHeaderViewHolder(val binding: ListItemReportBinding) : RecyclerView.ViewHolder(binding.root) 
    
    class ReportLabelViewHolder(val binding: ListItemReportLabelBinding) : RecyclerView.ViewHolder(binding.root)
    

    并像这样实现了其他覆盖方法:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            HEADER -> ReportHeaderViewHolder(DataBindingUtil.inflate(inflater, R.layout.list_item_report, parent, false))
            else -> ReportLabelViewHolder(DataBindingUtil.inflate(inflater, R.layout.list_item_report_label, parent, false))
        }
    }
    
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val reportItem = getItem(position)
        when (getItemViewType(position)) {
            HEADER -> {
                (holder as ReportHeaderViewHolder).binding.apply {
                    report = reportItem
                    executePendingBindings()
                }
            }
            LABEL -> {
                (holder as ReportLabelViewHolder).binding.apply {
                    date = reportItem?.name
                    executePendingBindings()
                }
            }
        }
    }
    

    我希望这有助于并激励人们找到更好的解决方案

    【讨论】:

    • 感谢您的出色工作!但是 GROUP BY 中应该有任何列名,不是吗?
    • group by 基本上是您要添加的分隔符类型,我的是日期,所以我按日期分组,如果您想做电话目录之类的事情,您可以按 substr(upper (名称) 0,1)
    • 我听不懂。根据文档,它是如何工作的:GROUP BY [column_name]。 Substr 只返回与列名无关的字符串。还是我错过了什么?
    • 哦,对不起,我没听懂你的问题,是的 group by 似乎也适用于 substr 和其他函数,基本上它评估每一行的函数,然后根据该值将它们组合在一起,如果您只是添加了列名,它将根据该列的值执行分组
    • 我认为Group By是为了避免重复的HEADER。
    【解决方案3】:

    您可以使用 Paging 3 库中的insertSeparators 获得相同的结果。 确保您的项目按日期为sorted

    在或viewmodel 内部检索Pager 类似的东西

    private val communicationResult: Flow<PagingData<CommunicationHistoryItem>> = Pager(
        PagingConfig(
            pageSize = 50,
            enablePlaceholders = false,
            maxSize = 400,
            initialLoadSize = 50
        )
    ) {
        CommunicationPagingSource(repository)
    }.flow.cachedIn(viewModelScope)
    

    毕竟insert separators 就像一个标题

    val groupedCommunicationResult = communicationResult
            .map { pagingData -> pagingData.map { CommunicationHistoryModel.Body(it) } }
            .map {
                it.insertSeparators{ after, before ->
                    if (before == null) {
                        //the end of the list
                        return@insertSeparators null
                    }
    
                    val afterDateStr = after?.createdDate
                    val beforeDateStr = before.createdDate
    
                    if (afterDateStr == null || beforeDateStr == null)
                        return@insertSeparators null
    
                    val afterDate = DateUtil.parseAsCalendar(afterDateStr)?.cleanTime()?.time ?: 0
                    val beforeDate = DateUtil.parseAsCalendar(beforeDateStr)?.cleanTime()?.time ?: 0
    
                    if (afterDate > beforeDate) {
                        CommunicationHistoryModel.Header( DateUtil.format(Date(beforeDate))) // dd.MM.yyyy
                    } else {
                        // no separator
                        null
                    }
                }
            }
    

    grouping 需要cleanTime dd.MM.yyyy 忽略时间

    fun Calendar.cleanTime(): Date {
        set(Calendar.HOUR_OF_DAY, 0)
        set(Calendar.MINUTE, 0)
        set(Calendar.SECOND, 0)
        set(Calendar.MILLISECOND, 0)
        return this.time
    }
    

    【讨论】:

    • 它只适用于列表项中间的分隔符,如果你也想要顶部的分隔符,那么添加这个 if (after == null) { CommunicationHistoryModel.Header( DateUtil.format(Date (beforeDate))) }
    【解决方案4】:

    绑定数据的时候也会传入前一项

      override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        val previousItem = if (position == 0) null else getItem(position - 1)
        holder.bind(item, previousItem)
      }
    

    然后每个视图设置一个标题,只有在前一个项目没有相同标题时才会显示该标题。

        val previousHeader =  previousItem?.name?.capitalize().first()
        val header = item?.name?.capitalize()?.first()
        view.cachedContactHeader.text = header
        view.cachedContactHeader.isVisible  = previousHeader != header
    

    【讨论】:

      【解决方案5】:

      Kiskae's answer 非常好,对于您的情况,选项 2 可能效果很好。

      在我的情况下,我想要一个不在数据库中的附加项目,如下所示:

      • 全部显示
      • 项目 1
      • 第 2 项

      它也需要是可点击的。有覆盖getItemCount 以返回+1 并为其他方法偏移位置的常用方法。

      但我偶然发现了另一种我还没有看到文档的方法,它可能对某些情况有用。您可以使用union 将其他元素合并到您的查询中:

      @Query("select '' as name, 0 as id " +
              "union " +
              "select name, id from user " +
              "order by 1 asc")
      DataSource.Factory<Integer, User> getAllDataSource();
      

      这意味着数据源实际上在开始时返回了另一个项目,并且不需要调整位置。在您的适配器中,您可以检查该项目并以不同方式处理它。

      在您的情况下,查询必须有所不同,但我认为这是可能的。

      【讨论】:

      • 解决方案应该和@Cruces说的一样。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多