Sprout 工程:构建 Android 月份选择器
已发表: 2020-06-26注意:本文基于截至 2020 年 6 月 1 日的 Material Components 版本1.2.0-beta01 。
在我在 Sprout Social 的一个小型 Android 团队工作的三年半中,激励我每天上班的主要因素之一是我们公司的自由和信任,可以以我们认为最好的方式解决问题。
自由研究和探索我们认为必要的问题的许多不同解决方案,同时考虑交付产品更新的时间框架,使我们能够为我们的客户和我们的软件找到最佳解决方案。
其中一项挑战涉及为我们的新移动报告功能构建 UI 组件。 这个新组件是一个月份选择器,它允许我们的用户确定分析报告的日期范围。

我们选择的起点是现有的材料组件库。 这个库不是从头开始,而是积极维护并与材料规范保持一致。 以这个库为基础,我们可能会减少我们必须自己编写的逻辑量。
在本文中,我将介绍我们如何处理此过程、构建 Sprout Android 应用程序的一些独特因素、在此过程中出现(并已修复)的一些“陷阱”,以及如果您正在从事类似的项目。
介绍
Android Material Components 1.1.0 Release 引入了一个新的 Date Picker UI 组件。 这个新的MaterialDatePicker在 AppCompat CalendarView上的一个受欢迎的新增功能是能够使用日历视图或文本输入字段选择日期范围。
旧的 AppCompat CalendarView 不是很灵活。 对于要解决的有限用例来说,这是一个很好的组件; 也就是说,选择单个日期和可选的最小和最大日期来指定允许的日期范围限制。
新的 MaterialDatePicker 具有更大的灵活性,允许使用扩展的行为功能。 它通过一系列接口工作,人们可以实现这些接口来调整和修改选择器的行为。
这种行为修改是在运行时通过MaterialDatePicker.Builder类上的一组构建器模式函数完成的。
这意味着我们能够通过可组合的接口组件扩展这个MaterialDatePicker的基本行为。
注意:虽然MaterialDatePicker使用了许多不同的组件,但在本文中,我们将仅介绍日期选择组件。
日期范围选择器
Sprout Social Android 团队正在构建我们的分析报告部分。
这个新部分将允许我们的用户选择一组过滤器和一组报告将涵盖的日期范围。


MaterialDatePicker附带了一些预构建的组件,我们可以利用这些组件来完成我们的用例。
对于我们最常见的情况,允许用户选择日期范围,预构建的MaterialDatePicker就足够了:
通过这个代码块,我们得到了一个允许用户选择日期范围的日期选择器。

每月日期选择器
具有更多独特日期选择的 Sprout Social 报告之一是 Twitter 趋势报告。
此报告与其他报告的不同之处在于,它不允许任何类型的日期范围,而是强制选择单个月份,这意味着用户只能选择 2020 年 3 月与 2020 年 3 月 3 日至 3 月 16 日。
我们的 Web 应用程序通过使用下拉表单字段来处理此问题:

MaterialDatePicker无法使用上一节中讨论的预构建的 Material Date Range Picker 来强制执行此类限制。 幸运的是,MaterialDatePicker 是使用可组合部件构建的,允许我们为特定用例扩展默认行为。
日期选择行为
MaterialDatePicker利用DateSelector作为用于选择器选择逻辑的接口。
来自 Javadoc:
“ {@link MaterialCalendar<S>}用户控制日历如何显示和返回选择的界面……”
您会注意到MaterialDatePicker.Builder.dateRangePicker()返回RangeDateSelector的构建器实例,我们在上面的示例中使用了它。
此类是实现DateSelector的预构建选择器。

头脑风暴每月的日期选择行为
对于我们的用例,我们想要一种方法让我们的用户选择整个月份作为选定的日期范围; 例如 2020 年 5 月、2020 年 4 月等。
我们认为上面引用的预先构建的RangeDateSelector让我们大部分时间到达了那里。 该组件允许用户选择日期范围并强制执行[lower, upper] 界限。
唯一缺少的是一种强制选择以自动选择整个月的方法。 RangeDateSelector的默认行为让用户选择开始日期和结束日期。
我们想要一种行为,当用户选择一个月中的某一天时,选择器会自动选择整个月份作为日期范围。

我们决定的解决方案是扩展RangeDateSelector ,然后覆盖日期选择行为以自动选择整个月份。
幸运的是,我们可以从DateSelector接口重写一个名为select(selection: Long)的函数。
当用户在选择器中选择一天时,将调用此函数,所选日期以 UTC 毫秒为单位从纪元开始传递。
实施每月日期选择行为
结果证明是最简单的部分,因为我们有一个明确的函数,我们可以重写以获得我们想要的行为。
基本逻辑是这样的:
- 用户选择一天。
-
select()函数在从纪元开始的Long UTC 毫秒内调用所选日期。 - 从传递给我们的给定日期开始查找该月的第一天和最后一天。
- 打电话给
super.select(1st of month)&super.select(last day of month) -
RangeDateSelector的父行为应按预期工作,并选择月份作为日期范围。
把它们放在一起
现在我们有了自定义MonthRangeDateSelector ,我们可以设置MaterialDatePicker 。
为了进一步举例,我们可以像这样处理选择的结果:
结果将如下所示:

陷阱
只有一个主要问题导致难以达成此解决方案。
用于构建MonthRangeDateSelector的主要组件是DateSelector类和RangeDateSelector接口。 本文中使用的库版本 (1.2.0-beta01) 限制了这两个文件的可见性,以阻止扩展或实现它们。
结果,尽管我们可以成功编译新的MonthRangeDateSelector ,但编译器确实显示了一个非常可怕的警告来阻止我们这样做:

隐藏此编译器警告的一种方法是添加@Suppress("RestrictedApi") ,如下所示:

这一经历说明,尽管 Material Components Library 已经为 Android 开发者社区提供了一些很棒的新组件,但它仍然是一项正在进行的工作。
这个库的很大一部分是对来自 Android 社区的反馈的开放性! 发现这个组件可见性限制后,我在 Github Project 上打开了一个 issue,甚至打开了一个 PR 来解决它。
Material Components 团队和 Android 社区之间的这种开放式反馈循环为每个人带来了良好的协作和成果。
结论
新的MaterialDatePicker具有一些开箱即用的强大功能,可能涵盖大多数日期选择用例。
但是,与 AppCompat CalendarView 之类的东西相比,它最好的部分是它是以可组合的方式构建的。 因此,它可以很容易地针对特定用例进行扩展和修改,而在CalendarView中完成这样的事情要困难得多。
特别感谢
我想强调一些帮助同行评审这篇文章的人:
- 尼克·劳特(Github)
- 迈克沃尔夫森(Github)
- 瑞恩菲利普斯(领英)
- 卢卡斯·默勒斯(Github)
- 米特·帕特尔(领英)
