Skip to content

Commit fc195cf

Browse files
ldjcmudsn5ft
authored andcommitted
Fix TalkBack content descriptions for MaterialDatePicker and improve scrolling logic
PiperOrigin-RevId: 261849633
1 parent 3da7ddc commit fc195cf

8 files changed

Lines changed: 112 additions & 36 deletions

File tree

lib/java/com/google/android/material/picker/DateStrings.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ static String getMonthDay(Date date, Locale locale) {
7474
}
7575
}
7676

77+
static String getMonthDayOfWeekDay(long timeInMillis) {
78+
if (VERSION.SDK_INT >= VERSION_CODES.N) {
79+
DateFormat df =
80+
DateFormat.getInstanceForSkeleton(DateFormat.ABBR_MONTH_WEEKDAY_DAY, Locale.getDefault());
81+
return df.format(new Date(timeInMillis));
82+
} else {
83+
java.text.DateFormat df =
84+
java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL, Locale.getDefault());
85+
return df.format(new Date(timeInMillis));
86+
}
87+
}
88+
89+
static String getYearMonthDayOfWeekDay(long timeInMillis) {
90+
if (VERSION.SDK_INT >= VERSION_CODES.N) {
91+
DateFormat df =
92+
DateFormat.getInstanceForSkeleton(
93+
DateFormat.YEAR_ABBR_MONTH_WEEKDAY_DAY, Locale.getDefault());
94+
return df.format(new Date(timeInMillis));
95+
} else {
96+
java.text.DateFormat df =
97+
java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL, Locale.getDefault());
98+
return df.format(new Date(timeInMillis));
99+
}
100+
}
101+
77102
static String getDateString(long timeInMillis) {
78103
return getDateString(timeInMillis, null);
79104
}

lib/java/com/google/android/material/picker/DaysOfWeekAdapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DaysOfWeekAdapter extends BaseAdapter {
4343
private final int firstDayOfWeek;
4444
/** Style value from Calendar.NARROW_FORMAT unavailable before 1.8 */
4545
private static final int NARROW_FORMAT = 4;
46+
4647
private static final int CALENDAR_DAY_STYLE =
4748
VERSION.SDK_INT >= VERSION_CODES.O ? NARROW_FORMAT : Calendar.SHORT;
4849

@@ -84,6 +85,10 @@ public View getView(int position, View convertView, ViewGroup parent) {
8485
calendar.set(Calendar.DAY_OF_WEEK, positionToDayOfWeek(position));
8586
dayOfWeek.setText(
8687
calendar.getDisplayName(Calendar.DAY_OF_WEEK, CALENDAR_DAY_STYLE, Locale.getDefault()));
88+
dayOfWeek.setContentDescription(
89+
String.format(
90+
parent.getContext().getString(R.string.mtrl_picker_day_of_week_column_header),
91+
calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault())));
8792
return dayOfWeek;
8893
}
8994

