Skip to content

Commit e9c893b

Browse files
Vetle444Vetle Finstad
andauthored
Add Android picker error haptics (#886)
Co-authored-by: Vetle Finstad <finstad@Vetles-MacBook-Pro-2.local>
1 parent 6a505ab commit e9c893b

6 files changed

Lines changed: 92 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## [60.2.5]
2+
- [Pickers][Android] Added error haptic feedback when TimePicker or DateAndTimePicker selections are clamped by minimum or maximum constraints.
3+
14
## [60.2.4]
25
- [NavigationListItem] Groups the row as one accessibility element, applies the platform button trait including when used in a CollectionView, and uses `Title` as the default screen reader description while still allowing consumers to override `SemanticProperties.Description`.
36

src/app/Playground/VetleSamples/VetlePage.xaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@
9090
HorizontalOptions="Start"/>
9191
</dui:VerticalStackLayout>
9292

93+
<!-- DatePicker MinimumDate/MaximumDate -->
94+
<Label Text="DatePicker Date Constraints"
95+
Style="{dui:Styles Label=SectionHeader}"
96+
Margin="{dui:Thickness Top=content_margin_large}"/>
97+
98+
<dui:VerticalStackLayout Spacing="{dui:Sizes content_margin_small}"
99+
Margin="{dui:Thickness Top=content_margin_small}">
100+
<Label Text="MaximumDate = DateTime.Now (today should stay selectable)"
101+
Style="{dui:Styles Label=UI200}"/>
102+
<dui:DatePicker MaximumDate="{x:Static system:DateTime.Now}"
103+
HorizontalOptions="Start"/>
104+
105+
<Label Text="MinimumDate = DateTime.Now (today should stay selectable)"
106+
Style="{dui:Styles Label=UI200}"
107+
Margin="{dui:Thickness Top=content_margin_small}"/>
108+
<dui:DatePicker MinimumDate="{x:Static system:DateTime.Now}"
109+
HorizontalOptions="Start"/>
110+
</dui:VerticalStackLayout>
111+
93112
<!-- TimePicker MinimumTime/MaximumTime -->
94113
<Label Text="TimePicker Time Constraints"
95114
Style="{dui:Styles Label=SectionHeader}"

src/library/DIPS.Mobile.UI/Components/Pickers/DateAndTimePicker/Android/DateAndTimePickerService.cs

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
using DIPS.Mobile.UI.API.Vibration;
12
using DIPS.Mobile.UI.Components.Pickers.DatePicker.Android;
23
using DIPS.Mobile.UI.Components.Pickers.TimePicker.Android;
34

45
namespace DIPS.Mobile.UI.Components.Pickers.DateAndTimePicker;
56

67
public partial class DateAndTimePickerService
78
{
8-
private static MaterialDatePickerFragment? m_materialDatePicker;
9-
private static MaterialTimePickerFragment? m_materialTimePicker;
9+
private static MaterialDatePickerFragment? s_materialDatePicker;
10+
private static MaterialTimePickerFragment? s_materialTimePicker;
1011

1112
public static partial void Open(DateAndTimePicker dateAndTimePicker, View chipTapped, bool datePickerTapped)
1213
{
@@ -26,18 +27,25 @@ public static partial void Open(DateAndTimePicker dateAndTimePicker, View chipTa
2627
MinimumDate = dateAndTimePicker.MinimumDate
2728
};
2829

29-
datePicker.SelectedDateCommand = new Command(() =>
30+
datePicker.SelectedDateCommand = new Command<DateTime?>(selectedDate =>
3031
{
3132
var selectedDateTime = new DateTime(datePicker.SelectedDate.Year, datePicker.SelectedDate.Month, datePicker.SelectedDate.Day, dateOnOpen.TimeOfDay.Hours, dateOnOpen.TimeOfDay.Minutes, 0, dateAndTimePicker.IgnoreLocalTime ? DateTimeKind.Utc : DateTimeKind.Local)
3233
.ConvertDate(dateAndTimePicker.GetDateTimeKind());
34+
35+
if (!IsOutsideDateBounds(dateAndTimePicker, selectedDate) &&
36+
IsOutsideBounds(dateAndTimePicker, selectedDateTime))
37+
{
38+
VibrationService.Error();
39+
}
40+
3341
dateAndTimePicker.SetSelectedDateTime(selectedDateTime);
3442
});
3543

36-
m_materialDatePicker = new MaterialDatePickerFragment(datePicker);
44+
s_materialDatePicker = new MaterialDatePickerFragment(datePicker);
3745
}
3846
else
3947
{
40-
var timePicker = new TimePicker.TimePicker { SelectedTime = dateOnOpen.ConvertDate(dateAndTimePicker.IgnoreLocalTime).TimeOfDay };
48+
var timePicker = CreateTimePicker(dateAndTimePicker, dateOnOpen);
4149

4250
timePicker.SelectedTimeCommand = new Command(() =>
4351
{
@@ -48,21 +56,57 @@ public static partial void Open(DateAndTimePicker dateAndTimePicker, View chipTa
4856
dateAndTimePicker.SetSelectedDateTime(selectedDateTime);
4957
});
5058

51-
m_materialTimePicker = new MaterialTimePickerFragment(timePicker);
59+
s_materialTimePicker = new MaterialTimePickerFragment(timePicker);
60+
}
61+
}
62+
63+
private static TimePicker.TimePicker CreateTimePicker(DateAndTimePicker dateAndTimePicker, DateTime dateOnOpen)
64+
{
65+
var selectedDate = dateOnOpen.ConvertDate(dateAndTimePicker.IgnoreLocalTime);
66+
var timePicker = new TimePicker.TimePicker { SelectedTime = selectedDate.TimeOfDay };
67+
68+
if (dateAndTimePicker.MaximumDate is { } maximumDate)
69+
{
70+
var convertedMaximumDate = maximumDate.ConvertDate(dateAndTimePicker.IgnoreLocalTime);
71+
if (selectedDate.Date == convertedMaximumDate.Date)
72+
{
73+
timePicker.MaximumTime = convertedMaximumDate.TimeOfDay;
74+
}
75+
}
76+
77+
if (dateAndTimePicker.MinimumDate is { } minimumDate)
78+
{
79+
var convertedMinimumDate = minimumDate.ConvertDate(dateAndTimePicker.IgnoreLocalTime);
80+
if (selectedDate.Date == convertedMinimumDate.Date)
81+
{
82+
timePicker.MinimumTime = convertedMinimumDate.TimeOfDay;
83+
}
5284
}
85+
86+
return timePicker;
5387
}
5488

89+
private static bool IsOutsideBounds(DateAndTimePicker dateAndTimePicker, DateTime? selectedDateTime) =>
90+
selectedDateTime is { } dateTime &&
91+
(dateAndTimePicker.MinimumDate is { } minimumDate && dateTime < minimumDate ||
92+
dateAndTimePicker.MaximumDate is { } maximumDate && dateTime > maximumDate);
93+
94+
private static bool IsOutsideDateBounds(DateAndTimePicker dateAndTimePicker, DateTime? selectedDateTime) =>
95+
selectedDateTime is { } dateTime &&
96+
(dateAndTimePicker.MinimumDate is { } minimumDate && dateTime.Date < minimumDate.Date ||
97+
dateAndTimePicker.MaximumDate is { } maximumDate && dateTime.Date > maximumDate.Date);
98+
5599
internal static partial bool IsOpen()
56100
{
57-
if (m_materialDatePicker is not null && m_materialDatePicker.IsOpen())
101+
if (s_materialDatePicker is not null && s_materialDatePicker.IsOpen())
58102
return true;
59103

60-
return m_materialTimePicker is not null && m_materialTimePicker.IsOpen();
104+
return s_materialTimePicker is not null && s_materialTimePicker.IsOpen();
61105
}
62106

63107
public static partial void Close()
64108
{
65-
m_materialDatePicker?.Close();
66-
m_materialTimePicker?.Close();
109+
s_materialDatePicker?.Close();
110+
s_materialTimePicker?.Close();
67111
}
68112
}

src/library/DIPS.Mobile.UI/Components/Pickers/DatePicker/Android/MaterialDatePickerFragment.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ public MaterialDatePickerFragment(DatePicker datePicker)
4444
var listValidators = new List<CalendarConstraints.IDateValidator>();
4545
if (datePicker.MinimumDate != null) //TODO: Support only setting one of these as well
4646
{
47-
listValidators.Add(DateValidatorPointForward.From(datePicker.MinimumDate.Value.ToLong()));
47+
listValidators.Add(DateValidatorPointForward.From(datePicker.MinimumDate.Value.Date.ToLong()));
4848
}
4949
if (datePicker.MaximumDate != null)
5050
{
51-
listValidators.Add(DateValidatorPointBackward.Before(datePicker.MaximumDate.Value.ToLong()));
51+
listValidators.Add(DateValidatorPointBackward.Before(datePicker.MaximumDate.Value.Date.ToLong()));
5252
}
5353

5454
if (listValidators.Any())

src/library/DIPS.Mobile.UI/Components/Pickers/TimePicker/Android/MaterialTimePickerFragment.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Android.Text.Format;
2+
using DIPS.Mobile.UI.API.Vibration;
23
using DIPS.Mobile.UI.Components.Pickers.DatePickerShared.Android;
34
using Google.Android.Material.TimePicker;
45
using Microsoft.Maui.Platform;
@@ -49,6 +50,14 @@ public void Close()
4950

5051
public void OnClick(View? v)
5152
{
52-
m_timePicker.SetSelectedDateTime(new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, m_materialTimePicker.Hour, m_materialTimePicker.Minute, 0));
53+
var selectedTime = new TimeSpan(m_materialTimePicker.Hour, m_materialTimePicker.Minute, 0);
54+
55+
if (m_timePicker.MinimumTime is { } minimumTime && selectedTime < minimumTime ||
56+
m_timePicker.MaximumTime is { } maximumTime && selectedTime > maximumTime)
57+
{
58+
VibrationService.Error();
59+
}
60+
61+
m_timePicker.SetSelectedDateTime(new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, selectedTime.Hours, selectedTime.Minutes, 0));
5362
}
5463
}

