From fc195cfe1007a563e2571c8a80b3fa5697e5470f Mon Sep 17 00:00:00 2001 From: ldjesper Date: Tue, 6 Aug 2019 02:45:23 -0400 Subject: [PATCH] Fix TalkBack content descriptions for MaterialDatePicker and improve scrolling logic PiperOrigin-RevId: 261849633 --- .../android/material/picker/DateStrings.java | 25 +++++++ .../material/picker/DaysOfWeekAdapter.java | 5 ++ .../material/picker/MaterialCalendar.java | 74 +++++++++++-------- .../picker/MaterialCalendarGridView.java | 14 ++++ .../material/picker/MaterialDatePicker.java | 7 +- .../android/material/picker/MonthAdapter.java | 9 ++- .../material/picker/YearGridAdapter.java | 11 ++- .../material/picker/res/values/strings.xml | 3 + 8 files changed, 112 insertions(+), 36 deletions(-) diff --git a/lib/java/com/google/android/material/picker/DateStrings.java b/lib/java/com/google/android/material/picker/DateStrings.java index e034de7fc..d5f2eec30 100644 --- a/lib/java/com/google/android/material/picker/DateStrings.java +++ b/lib/java/com/google/android/material/picker/DateStrings.java @@ -74,6 +74,31 @@ class DateStrings { } } + static String getMonthDayOfWeekDay(long timeInMillis) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + DateFormat df = + DateFormat.getInstanceForSkeleton(DateFormat.ABBR_MONTH_WEEKDAY_DAY, Locale.getDefault()); + return df.format(new Date(timeInMillis)); + } else { + java.text.DateFormat df = + java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL, Locale.getDefault()); + return df.format(new Date(timeInMillis)); + } + } + + static String getYearMonthDayOfWeekDay(long timeInMillis) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + DateFormat df = + DateFormat.getInstanceForSkeleton( + DateFormat.YEAR_ABBR_MONTH_WEEKDAY_DAY, Locale.getDefault()); + return df.format(new Date(timeInMillis)); + } else { + java.text.DateFormat df = + java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL, Locale.getDefault()); + return df.format(new Date(timeInMillis)); + } + } + static String getDateString(long timeInMillis) { return getDateString(timeInMillis, null); } diff --git a/lib/java/com/google/android/material/picker/DaysOfWeekAdapter.java b/lib/java/com/google/android/material/picker/DaysOfWeekAdapter.java index 3805ccd9d..e65b786aa 100644 --- a/lib/java/com/google/android/material/picker/DaysOfWeekAdapter.java +++ b/lib/java/com/google/android/material/picker/DaysOfWeekAdapter.java @@ -43,6 +43,7 @@ class DaysOfWeekAdapter extends BaseAdapter { private final int firstDayOfWeek; /** Style value from Calendar.NARROW_FORMAT unavailable before 1.8 */ private static final int NARROW_FORMAT = 4; + private static final int CALENDAR_DAY_STYLE = VERSION.SDK_INT >= VERSION_CODES.O ? NARROW_FORMAT : Calendar.SHORT; @@ -84,6 +85,10 @@ class DaysOfWeekAdapter extends BaseAdapter { calendar.set(Calendar.DAY_OF_WEEK, positionToDayOfWeek(position)); dayOfWeek.setText( calendar.getDisplayName(Calendar.DAY_OF_WEEK, CALENDAR_DAY_STYLE, Locale.getDefault())); + dayOfWeek.setContentDescription( + String.format( + parent.getContext().getString(R.string.mtrl_picker_day_of_week_column_header), + calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault()))); return dayOfWeek; } diff --git a/lib/java/com/google/android/material/picker/MaterialCalendar.java b/lib/java/com/google/android/material/picker/MaterialCalendar.java index fa1aa754d..4b401aef5 100644 --- a/lib/java/com/google/android/material/picker/MaterialCalendar.java +++ b/lib/java/com/google/android/material/picker/MaterialCalendar.java @@ -68,6 +68,7 @@ public final class MaterialCalendar extends PickerFragment { private static final String GRID_SELECTOR_KEY = "GRID_SELECTOR_KEY"; private static final String CALENDAR_CONSTRAINTS_KEY = "CALENDAR_CONSTRAINTS_KEY"; private static final String CURRENT_MONTH_KEY = "CURRENT_MONTH_KEY"; + private static final int SMOOTH_SCROLL_MAX = 3; @VisibleForTesting @RestrictTo(Scope.LIBRARY_GROUP) @@ -83,7 +84,6 @@ public final class MaterialCalendar extends PickerFragment { private RecyclerView recyclerView; private View yearFrame; private View dayFrame; - private MaterialButton monthDropSelect; static MaterialCalendar newInstance( DateSelector dateSelector, int themeResId, CalendarConstraints calendarConstraints) { @@ -140,27 +140,38 @@ public final class MaterialCalendar extends PickerFragment { View root = themedInflater.inflate(layout, viewGroup, false); GridView daysHeader = root.findViewById(R.id.mtrl_calendar_days_of_week); + ViewCompat.setAccessibilityDelegate( + daysHeader, + new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo( + View view, AccessibilityNodeInfoCompat accessibilityNodeInfoCompat) { + super.onInitializeAccessibilityNodeInfo(view, accessibilityNodeInfoCompat); + // Remove announcing row/col info. + accessibilityNodeInfoCompat.setCollectionInfo(null); + } + }); daysHeader.setAdapter(new DaysOfWeekAdapter()); daysHeader.setNumColumns(earliestMonth.daysInWeek); daysHeader.setEnabled(false); - final RecyclerView monthsPager = root.findViewById(R.id.mtrl_calendar_months); + recyclerView = root.findViewById(R.id.mtrl_calendar_months); LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(), orientation, false) { @Override protected void calculateExtraLayoutSpace(@NonNull State state, @NonNull int[] ints) { if (orientation == LinearLayoutManager.HORIZONTAL) { - ints[0] = monthsPager.getWidth(); - ints[1] = monthsPager.getWidth(); + ints[0] = recyclerView.getWidth(); + ints[1] = recyclerView.getWidth(); } else { - ints[0] = monthsPager.getHeight(); - ints[1] = monthsPager.getHeight(); + ints[0] = recyclerView.getHeight(); + ints[1] = recyclerView.getHeight(); } } }; - monthsPager.setLayoutManager(layoutManager); - monthsPager.setTag(MONTHS_VIEW_GROUP_TAG); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setTag(MONTHS_VIEW_GROUP_TAG); final MonthsPagerAdapter monthsPagerAdapter = new MonthsPagerAdapter( @@ -177,14 +188,14 @@ public final class MaterialCalendar extends PickerFragment { listener.onSelectionChanged(dateSelector.getSelection()); } // TODO(b/134663744): Look into monthsPager.getAdapter().notifyItemRangeChanged(); - monthsPager.getAdapter().notifyDataSetChanged(); + recyclerView.getAdapter().notifyDataSetChanged(); if (yearSelector != null) { yearSelector.getAdapter().notifyDataSetChanged(); } } } }); - monthsPager.setAdapter(monthsPagerAdapter); + recyclerView.setAdapter(monthsPagerAdapter); int columns = themedContext.getResources().getInteger(R.integer.mtrl_calendar_year_selector_span); @@ -202,9 +213,9 @@ public final class MaterialCalendar extends PickerFragment { } if (!MaterialDatePicker.isFullscreen(themedContext)) { - new LinearSnapHelper().attachToRecyclerView(monthsPager); + new LinearSnapHelper().attachToRecyclerView(recyclerView); } - monthsPager.scrollToPosition(monthsPagerAdapter.getPosition(current)); + recyclerView.scrollToPosition(monthsPagerAdapter.getPosition(current)); return root; } @@ -275,16 +286,20 @@ public final class MaterialCalendar extends PickerFragment { * CalendarConstraints}. */ void setCurrentMonth(Month moveTo) { - setCurrentMonth(moveTo, /* smooth= */ true); - } - - void setCurrentMonth(Month moveTo, boolean smooth) { + MonthsPagerAdapter adapter = (MonthsPagerAdapter) recyclerView.getAdapter(); + int moveToPosition = adapter.getPosition(moveTo); + int distance = moveToPosition - adapter.getPosition(current); + boolean jump = Math.abs(distance) > SMOOTH_SCROLL_MAX; + boolean isForward = distance > 0; current = moveTo; - int moveToPosition = ((MonthsPagerAdapter) recyclerView.getAdapter()).getPosition(current); - if (smooth) { + if (jump && isForward) { + recyclerView.scrollToPosition(moveToPosition - SMOOTH_SCROLL_MAX); + recyclerView.smoothScrollToPosition(moveToPosition); + } else if (jump) { + recyclerView.scrollToPosition(moveToPosition + SMOOTH_SCROLL_MAX); recyclerView.smoothScrollToPosition(moveToPosition); } else { - recyclerView.scrollToPosition(moveToPosition); + recyclerView.smoothScrollToPosition(moveToPosition); } } @@ -334,8 +349,7 @@ public final class MaterialCalendar extends PickerFragment { private void addActionsToMonthNavigation( final View root, final MonthsPagerAdapter monthsPagerAdapter) { - recyclerView = root.findViewById(R.id.mtrl_calendar_months); - monthDropSelect = root.findViewById(R.id.month_navigation_fragment_toggle); + final MaterialButton monthDropSelect = root.findViewById(R.id.month_navigation_fragment_toggle); ViewCompat.setAccessibilityDelegate( monthDropSelect, new AccessibilityDelegateCompat() { @@ -362,13 +376,11 @@ public final class MaterialCalendar extends PickerFragment { new OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - LinearLayoutManager layoutManager = - (LinearLayoutManager) recyclerView.getLayoutManager(); int currentItem; if (dx < 0) { - currentItem = layoutManager.findFirstVisibleItemPosition(); + currentItem = getLayoutManager().findFirstVisibleItemPosition(); } else { - currentItem = layoutManager.findLastVisibleItemPosition(); + currentItem = getLayoutManager().findLastVisibleItemPosition(); } monthDropSelect.setText(monthsPagerAdapter.getPageTitle(currentItem)); } @@ -398,9 +410,7 @@ public final class MaterialCalendar extends PickerFragment { new OnClickListener() { @Override public void onClick(View view) { - int currentItem = - ((LinearLayoutManager) recyclerView.getLayoutManager()) - .findFirstVisibleItemPosition(); + int currentItem = getLayoutManager().findFirstVisibleItemPosition(); if (currentItem + 1 < recyclerView.getAdapter().getItemCount()) { setCurrentMonth(monthsPagerAdapter.getPageMonth(currentItem + 1)); } @@ -410,13 +420,15 @@ public final class MaterialCalendar extends PickerFragment { new OnClickListener() { @Override public void onClick(View view) { - int currentItem = - ((LinearLayoutManager) recyclerView.getLayoutManager()) - .findLastVisibleItemPosition(); + int currentItem = getLayoutManager().findLastVisibleItemPosition(); if (currentItem - 1 >= 0) { setCurrentMonth(monthsPagerAdapter.getPageMonth(currentItem - 1)); } } }); } + + LinearLayoutManager getLayoutManager() { + return (LinearLayoutManager) recyclerView.getLayoutManager(); + } } diff --git a/lib/java/com/google/android/material/picker/MaterialCalendarGridView.java b/lib/java/com/google/android/material/picker/MaterialCalendarGridView.java index 6e705f600..92b73bb45 100644 --- a/lib/java/com/google/android/material/picker/MaterialCalendarGridView.java +++ b/lib/java/com/google/android/material/picker/MaterialCalendarGridView.java @@ -21,6 +21,9 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import androidx.core.util.Pair; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; @@ -46,6 +49,17 @@ final class MaterialCalendarGridView extends GridView { setNextFocusLeftId(R.id.cancel_button); setNextFocusRightId(R.id.confirm_button); } + ViewCompat.setAccessibilityDelegate( + this, + new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo( + View view, AccessibilityNodeInfoCompat accessibilityNodeInfoCompat) { + super.onInitializeAccessibilityNodeInfo(view, accessibilityNodeInfoCompat); + // Stop announcing of row/col information in favor of internationalized day information. + accessibilityNodeInfoCompat.setCollectionInfo(null); + } + }); } @Override diff --git a/lib/java/com/google/android/material/picker/MaterialDatePicker.java b/lib/java/com/google/android/material/picker/MaterialDatePicker.java index 7c20b75c5..316ced595 100644 --- a/lib/java/com/google/android/material/picker/MaterialDatePicker.java +++ b/lib/java/com/google/android/material/picker/MaterialDatePicker.java @@ -177,6 +177,8 @@ public class MaterialDatePicker extends DialogFragment { new LayoutParams(getPaddedPickerWidth(context), getDialogPickerHeight(context))); } headerSelectionText = root.findViewById(R.id.mtrl_picker_header_selection_text); + ViewCompat.setAccessibilityLiveRegion( + headerSelectionText, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); headerToggleButton = root.findViewById(R.id.mtrl_picker_header_toggle); ((TextView) root.findViewById(R.id.mtrl_picker_title_text)).setText(titleTextResId); initHeaderToggle(context); @@ -267,7 +269,10 @@ public class MaterialDatePicker extends DialogFragment { } private void updateHeader() { - headerSelectionText.setText(getHeaderText()); + String headerText = getHeaderText(); + headerSelectionText.setContentDescription( + String.format(getString(R.string.mtrl_picker_announce_current_selection), headerText)); + headerSelectionText.setText(headerText); } private void startPickerFragment() { diff --git a/lib/java/com/google/android/material/picker/MonthAdapter.java b/lib/java/com/google/android/material/picker/MonthAdapter.java index e6bacf025..b929a4df6 100644 --- a/lib/java/com/google/android/material/picker/MonthAdapter.java +++ b/lib/java/com/google/android/material/picker/MonthAdapter.java @@ -108,9 +108,16 @@ class MonthAdapter extends BaseAdapter { day.setVisibility(View.GONE); day.setEnabled(false); } else { + int dayNumber = offsetPosition + 1; // The tag and text uniquely identify the view within the MaterialCalendar for testing - day.setText(String.valueOf(offsetPosition + 1)); day.setTag(month); + day.setText(String.valueOf(dayNumber)); + long dayInMillis = month.getDay(dayNumber); + if (month.year == Month.today().year) { + day.setContentDescription(DateStrings.getMonthDayOfWeekDay(dayInMillis)); + } else { + day.setContentDescription(DateStrings.getYearMonthDayOfWeekDay(dayInMillis)); + } day.setVisibility(View.VISIBLE); day.setEnabled(true); } diff --git a/lib/java/com/google/android/material/picker/YearGridAdapter.java b/lib/java/com/google/android/material/picker/YearGridAdapter.java index f58a78e2a..61ce85215 100644 --- a/lib/java/com/google/android/material/picker/YearGridAdapter.java +++ b/lib/java/com/google/android/material/picker/YearGridAdapter.java @@ -59,7 +59,13 @@ class YearGridAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull YearGridAdapter.ViewHolder viewHolder, int position) { int year = getYearForPosition(position); + String navigateYear = + viewHolder + .textView + .getContext() + .getString(R.string.mtrl_picker_navigate_to_year_description); viewHolder.textView.setText(String.format(Locale.getDefault(), "%d", year)); + viewHolder.textView.setContentDescription(String.format(navigateYear, year)); CalendarStyle styles = materialCalendar.getCalendarStyle(); Calendar calendar = Calendar.getInstance(); CalendarItemStyle style = calendar.get(Calendar.YEAR) == year ? styles.todayYear : styles.year; @@ -77,9 +83,8 @@ class YearGridAdapter extends RecyclerView.Adapter { return new OnClickListener() { @Override public void onClick(View view) { - Month moveTo = - Month.create(year, materialCalendar.getCalendarConstraints().getOpening().month); - materialCalendar.setCurrentMonth(moveTo, /*smooth= */ false); + Month moveTo = Month.create(year, materialCalendar.getCurrentMonth().month); + materialCalendar.setCurrentMonth(moveTo); materialCalendar.setSelector(CalendarSelector.DAY); } }; diff --git a/lib/java/com/google/android/material/picker/res/values/strings.xml b/lib/java/com/google/android/material/picker/res/values/strings.xml index 94218f5b7..5ba988c5a 100644 --- a/lib/java/com/google/android/material/picker/res/values/strings.xml +++ b/lib/java/com/google/android/material/picker/res/values/strings.xml @@ -43,5 +43,8 @@ Move to next month Tap to switch to selecting a year Tap to switch to selecting a day + Column of Days: %1$s + Current Selection: %1$s + Navigate to year %1$s