lib/java/com/google/android/material/picker/MaterialCalendar.java

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ enum CalendarSelector {
6868
private static final String GRID_SELECTOR_KEY = "GRID_SELECTOR_KEY";
6969
private static final String CALENDAR_CONSTRAINTS_KEY = "CALENDAR_CONSTRAINTS_KEY";
7070
private static final String CURRENT_MONTH_KEY = "CURRENT_MONTH_KEY";
71+
private static final int SMOOTH_SCROLL_MAX = 3;
7172

7273
@VisibleForTesting
7374
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -83,7 +84,6 @@ enum CalendarSelector {
8384
private RecyclerView recyclerView;
8485
private View yearFrame;
8586
private View dayFrame;
86-
private MaterialButton monthDropSelect;
8787

8888
static <T> MaterialCalendar<T> newInstance(
8989
DateSelector<T> dateSelector, int themeResId, CalendarConstraints calendarConstraints) {
@@ -140,27 +140,38 @@ public View onCreateView(
140140

141141
View root = themedInflater.inflate(layout, viewGroup, false);
142142
GridView daysHeader = root.findViewById(R.id.mtrl_calendar_days_of_week);
143+
ViewCompat.setAccessibilityDelegate(
144+
daysHeader,
145+
new AccessibilityDelegateCompat() {
146+
@Override
147+
public void onInitializeAccessibilityNodeInfo(
148+
View view, AccessibilityNodeInfoCompat accessibilityNodeInfoCompat) {
149+
super.onInitializeAccessibilityNodeInfo(view, accessibilityNodeInfoCompat);
150+
// Remove announcing row/col info.
151+
accessibilityNodeInfoCompat.setCollectionInfo(null);
152+
}
153+
});
143154
daysHeader.setAdapter(new DaysOfWeekAdapter());
144155
daysHeader.setNumColumns(earliestMonth.daysInWeek);
145156
daysHeader.setEnabled(false);
146157

147-
final RecyclerView monthsPager = root.findViewById(R.id.mtrl_calendar_months);
158+
recyclerView = root.findViewById(R.id.mtrl_calendar_months);
148159

149160
LinearLayoutManager layoutManager =
150161
new LinearLayoutManager(getContext(), orientation, false) {
151162
@Override
152163
protected void calculateExtraLayoutSpace(@NonNull State state, @NonNull int[] ints) {
153164
if (orientation == LinearLayoutManager.HORIZONTAL) {
154-
ints[0] = monthsPager.getWidth();
155-
ints[1] = monthsPager.getWidth();
165+
ints[0] = recyclerView.getWidth();
166+
ints[1] = recyclerView.getWidth();
156167
} else {
157-
ints[0] = monthsPager.getHeight();
158-
ints[1] = monthsPager.getHeight();
168+
ints[0] = recyclerView.getHeight();
169+
ints[1] = recyclerView.getHeight();
159170
}
160171
}
161172
};
162-
monthsPager.setLayoutManager(layoutManager);
163-
monthsPager.setTag(MONTHS_VIEW_GROUP_TAG);
173+
recyclerView.setLayoutManager(layoutManager);
174+
recyclerView.setTag(MONTHS_VIEW_GROUP_TAG);
164175

165176
final MonthsPagerAdapter monthsPagerAdapter =
166177
new MonthsPagerAdapter(
@@ -177,14 +188,14 @@ public void onDayClick(long day) {
177188
listener.onSelectionChanged(dateSelector.getSelection());
178189
}
179190
// TODO(b/134663744): Look into monthsPager.getAdapter().notifyItemRangeChanged();
180-
monthsPager.getAdapter().notifyDataSetChanged();
191+
recyclerView.getAdapter().notifyDataSetChanged();
181192
if (yearSelector != null) {
182193
yearSelector.getAdapter().notifyDataSetChanged();
183194
}
184195
}
185196
}
186197
});
187-
monthsPager.setAdapter(monthsPagerAdapter);
198+
recyclerView.setAdapter(monthsPagerAdapter);
188199

189200
int columns =
190201
themedContext.getResources().getInteger(R.integer.mtrl_calendar_year_selector_span);
@@ -202,9 +213,9 @@ public void onDayClick(long day) {
202213
}
203214

204215
if (!MaterialDatePicker.isFullscreen(themedContext)) {
205-
new LinearSnapHelper().attachToRecyclerView(monthsPager);
216+
new LinearSnapHelper().attachToRecyclerView(recyclerView);
206217
}
207-
monthsPager.scrollToPosition(monthsPagerAdapter.getPosition(current));
218+
recyclerView.scrollToPosition(monthsPagerAdapter.getPosition(current));
208219
return root;
209220
}
210221

