@@ -96,6 +96,7 @@ enum CalendarSelector {
9696 private View dayFrame ;
9797 private MaterialButton monthDropSelect ;
9898 private AccessibilityManager accessibilityManager ;
99+ @ Nullable private PagerSnapHelper pagerSnapHelper ;
99100
100101 @ NonNull
101102 public static <T > MaterialCalendar <T > newInstance (
@@ -228,6 +229,17 @@ public void onDayClick(long day) {
228229 }
229230 }
230231 }
232+ },
233+ new OnMonthNavigationListener () {
234+ @ Override
235+ public boolean onMonthNavigationPrevious () {
236+ return handleNavigateToMonthForKeyboard (/* forward= */ false , themedContext );
237+ }
238+
239+ @ Override
240+ public boolean onMonthNavigationNext () {
241+ return handleNavigateToMonthForKeyboard (/* forward= */ true , themedContext );
242+ }
231243 });
232244 recyclerView .setAdapter (monthsPagerAdapter );
233245
@@ -242,13 +254,13 @@ public void onDayClick(long day) {
242254 yearSelector .addItemDecoration (createItemDecoration ());
243255 }
244256
257+ if (!MaterialDatePicker .isFullscreen (themedContext )) {
258+ pagerSnapHelper = new PagerSnapHelper ();
259+ pagerSnapHelper .attachToRecyclerView (recyclerView );
260+ }
245261 if (root .findViewById (R .id .month_navigation_fragment_toggle ) != null ) {
246262 addActionsToMonthNavigation (root , monthsPagerAdapter );
247263 }
248-
249- if (!MaterialDatePicker .isFullscreen (themedContext )) {
250- new PagerSnapHelper ().attachToRecyclerView (recyclerView );
251- }
252264 recyclerView .scrollToPosition (monthsPagerAdapter .getPosition (current ));
253265 setUpForAccessibility ();
254266 updateAccessibilityPaneTitle (root );
@@ -334,6 +346,44 @@ CalendarConstraints getCalendarConstraints() {
334346 return calendarConstraints ;
335347 }
336348
349+ /**
350+ * Handles navigating to the adjacent month in response to keyboard navigation.
351+ *
352+ * <p>This method pages horizontally to switch months in non-fullscreen mode. In fullscreen mode,
353+ * this method returns {@code false} because months are scrolled vertically.
354+ *
355+ * @param forward {@code true} to navigate to the next month, {@code false} to navigate to the
356+ * previous month.
357+ * @param context The context.
358+ * @return {@code true} if the event was handled.
359+ */
360+ private boolean handleNavigateToMonthForKeyboard (boolean forward , @ NonNull Context context ) {
361+ if (MaterialDatePicker .isFullscreen (context )) {
362+ return false ;
363+ }
364+
365+ // Do not navigate if scroll is in progress. Return true to indicate the event was handled,
366+ // but in practice navigation is ignored during scroll.
367+ if (recyclerView .getScrollState () != RecyclerView .SCROLL_STATE_IDLE ) {
368+ return true ;
369+ }
370+
371+ MonthsPagerAdapter adapter = (MonthsPagerAdapter ) recyclerView .getAdapter ();
372+ if (adapter == null || current == null ) {
373+ return false ;
374+ }
375+
376+ int currentItem = adapter .getPosition (current );
377+ int newItem = currentItem + (forward ? 1 : -1 );
378+
379+ if (newItem >= 0 && newItem < adapter .getItemCount ()) {
380+ adapter .setKeyboardFocusDirection (forward ? View .FOCUS_FORWARD : View .FOCUS_BACKWARD );
381+ setCurrentMonth (adapter .getPageMonth (newItem ));
382+ return true ;
383+ }
384+ return false ;
385+ }
386+
337387 /**
338388 * Changes the currently displayed month to {@code moveTo}.
339389 *
@@ -361,9 +411,22 @@ void setCurrentMonth(Month moveTo) {
361411 postSmoothRecyclerViewScroll (moveToPosition );
362412 }
363413 }
414+ updateCurrentVisibleMonth ();
364415 updateNavigationButtonsEnabled (moveToPosition );
365416 }
366417
418+ private void updateCurrentVisibleMonth () {
419+ Context context = getContext ();
420+ if (context == null || MaterialDatePicker .isFullscreen (context )) {
421+ return ;
422+ }
423+
424+ MonthsPagerAdapter adapter = (MonthsPagerAdapter ) recyclerView .getAdapter ();
425+ if (adapter != null ) {
426+ adapter .setVisibleMonth (current );
427+ }
428+ }
429+
367430 @ Nullable
368431 @ Override
369432 public DateSelector <S > getDateSelector () {
@@ -379,6 +442,20 @@ interface OnDayClickListener {
379442 void onDayClick (long day );
380443 }
381444
445+ /**
446+ * Listener for month navigation events.
447+ *
448+ * <p>This listener is used by {@link MaterialCalendarGridView} to signal when keyboard navigation
449+ * reaches the start or end of a month, allowing the calendar to scroll to the previous or next
450+ * month.
451+ */
452+ interface OnMonthNavigationListener {
453+
454+ boolean onMonthNavigationPrevious ();
455+
456+ boolean onMonthNavigationNext ();
457+ }
458+
382459 /** Returns the pixel height of each {@link android.view.View} representing a day. */
383460 @ Px
384461 static int getDayHeight (@ NonNull Context context ) {
@@ -472,18 +549,34 @@ public void onInitializeAccessibilityNodeInfo(
472549 new OnScrollListener () {
473550 @ Override
474551 public void onScrolled (@ NonNull RecyclerView recyclerView , int dx , int dy ) {
475- int currentItem ;
476- if (dx < 0 ) {
477- currentItem = getLayoutManager ().findFirstVisibleItemPosition ();
478- } else {
479- currentItem = getLayoutManager ().findLastVisibleItemPosition ();
552+ int position =
553+ dx < 0
554+ ? getLayoutManager ().findFirstVisibleItemPosition ()
555+ : getLayoutManager ().findLastVisibleItemPosition ();
556+ if (pagerSnapHelper == null ) {
557+ current = monthsPagerAdapter .getPageMonth (position );
558+ }
559+ monthDropSelect .setText (monthsPagerAdapter .getPageTitle (position ));
560+ updateNavigationButtonsEnabled (position );
561+ }
562+
563+ @ Override
564+ public void onScrollStateChanged (@ NonNull RecyclerView recyclerView , int newState ) {
565+ if (newState != RecyclerView .SCROLL_STATE_IDLE || pagerSnapHelper == null ) {
566+ return ;
480567 }
481- Month moveToMonth = monthsPagerAdapter .getPageMonth (currentItem );
482- current = moveToMonth ;
483- monthDropSelect .setText (monthsPagerAdapter .getPageTitle (currentItem ));
484568
485- int currentMonthPosition = monthsPagerAdapter .getPosition (moveToMonth );
486- updateNavigationButtonsEnabled (currentMonthPosition );
569+ // If horizontal mode, find the snapped view and set it as the current month.
570+ View snapView = pagerSnapHelper .findSnapView (getLayoutManager ());
571+ if (snapView != null ) {
572+ int snapPosition = recyclerView .getChildAdapterPosition (snapView );
573+ if (snapPosition != RecyclerView .NO_POSITION ) {
574+ current = monthsPagerAdapter .getPageMonth (snapPosition );
575+ monthDropSelect .setText (monthsPagerAdapter .getPageTitle (snapPosition ));
576+ updateNavigationButtonsEnabled (snapPosition );
577+ }
578+ }
579+ updateCurrentVisibleMonth ();
487580 }
488581 });
489582
@@ -500,6 +593,7 @@ public void onClick(View view) {
500593 @ Override
501594 public void onClick (View view ) {
502595 int currentItem = getLayoutManager ().findFirstVisibleItemPosition ();
596+ monthsPagerAdapter .setKeyboardFocusDirection (View .FOCUS_FORWARD );
503597 setCurrentMonth (monthsPagerAdapter .getPageMonth (currentItem + 1 ));
504598 }
505599 });
@@ -508,6 +602,7 @@ public void onClick(View view) {
508602 @ Override
509603 public void onClick (View view ) {
510604 int currentItem = getLayoutManager ().findLastVisibleItemPosition ();
605+ monthsPagerAdapter .setKeyboardFocusDirection (View .FOCUS_BACKWARD );
511606 setCurrentMonth (monthsPagerAdapter .getPageMonth (currentItem - 1 ));
512607 }
513608 });
@@ -517,8 +612,12 @@ public void onClick(View view) {
517612 }
518613
519614 private void updateNavigationButtonsEnabled (int currentMonthPosition ) {
520- monthNext .setEnabled (currentMonthPosition + 1 < recyclerView .getAdapter ().getItemCount ());
521- monthPrev .setEnabled (currentMonthPosition - 1 >= 0 );
615+ if (monthNext != null ) {
616+ monthNext .setEnabled (currentMonthPosition + 1 < recyclerView .getAdapter ().getItemCount ());
617+ }
618+ if (monthPrev != null ) {
619+ monthPrev .setEnabled (currentMonthPosition - 1 >= 0 );
620+ }
522621 }
523622
524623 private void postSmoothRecyclerViewScroll (final int position ) {
0 commit comments