wiki/Components/Pickers.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ There are three types of date/time pickers:
161161
## Date Picker
162162
A date picker should be used by you to let people pick a date. The date picker is an in-line button with a title that people tap to start picking the date. When a date is picked, the date will be displayed as a part of the in-line button.
163163

164+
When `MinimumDate` or `MaximumDate` is set, Android disables dates outside the allowed range.
165+
164166
### Usage
165167
```xml
166168
<dui:DatePicker SelectedDate="{Binding SelectedBirthday}"/>
@@ -191,7 +193,7 @@ A `TimePicker` should be used by you to let people pick a time. The `TimePicker`
191193
```
192194

193195
### Constraining selectable times
194-
You can set `MinimumTime` and `MaximumTime` to restrict the range of times people can choose. On iOS the time picker wheel prevents out-of-range selection. On Android the selected time is clamped after confirmation.
196+
You can set `MinimumTime` and `MaximumTime` to restrict the range of times people can choose. On iOS the time picker wheel prevents out-of-range selection. On Android the selected time is clamped after confirmation, and out-of-range confirmations give error haptic feedback.
195197

196198
```xml
197199
<dui:TimePicker SelectedTime="{Binding SelectedTime}"
@@ -207,7 +209,7 @@ Inspect the [components properties class](https://github.com/DIPSAS/DIPS.Mobile.
207209
## DateAndTime Picker
208210
A `DateAndTimePicker` should be used by you to let people pick both a date -and time. The `DateAndTimePicker` is two in-line buttons with a title that people tap to start picking the date or time. When date is picked, the date or time will be displayed as part of the in-line button.
209211

210-
When `MaximumDate` or `MinimumDate` is set and the selected date equals the boundary date, the time picker is also constrained to the boundary time. On iOS the time wheel prevents out-of-range selection; on Android the value is clamped after confirmation.
212+
When `MaximumDate` or `MinimumDate` is set and the selected date equals the boundary date, the time picker is also constrained to the boundary time. On iOS the time wheel prevents out-of-range selection; on Android the value is clamped after confirmation, and out-of-range confirmations give error haptic feedback.
211213

212214
### Usage
213215
```xml

0 commit comments

Comments
 (0)