@@ -275,16 +286,20 @@ CalendarConstraints getCalendarConstraints() {
275286
* CalendarConstraints}.
276287
*/
277288
void setCurrentMonth(Month moveTo) {
278-
setCurrentMonth(moveTo, /* smooth= */ true);
279-
}
280-
281-
void setCurrentMonth(Month moveTo, boolean smooth) {
289+
MonthsPagerAdapter adapter = (MonthsPagerAdapter) recyclerView.getAdapter();
290+
int moveToPosition = adapter.getPosition(moveTo);
291+
int distance = moveToPosition - adapter.getPosition(current);
292+
boolean jump = Math.abs(distance) > SMOOTH_SCROLL_MAX;
293+
boolean isForward = distance > 0;
282294
current = moveTo;
283-
int moveToPosition = ((MonthsPagerAdapter) recyclerView.getAdapter()).getPosition(current);
284-
if (smooth) {
295+
if (jump && isForward) {
296+
recyclerView.scrollToPosition(moveToPosition - SMOOTH_SCROLL_MAX);
297+
recyclerView.smoothScrollToPosition(moveToPosition);
298+
} else if (jump) {
299+
recyclerView.scrollToPosition(moveToPosition + SMOOTH_SCROLL_MAX);
285300
recyclerView.smoothScrollToPosition(moveToPosition);
286301
} else {
287-
recyclerView.scrollToPosition(moveToPosition);
302+
recyclerView.smoothScrollToPosition(moveToPosition);
288303
}
289304
}
290305

@@ -334,8 +349,7 @@ void toggleVisibleSelector() {
334349

335350
private void addActionsToMonthNavigation(
336351
final View root, final MonthsPagerAdapter monthsPagerAdapter) {
337-
recyclerView = root.findViewById(R.id.mtrl_calendar_months);
338-
monthDropSelect = root.findViewById(R.id.month_navigation_fragment_toggle);
352+
final MaterialButton monthDropSelect = root.findViewById(R.id.month_navigation_fragment_toggle);
339353
ViewCompat.setAccessibilityDelegate(
340354
monthDropSelect,
341355
new AccessibilityDelegateCompat() {
@@ -362,13 +376,11 @@ public void onInitializeAccessibilityNodeInfo(
362376
new OnScrollListener() {
363377
@Override
364378
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
365-
LinearLayoutManager layoutManager =
366-
(LinearLayoutManager) recyclerView.getLayoutManager();
367379
int currentItem;
368380
if (dx < 0) {
369-
currentItem = layoutManager.findFirstVisibleItemPosition();
381+
currentItem = getLayoutManager().findFirstVisibleItemPosition();
370382
} else {
371-
currentItem = layoutManager.findLastVisibleItemPosition();
383+
currentItem = getLayoutManager().findLastVisibleItemPosition();
372384
}
373385
monthDropSelect.setText(monthsPagerAdapter.getPageTitle(currentItem));
374386
}
@@ -398,9 +410,7 @@ public void onClick(View view) {
398410
new OnClickListener() {
399411
@Override
400412
public void onClick(View view) {
401-
int currentItem =
402-
((LinearLayoutManager) recyclerView.getLayoutManager())
403-
.findFirstVisibleItemPosition();
413+
int currentItem = getLayoutManager().findFirstVisibleItemPosition();
404414
if (currentItem + 1 < recyclerView.getAdapter().getItemCount()) {
405415
setCurrentMonth(monthsPagerAdapter.getPageMonth(currentItem + 1));
406416
}
@@ -410,13 +420,15 @@ public void onClick(View view) {
410420
new OnClickListener() {
411421
@Override
412422
public void onClick(View view) {
413-
int currentItem =
414-
((LinearLayoutManager) recyclerView.getLayoutManager())
415-
.findLastVisibleItemPosition();
423+
int currentItem = getLayoutManager().findLastVisibleItemPosition();
416424
if (currentItem - 1 >= 0) {
417425
setCurrentMonth(monthsPagerAdapter.getPageMonth(currentItem - 1));
418426
}
419427
}
420428
});
421429
}
430+
431+
LinearLayoutManager getLayoutManager() {
432+
return (LinearLayoutManager) recyclerView.getLayoutManager();
433+
}
422434
}

lib/java/com/google/android/material/picker/MaterialCalendarGridView.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import android.graphics.Canvas;
2222
import android.graphics.Rect;
2323
import androidx.core.util.Pair;
24+
import androidx.core.view.AccessibilityDelegateCompat;
25+
import androidx.core.view.ViewCompat;
26+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
2427
import android.util.AttributeSet;
2528
import android.view.KeyEvent;
2629
import android.view.View;
@@ -46,6 +49,17 @@ public MaterialCalendarGridView(Context context, AttributeSet attrs, int defStyl
4649
setNextFocusLeftId(R.id.cancel_button);
4750
setNextFocusRightId(R.id.confirm_button);
4851
}
52+
ViewCompat.setAccessibilityDelegate(
53+
this,
54+
new AccessibilityDelegateCompat() {
55+
@Override
56+
public void onInitializeAccessibilityNodeInfo(
57+
View view, AccessibilityNodeInfoCompat accessibilityNodeInfoCompat) {
58+
super.onInitializeAccessibilityNodeInfo(view, accessibilityNodeInfoCompat);
59+
// Stop announcing of row/col information in favor of internationalized day information.
60+
accessibilityNodeInfoCompat.setCollectionInfo(null);
61+
}
62+
});
4963
}
5064

5165
@Override

lib/java/com/google/android/material/picker/MaterialDatePicker.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ public final View onCreateView(
177177
new LayoutParams(getPaddedPickerWidth(context), getDialogPickerHeight(context)));
178178
}
179179
headerSelectionText = root.findViewById(R.id.mtrl_picker_header_selection_text);
180+
ViewCompat.setAccessibilityLiveRegion(
181+
headerSelectionText, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
180182
headerToggleButton = root.findViewById(R.id.mtrl_picker_header_toggle);
181183
((TextView) root.findViewById(R.id.mtrl_picker_title_text)).setText(titleTextResId);
182184
initHeaderToggle(context);
@@ -267,7 +269,10 @@ public final S getSelection() {
267269
}
268270

269271
private void updateHeader() {
270-
headerSelectionText.setText(getHeaderText());
272+
String headerText = getHeaderText();
273+
headerSelectionText.setContentDescription(
274+
String.format(getString(R.string.mtrl_picker_announce_current_selection), headerText));
275+
headerSelectionText.setText(headerText);
271276
}
272277

273278
private void startPickerFragment() {

lib/java/com/google/android/material/picker/MonthAdapter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,16 @@ public TextView getView(int position, View convertView, ViewGroup parent) {
108108
day.setVisibility(View.GONE);
109109
day.setEnabled(false);
110110
} else {
111+
int dayNumber = offsetPosition + 1;
111112
// The tag and text uniquely identify the view within the MaterialCalendar for testing
112-
day.setText(String.valueOf(offsetPosition + 1));
113113
day.setTag(month);
114+
day.setText(String.valueOf(dayNumber));
115+
long dayInMillis = month.getDay(dayNumber);
116+
if (month.year == Month.today().year) {
117+
day.setContentDescription(DateStrings.getMonthDayOfWeekDay(dayInMillis));
118+
} else {
119+
day.setContentDescription(DateStrings.getYearMonthDayOfWeekDay(dayInMillis));
120+
}
114121
day.setVisibility(View.VISIBLE);
115122
day.setEnabled(true);
116123
}

lib/java/com/google/android/material/picker/YearGridAdapter.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ public YearGridAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGrou
5959
@Override
6060
public void onBindViewHolder(@NonNull YearGridAdapter.ViewHolder viewHolder, int position) {
6161
int year = getYearForPosition(position);
62+
String navigateYear =
63+
viewHolder
64+
.textView
65+
.getContext()
66+
.getString(R.string.mtrl_picker_navigate_to_year_description);
6267
viewHolder.textView.setText(String.format(Locale.getDefault(), "%d", year));
68+
viewHolder.textView.setContentDescription(String.format(navigateYear, year));
6369
CalendarStyle styles = materialCalendar.getCalendarStyle();
6470
Calendar calendar = Calendar.getInstance();
6571
CalendarItemStyle style = calendar.get(Calendar.YEAR) == year ? styles.todayYear : styles.year;
@@ -77,9 +83,8 @@ private OnClickListener createYearClickListener(final int year) {
7783
return new OnClickListener() {
7884
@Override
7985
public void onClick(View view) {
80-
Month moveTo =
81-
Month.create(year, materialCalendar.getCalendarConstraints().getOpening().month);
82-
materialCalendar.setCurrentMonth(moveTo, /*smooth= */ false);
86+
Month moveTo = Month.create(year, materialCalendar.getCurrentMonth().month);
87+
materialCalendar.setCurrentMonth(moveTo);
8388
materialCalendar.setSelector(CalendarSelector.DAY);
8489
}
8590
};

lib/java/com/google/android/material/picker/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,8 @@
4343
<string name="mtrl_picker_a11y_next_month" description="a11y string to indicate this button moves the calendar to the next month [CHAR_LIMIT=NONE]">Move to next month</string>
4444
<string name="mtrl_picker_toggle_to_year_selection" description="a11y string to indicate this button switches the user to choosing a year [CHAR_LIMIT=NONE]">Tap to switch to selecting a year</string>
4545
<string name="mtrl_picker_toggle_to_day_selection" description="a11y string to indicate this button switches the user to choosing a day [CHAR_LIMIT=NONE]">Tap to switch to selecting a day</string>
46+
<string name="mtrl_picker_day_of_week_column_header" description="a11y string to indicate this is a header for a column of days for one day of the week (e.g., Monday) [CHAR_LIMIT=NONE]">Column of Days: %1$s</string>
47+
<string name="mtrl_picker_announce_current_selection" description="a11y string read on selection change to indicate the new selection [CHAR_LIMIT=NONE]">Current Selection: %1$s</string>
48+
<string name="mtrl_picker_navigate_to_year_description" description="a11y string that informs the user that tapping this button will switch the year [CHAR_LIMIT=NONE]">Navigate to year %1$s</string>
4649

4750
</resources>

0 commit comments

Comments
 (0)