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