1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.wallpaper.widget; 17 18 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED; 19 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; 20 21 import android.app.Activity; 22 import android.content.Context; 23 import android.text.TextUtils; 24 import android.util.AttributeSet; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.view.accessibility.AccessibilityEvent; 29 import android.widget.FrameLayout; 30 import android.widget.ImageView; 31 32 import androidx.annotation.LayoutRes; 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.core.widget.ImageViewCompat; 36 37 import com.android.internal.util.ArrayUtils; 38 import com.android.wallpaper.R; 39 import com.android.wallpaper.util.ResourceUtils; 40 import com.android.wallpaper.util.SizeCalculator; 41 42 import com.google.android.material.bottomsheet.BottomSheetBehavior; 43 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback; 44 45 import java.util.ArrayDeque; 46 import java.util.Arrays; 47 import java.util.Deque; 48 import java.util.EnumMap; 49 import java.util.HashSet; 50 import java.util.Map; 51 import java.util.Set; 52 53 /** A {@code ViewGroup} which provides the specific actions for the user to interact with. */ 54 public class BottomActionBar extends FrameLayout { 55 56 /** 57 * Interface to be implemented by an Activity hosting a {@link BottomActionBar} 58 */ 59 public interface BottomActionBarHost { 60 /** Gets {@link BottomActionBar}. */ getBottomActionBar()61 BottomActionBar getBottomActionBar(); 62 } 63 64 /** 65 * The listener for {@link BottomActionBar} visibility change notification. 66 */ 67 public interface VisibilityChangeListener { 68 /** 69 * Called when {@link BottomActionBar} visibility changes. 70 * 71 * @param isVisible {@code true} if it's visible; {@code false} otherwise. 72 */ onVisibilityChange(boolean isVisible)73 void onVisibilityChange(boolean isVisible); 74 } 75 76 /** This listens to changes to an action view's selected state. */ 77 public interface OnActionSelectedListener { 78 79 /** 80 * This is called when an action view's selected state changes. 81 * @param selected whether the action view is selected. 82 */ onActionSelected(boolean selected)83 void onActionSelected(boolean selected); 84 } 85 86 /** 87 * A Callback to notify the registrant to change it's accessibility param when 88 * {@link BottomActionBar} state changes. 89 */ 90 public interface AccessibilityCallback { 91 /** 92 * Called when {@link BottomActionBar} collapsed. 93 */ onBottomSheetCollapsed()94 void onBottomSheetCollapsed(); 95 96 /** 97 * Called when {@link BottomActionBar} expanded. 98 */ onBottomSheetExpanded()99 void onBottomSheetExpanded(); 100 } 101 102 /** 103 * Object to host content view for bottom sheet to display. 104 * 105 * <p> The view would be created in the constructor. 106 */ 107 public static abstract class BottomSheetContent<T extends View> { 108 109 private T mContentView; 110 private boolean mIsVisible; 111 BottomSheetContent(Context context)112 public BottomSheetContent(Context context) { 113 mContentView = createView(context); 114 setVisibility(false); 115 } 116 117 /** Gets the view id to inflate. */ 118 @LayoutRes getViewId()119 public abstract int getViewId(); 120 121 /** Gets called when the content view is created. */ onViewCreated(T view)122 public abstract void onViewCreated(T view); 123 124 /** Gets called when the current content view is going to recreate. */ onRecreateView(T oldView)125 public void onRecreateView(T oldView) {} 126 recreateView(Context context)127 private void recreateView(Context context) { 128 // Inform that the view is going to recreate. 129 onRecreateView(mContentView); 130 // Create a new view with the given context. 131 mContentView = createView(context); 132 setVisibility(mIsVisible); 133 } 134 createView(Context context)135 private T createView(Context context) { 136 T contentView = (T) LayoutInflater.from(context).inflate(getViewId(), null); 137 onViewCreated(contentView); 138 contentView.setFocusable(true); 139 return contentView; 140 } 141 setVisibility(boolean isVisible)142 private void setVisibility(boolean isVisible) { 143 mIsVisible = isVisible; 144 mContentView.setVisibility(mIsVisible ? VISIBLE : GONE); 145 } 146 } 147 148 // TODO(b/154299462): Separate downloadable related actions from WallpaperPicker. 149 /** The action items in the bottom action bar. */ 150 public enum BottomAction { 151 ROTATION, 152 DELETE, 153 INFORMATION(R.string.accessibility_info_shown, R.string.accessibility_info_hidden), 154 EDIT, 155 CUSTOMIZE(R.string.accessibility_customize_shown, R.string.accessibility_customize_hidden), 156 DOWNLOAD, 157 PROGRESS, 158 APPLY, 159 APPLY_TEXT; 160 161 private final int mShownAccessibilityResId; 162 private final int mHiddenAccessibilityResId; 163 BottomAction()164 BottomAction() { 165 this(/* shownAccessibilityLabelResId= */ 0, /* shownAccessibilityLabelResId= */ 0); 166 } 167 BottomAction(int shownAccessibilityLabelResId, int hiddenAccessibilityLabelResId)168 BottomAction(int shownAccessibilityLabelResId, int hiddenAccessibilityLabelResId) { 169 mShownAccessibilityResId = shownAccessibilityLabelResId; 170 mHiddenAccessibilityResId = hiddenAccessibilityLabelResId; 171 } 172 173 /** 174 * Returns the string resource id of the currently bottom action for its shown or hidden 175 * state. 176 */ getAccessibilityStringRes(boolean isShown)177 public int getAccessibilityStringRes(boolean isShown) { 178 return isShown ? mShownAccessibilityResId : mHiddenAccessibilityResId; 179 } 180 } 181 182 private final Map<BottomAction, View> mActionMap = new EnumMap<>(BottomAction.class); 183 private final Map<BottomAction, BottomSheetContent<?>> mContentViewMap = 184 new EnumMap<>(BottomAction.class); 185 private final Map<BottomAction, OnActionSelectedListener> mActionSelectedListeners = 186 new EnumMap<>(BottomAction.class); 187 188 private final ViewGroup mBottomSheetView; 189 private final QueueStateBottomSheetBehavior<ViewGroup> mBottomSheetBehavior; 190 private final Set<VisibilityChangeListener> mVisibilityChangeListeners = new HashSet<>(); 191 192 // The current selected action in the BottomActionBar, can be null when no action is selected. 193 @Nullable private BottomAction mSelectedAction; 194 // The last selected action in the BottomActionBar. 195 @Nullable private BottomAction mLastSelectedAction; 196 @Nullable private AccessibilityCallback mAccessibilityCallback; 197 BottomActionBar(@onNull Context context, @Nullable AttributeSet attrs)198 public BottomActionBar(@NonNull Context context, @Nullable AttributeSet attrs) { 199 super(context, attrs); 200 LayoutInflater.from(context).inflate(R.layout.bottom_actions_layout, this, true); 201 202 mActionMap.put(BottomAction.ROTATION, findViewById(R.id.action_rotation)); 203 mActionMap.put(BottomAction.DELETE, findViewById(R.id.action_delete)); 204 mActionMap.put(BottomAction.INFORMATION, findViewById(R.id.action_information)); 205 mActionMap.put(BottomAction.EDIT, findViewById(R.id.action_edit)); 206 mActionMap.put(BottomAction.CUSTOMIZE, findViewById(R.id.action_customize)); 207 mActionMap.put(BottomAction.DOWNLOAD, findViewById(R.id.action_download)); 208 mActionMap.put(BottomAction.PROGRESS, findViewById(R.id.action_progress)); 209 mActionMap.put(BottomAction.APPLY, findViewById(R.id.action_apply)); 210 mActionMap.put(BottomAction.APPLY_TEXT, findViewById(R.id.action_apply_text_button)); 211 212 mBottomSheetView = findViewById(R.id.action_bottom_sheet); 213 SizeCalculator.adjustBackgroundCornerRadius(mBottomSheetView); 214 setColor(context); 215 216 mBottomSheetBehavior = (QueueStateBottomSheetBehavior<ViewGroup>) BottomSheetBehavior.from( 217 mBottomSheetView); 218 mBottomSheetBehavior.setState(STATE_COLLAPSED); 219 mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetCallback() { 220 @Override 221 public void onStateChanged(@NonNull View bottomSheet, int newState) { 222 if (mBottomSheetBehavior.isQueueProcessing()) { 223 // Avoid button and bottom sheet mismatching from quick tapping buttons when 224 // bottom sheet is changing state. 225 disableActions(); 226 // If bottom sheet is going with expanded-collapsed-expanded, the new content 227 // will be updated in collapsed state. The first state change from expanded to 228 // collapsed should still show the previous content view. 229 if (mSelectedAction != null && newState == STATE_COLLAPSED) { 230 updateContentViewFor(mSelectedAction); 231 } 232 return; 233 } 234 235 notifyAccessibilityCallback(newState); 236 237 // Enable all buttons when queue is not processing. 238 enableActions(); 239 if (!isExpandable(mSelectedAction)) { 240 return; 241 } 242 // Ensure the button state is the same as bottom sheet state to catch up the state 243 // change from dragging or some unexpected bottom sheet state changes. 244 if (newState == STATE_COLLAPSED) { 245 updateSelectedState(mSelectedAction, /* selected= */ false); 246 } else if (newState == STATE_EXPANDED) { 247 updateSelectedState(mSelectedAction, /* selected= */ true); 248 } 249 } 250 @Override 251 public void onSlide(@NonNull View bottomSheet, float slideOffset) { } 252 }); 253 254 setOnApplyWindowInsetsListener((v, windowInsets) -> { 255 v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), 256 windowInsets.getSystemWindowInsetBottom()); 257 return windowInsets; 258 }); 259 260 // Skip "info selected" and "customize selected" Talkback while double tapping on info and 261 // customize action. 262 skipAccessibilityEvent(new BottomAction[]{BottomAction.INFORMATION, BottomAction.CUSTOMIZE}, 263 new int[]{AccessibilityEvent.TYPE_VIEW_CLICKED, 264 AccessibilityEvent.TYPE_VIEW_SELECTED}); 265 } 266 267 @Override onVisibilityAggregated(boolean isVisible)268 public void onVisibilityAggregated(boolean isVisible) { 269 super.onVisibilityAggregated(isVisible); 270 mVisibilityChangeListeners.forEach(listener -> listener.onVisibilityChange(isVisible)); 271 } 272 273 /** 274 * Binds the {@code bottomSheetContent} with the {@code action}, the {@code action} button 275 * would be able to expand/collapse the bottom sheet to show the content. 276 * 277 * @param bottomSheetContent the content object with view being added to the bottom sheet 278 * @param action the action to be bound to expand / collapse the bottom sheet 279 */ bindBottomSheetContentWithAction(BottomSheetContent<?> bottomSheetContent, BottomAction action)280 public void bindBottomSheetContentWithAction(BottomSheetContent<?> bottomSheetContent, 281 BottomAction action) { 282 mContentViewMap.put(action, bottomSheetContent); 283 mBottomSheetView.addView(bottomSheetContent.mContentView); 284 setActionClickListener(action, actionView -> { 285 if (mBottomSheetBehavior.getState() == STATE_COLLAPSED) { 286 updateContentViewFor(action); 287 } 288 mBottomSheetView.setAccessibilityTraversalAfter(actionView.getId()); 289 }); 290 } 291 292 /** Collapses the bottom sheet. */ collapseBottomSheetIfExpanded()293 public void collapseBottomSheetIfExpanded() { 294 hideBottomSheetAndDeselectButtonIfExpanded(); 295 } 296 297 /** Enables or disables action buttons that show the bottom sheet. */ enableActionButtonsWithBottomSheet(boolean enabled)298 public void enableActionButtonsWithBottomSheet(boolean enabled) { 299 if (enabled) { 300 enableActions(mContentViewMap.keySet().toArray(new BottomAction[0])); 301 } else { 302 disableActions(mContentViewMap.keySet().toArray(new BottomAction[0])); 303 } 304 } 305 306 /** 307 * Sets a click listener to a specific action. 308 * 309 * @param bottomAction the specific action 310 * @param actionClickListener the click listener for the action 311 */ setActionClickListener( BottomAction bottomAction, OnClickListener actionClickListener)312 public void setActionClickListener( 313 BottomAction bottomAction, OnClickListener actionClickListener) { 314 View buttonView = mActionMap.get(bottomAction); 315 if (buttonView.hasOnClickListeners()) { 316 throw new IllegalStateException( 317 "Had already set a click listener to button: " + bottomAction); 318 } 319 buttonView.setOnClickListener(view -> { 320 if (mSelectedAction != null && isActionSelected(mSelectedAction)) { 321 updateSelectedState(mSelectedAction, /* selected= */ false); 322 if (isExpandable(mSelectedAction)) { 323 mBottomSheetBehavior.enqueue(STATE_COLLAPSED); 324 } 325 } else { 326 // Error handling, set to null if the action is not selected. 327 mSelectedAction = null; 328 } 329 330 if (bottomAction == mSelectedAction) { 331 // Deselect the selected action. 332 mSelectedAction = null; 333 } else { 334 // Select a different action from the current selected action. 335 // Also keep the same action for unselected case for a11y. 336 mLastSelectedAction = mSelectedAction = bottomAction; 337 updateSelectedState(mSelectedAction, /* selected= */ true); 338 if (isExpandable(mSelectedAction)) { 339 mBottomSheetBehavior.enqueue(STATE_EXPANDED); 340 } 341 } 342 actionClickListener.onClick(view); 343 mBottomSheetBehavior.processQueueForStateChange(); 344 }); 345 } 346 347 /** 348 * Sets a selected listener to a specific action. This is triggered each time the bottom 349 * action's selected state changes. 350 * 351 * @param bottomAction the specific action 352 * @param actionSelectedListener the selected listener for the action 353 */ setActionSelectedListener( BottomAction bottomAction, OnActionSelectedListener actionSelectedListener)354 public void setActionSelectedListener( 355 BottomAction bottomAction, OnActionSelectedListener actionSelectedListener) { 356 if (mActionSelectedListeners.containsKey(bottomAction)) { 357 throw new IllegalStateException( 358 "Had already set a selected listener to button: " + bottomAction); 359 } 360 mActionSelectedListeners.put(bottomAction, actionSelectedListener); 361 } 362 363 /** Set back button visibility. */ setBackButtonVisibility(int visibility)364 public void setBackButtonVisibility(int visibility) { 365 findViewById(R.id.action_back).setVisibility(visibility); 366 } 367 368 /** Binds the cancel button to back key. */ bindBackButtonToSystemBackKey(Activity activity)369 public void bindBackButtonToSystemBackKey(Activity activity) { 370 findViewById(R.id.action_back).setOnClickListener(v -> activity.onBackPressed()); 371 } 372 373 /** Returns {@code true} if visible. */ isVisible()374 public boolean isVisible() { 375 return getVisibility() == VISIBLE; 376 } 377 378 /** Shows {@link BottomActionBar}. */ show()379 public void show() { 380 setVisibility(VISIBLE); 381 } 382 383 /** Hides {@link BottomActionBar}. */ hide()384 public void hide() { 385 setVisibility(GONE); 386 } 387 388 /** 389 * Adds the visibility change listener. 390 * 391 * @param visibilityChangeListener the listener to be notified. 392 */ addVisibilityChangeListener(VisibilityChangeListener visibilityChangeListener)393 public void addVisibilityChangeListener(VisibilityChangeListener visibilityChangeListener) { 394 if (visibilityChangeListener == null) { 395 return; 396 } 397 mVisibilityChangeListeners.add(visibilityChangeListener); 398 visibilityChangeListener.onVisibilityChange(isVisible()); 399 } 400 401 /** 402 * Sets a AccessibilityCallback. 403 * 404 * @param accessibilityCallback the callback to be notified. 405 */ setAccessibilityCallback(@ullable AccessibilityCallback accessibilityCallback)406 public void setAccessibilityCallback(@Nullable AccessibilityCallback accessibilityCallback) { 407 mAccessibilityCallback = accessibilityCallback; 408 } 409 410 /** 411 * Shows the specific actions. 412 * 413 * @param actions the specific actions 414 */ showActions(BottomAction... actions)415 public void showActions(BottomAction... actions) { 416 for (BottomAction action : actions) { 417 mActionMap.get(action).setVisibility(VISIBLE); 418 } 419 } 420 421 /** 422 * Hides the specific actions. 423 * 424 * @param actions the specific actions 425 */ hideActions(BottomAction... actions)426 public void hideActions(BottomAction... actions) { 427 for (BottomAction action : actions) { 428 mActionMap.get(action).setVisibility(GONE); 429 430 if (isExpandable(action) && mSelectedAction == action) { 431 hideBottomSheetAndDeselectButtonIfExpanded(); 432 } 433 } 434 } 435 436 /** 437 * Focus the specific action. 438 * 439 * @param action the specific action 440 */ focusAccessibilityAction(BottomAction action)441 public void focusAccessibilityAction(BottomAction action) { 442 mActionMap.get(action).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 443 } 444 445 /** 446 * Shows the specific actions only. In other words, the other actions will be hidden. 447 * 448 * @param actions the specific actions which will be shown. Others will be hidden. 449 */ showActionsOnly(BottomAction... actions)450 public void showActionsOnly(BottomAction... actions) { 451 final Set<BottomAction> actionsSet = new HashSet<>(Arrays.asList(actions)); 452 453 mActionMap.keySet().forEach(action -> { 454 if (actionsSet.contains(action)) { 455 showActions(action); 456 } else { 457 hideActions(action); 458 } 459 }); 460 } 461 462 /** 463 * Checks if the specific actions are shown. 464 * 465 * @param actions the specific actions to be verified 466 * @return {@code true} if the actions are shown; {@code false} otherwise 467 */ areActionsShown(BottomAction... actions)468 public boolean areActionsShown(BottomAction... actions) { 469 final Set<BottomAction> actionsSet = new HashSet<>(Arrays.asList(actions)); 470 return actionsSet.stream().allMatch(bottomAction -> { 471 View view = mActionMap.get(bottomAction); 472 return view != null && view.getVisibility() == VISIBLE; 473 }); 474 } 475 476 /** 477 * All actions will be hidden. 478 */ hideAllActions()479 public void hideAllActions() { 480 showActionsOnly(/* No actions to show */); 481 } 482 483 /** Enables all the actions' {@link View}. */ enableActions()484 public void enableActions() { 485 enableActions(BottomAction.values()); 486 } 487 488 /** Disables all the actions' {@link View}. */ disableActions()489 public void disableActions() { 490 disableActions(BottomAction.values()); 491 } 492 493 /** 494 * Enables specified actions' {@link View}. 495 * 496 * @param actions the specified actions to enable their views 497 */ enableActions(BottomAction... actions)498 public void enableActions(BottomAction... actions) { 499 for (BottomAction action : actions) { 500 mActionMap.get(action).setEnabled(true); 501 } 502 } 503 504 /** 505 * Disables specified actions' {@link View}. 506 * 507 * @param actions the specified actions to disable their views 508 */ disableActions(BottomAction... actions)509 public void disableActions(BottomAction... actions) { 510 for (BottomAction action : actions) { 511 mActionMap.get(action).setEnabled(false); 512 } 513 } 514 515 /** Sets a default selected action button. */ setDefaultSelectedButton(BottomAction action)516 public void setDefaultSelectedButton(BottomAction action) { 517 if (mSelectedAction == null) { 518 mSelectedAction = action; 519 updateSelectedState(mSelectedAction, /* selected= */ true); 520 } 521 } 522 523 /** Deselects an action button. */ deselectAction(BottomAction action)524 public void deselectAction(BottomAction action) { 525 if (isExpandable(action)) { 526 mBottomSheetBehavior.setState(STATE_COLLAPSED); 527 } 528 updateSelectedState(action, /* selected= */ false); 529 if (action == mSelectedAction) { 530 mSelectedAction = null; 531 } 532 } 533 isActionSelected(BottomAction action)534 public boolean isActionSelected(BottomAction action) { 535 return mActionMap.get(action).isSelected(); 536 } 537 538 /** Returns {@code true} if the state of bottom sheet is collapsed. */ isBottomSheetCollapsed()539 public boolean isBottomSheetCollapsed() { 540 return mBottomSheetBehavior.getState() == STATE_COLLAPSED; 541 } 542 543 /** Resets {@link BottomActionBar} to initial state. */ reset()544 public void reset() { 545 // Not visible by default, see res/layout/bottom_action_bar.xml 546 hide(); 547 // All actions are hide and enabled by default, see res/layout/bottom_action_bar.xml 548 hideAllActions(); 549 enableActions(); 550 // Clears all the actions' click listeners 551 mActionMap.values().forEach(v -> v.setOnClickListener(null)); 552 findViewById(R.id.action_back).setOnClickListener(null); 553 // Deselect all buttons. 554 mActionMap.keySet().forEach(a -> updateSelectedState(a, /* selected= */ false)); 555 // Clear values. 556 mContentViewMap.clear(); 557 mActionSelectedListeners.clear(); 558 mBottomSheetView.removeAllViews(); 559 mBottomSheetBehavior.reset(); 560 mSelectedAction = null; 561 } 562 563 /** Dynamic update color with {@code Context}. */ setColor(Context context)564 public void setColor(Context context) { 565 // Set bottom sheet background. 566 mBottomSheetView.setBackground(context.getDrawable(R.drawable.bottom_sheet_background)); 567 if (mBottomSheetView.getChildCount() > 0) { 568 // Update the bottom sheet content view if any. 569 mBottomSheetView.removeAllViews(); 570 mContentViewMap.values().forEach(bottomSheetContent -> { 571 bottomSheetContent.recreateView(context); 572 mBottomSheetView.addView(bottomSheetContent.mContentView); 573 }); 574 } 575 576 // Set the bar background and action buttons. 577 ViewGroup actionTabs = findViewById(R.id.action_tabs); 578 actionTabs.setBackgroundColor( 579 ResourceUtils.getColorAttr(context, android.R.attr.colorBackground)); 580 for (int i = 0; i < actionTabs.getChildCount(); i++) { 581 View v = actionTabs.getChildAt(i); 582 if (v instanceof ImageView) { 583 v.setBackground(context.getDrawable(R.drawable.bottom_action_button_background)); 584 ImageViewCompat.setImageTintList((ImageView) v, 585 context.getColorStateList(R.color.bottom_action_button_color_tint)); 586 } 587 } 588 } 589 updateSelectedState(BottomAction bottomAction, boolean selected)590 private void updateSelectedState(BottomAction bottomAction, boolean selected) { 591 View bottomActionView = mActionMap.get(bottomAction); 592 if (bottomActionView.isSelected() == selected) { 593 return; 594 } 595 596 OnActionSelectedListener listener = mActionSelectedListeners.get(bottomAction); 597 if (listener != null) { 598 listener.onActionSelected(selected); 599 } 600 bottomActionView.setSelected(selected); 601 } 602 hideBottomSheetAndDeselectButtonIfExpanded()603 private void hideBottomSheetAndDeselectButtonIfExpanded() { 604 if (isExpandable(mSelectedAction) && mBottomSheetBehavior.getState() == STATE_EXPANDED) { 605 mBottomSheetBehavior.setState(STATE_COLLAPSED); 606 updateSelectedState(mSelectedAction, /* selected= */ false); 607 mSelectedAction = null; 608 } 609 } 610 updateContentViewFor(BottomAction action)611 private void updateContentViewFor(BottomAction action) { 612 mContentViewMap.forEach((a, content) -> content.setVisibility(a.equals(action))); 613 } 614 isExpandable(BottomAction action)615 private boolean isExpandable(BottomAction action) { 616 return action != null && mContentViewMap.containsKey(action); 617 } 618 notifyAccessibilityCallback(int state)619 private void notifyAccessibilityCallback(int state) { 620 if (mAccessibilityCallback == null) { 621 return; 622 } 623 624 if (state == STATE_COLLAPSED) { 625 CharSequence text = getAccessibilityText(mLastSelectedAction, /* isShown= */ false); 626 if (!TextUtils.isEmpty(text)) { 627 setAccessibilityPaneTitle(text); 628 } 629 mAccessibilityCallback.onBottomSheetCollapsed(); 630 } else if (state == STATE_EXPANDED) { 631 CharSequence text = getAccessibilityText(mSelectedAction, /* isShown= */ true); 632 if (!TextUtils.isEmpty(text)) { 633 setAccessibilityPaneTitle(text); 634 } 635 mAccessibilityCallback.onBottomSheetExpanded(); 636 } 637 } 638 getAccessibilityText(BottomAction action, boolean isShown)639 private CharSequence getAccessibilityText(BottomAction action, boolean isShown) { 640 if (action == null) { 641 return null; 642 } 643 int resId = action.getAccessibilityStringRes(isShown); 644 if (resId != 0) { 645 return mContext.getText(resId); 646 } 647 return null; 648 } 649 650 /** 651 * Skip bottom action's Accessibility event. 652 * 653 * @param actions the {@link BottomAction} actions to be skipped. 654 * @param eventTypes the {@link AccessibilityEvent} event types to be skipped. 655 */ skipAccessibilityEvent(BottomAction[] actions, int[] eventTypes)656 private void skipAccessibilityEvent(BottomAction[] actions, int[] eventTypes) { 657 for (BottomAction action : actions) { 658 View view = mActionMap.get(action); 659 view.setAccessibilityDelegate(new AccessibilityDelegate() { 660 @Override 661 public void sendAccessibilityEvent(View host, int eventType) { 662 if (!ArrayUtils.contains(eventTypes, eventType)) { 663 super.sendAccessibilityEvent(host, eventType); 664 } 665 } 666 }); 667 } 668 } 669 670 /** A {@link BottomSheetBehavior} that can process a queue of bottom sheet states.*/ 671 public static class QueueStateBottomSheetBehavior<V extends View> 672 extends BottomSheetBehavior<V> { 673 674 private final Deque<Integer> mStateQueue = new ArrayDeque<>(); 675 private boolean mIsQueueProcessing; 676 QueueStateBottomSheetBehavior(Context context, @Nullable AttributeSet attrs)677 public QueueStateBottomSheetBehavior(Context context, @Nullable AttributeSet attrs) { 678 super(context, attrs); 679 // Binds the default callback for processing queue. 680 setBottomSheetCallback(null); 681 } 682 683 /** Enqueues the bottom sheet states. */ enqueue(int state)684 public void enqueue(int state) { 685 if (!mStateQueue.isEmpty() && mStateQueue.getLast() == state) { 686 return; 687 } 688 mStateQueue.add(state); 689 } 690 691 /** Processes the queue of bottom sheet state that was set via {@link #enqueue}. */ processQueueForStateChange()692 public void processQueueForStateChange() { 693 if (mStateQueue.isEmpty()) { 694 return; 695 } 696 setState(mStateQueue.getFirst()); 697 mIsQueueProcessing = true; 698 } 699 700 /** 701 * Returns {@code true} if the queue is processing. For example, if the bottom sheet is 702 * going with expanded-collapsed-expanded, it would return {@code true} until last expanded 703 * state is finished. 704 */ isQueueProcessing()705 public boolean isQueueProcessing() { 706 return mIsQueueProcessing; 707 } 708 709 /** Resets the queue state. */ reset()710 public void reset() { 711 mStateQueue.clear(); 712 mIsQueueProcessing = false; 713 } 714 715 @Override setBottomSheetCallback(BottomSheetCallback callback)716 public void setBottomSheetCallback(BottomSheetCallback callback) { 717 super.setBottomSheetCallback(new BottomSheetCallback() { 718 @Override 719 public void onStateChanged(@NonNull View bottomSheet, int newState) { 720 if (!mStateQueue.isEmpty()) { 721 if (newState == mStateQueue.getFirst()) { 722 mStateQueue.removeFirst(); 723 if (mStateQueue.isEmpty()) { 724 mIsQueueProcessing = false; 725 } else { 726 setState(mStateQueue.getFirst()); 727 } 728 } else { 729 setState(mStateQueue.getFirst()); 730 } 731 } 732 733 if (callback != null) { 734 callback.onStateChanged(bottomSheet, newState); 735 } 736 } 737 738 @Override 739 public void onSlide(@NonNull View bottomSheet, float slideOffset) { 740 if (callback != null) { 741 callback.onSlide(bottomSheet, slideOffset); 742 } 743 } 744 }); 745 } 746 } 747 } 748