1 /* 2 * Copyright (C) 2015 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 17 package com.android.internal.widget.floatingtoolbar; 18 19 import android.annotation.Nullable; 20 import android.graphics.Rect; 21 import android.view.Menu; 22 import android.view.MenuItem; 23 import android.view.View; 24 import android.view.View.OnLayoutChangeListener; 25 import android.view.Window; 26 import android.widget.PopupWindow; 27 28 import java.util.ArrayList; 29 import java.util.Comparator; 30 import java.util.List; 31 import java.util.Objects; 32 33 /** 34 * A floating toolbar for showing contextual menu items. 35 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the 36 * the remaining menu items in a vertical overflow view when the overflow button is clicked. 37 * The horizontal toolbar morphs into the vertical overflow view. 38 */ 39 public final class FloatingToolbar { 40 41 // This class is responsible for the public API of the floating toolbar. 42 // It delegates rendering operations to the FloatingToolbarPopup. 43 44 public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar"; 45 46 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER = 47 item -> false; 48 49 private final Window mWindow; 50 private final FloatingToolbarPopup mPopup; 51 52 private final Rect mContentRect = new Rect(); 53 54 private Menu mMenu; 55 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 56 57 private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() { 58 59 private final Rect mNewRect = new Rect(); 60 private final Rect mOldRect = new Rect(); 61 62 @Override 63 public void onLayoutChange( 64 View view, 65 int newLeft, int newRight, int newTop, int newBottom, 66 int oldLeft, int oldRight, int oldTop, int oldBottom) { 67 mNewRect.set(newLeft, newRight, newTop, newBottom); 68 mOldRect.set(oldLeft, oldRight, oldTop, oldBottom); 69 if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) { 70 mPopup.setWidthChanged(true); 71 updateLayout(); 72 } 73 } 74 }; 75 76 /** 77 * Sorts the list of menu items to conform to certain requirements. 78 */ 79 private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> { 80 // Ensure the assist menu item is always the first item: 81 if (menuItem1.getItemId() == android.R.id.textAssist) { 82 return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1; 83 } 84 if (menuItem2.getItemId() == android.R.id.textAssist) { 85 return 1; 86 } 87 88 // Order by SHOW_AS_ACTION type: 89 if (menuItem1.requiresActionButton()) { 90 return menuItem2.requiresActionButton() ? 0 : -1; 91 } 92 if (menuItem2.requiresActionButton()) { 93 return 1; 94 } 95 if (menuItem1.requiresOverflow()) { 96 return menuItem2.requiresOverflow() ? 0 : 1; 97 } 98 if (menuItem2.requiresOverflow()) { 99 return -1; 100 } 101 102 // Order by order value: 103 return menuItem1.getOrder() - menuItem2.getOrder(); 104 }; 105 106 /** 107 * Initializes a floating toolbar. 108 */ FloatingToolbar(Window window)109 public FloatingToolbar(Window window) { 110 // TODO(b/65172902): Pass context in constructor when DecorView (and other callers) 111 // supports multi-display. 112 mWindow = Objects.requireNonNull(window); 113 mPopup = FloatingToolbarPopup.createInstance(window.getContext(), window.getDecorView()); 114 } 115 116 /** 117 * Sets the menu to be shown in this floating toolbar. 118 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 119 * toolbar. 120 */ setMenu(Menu menu)121 public FloatingToolbar setMenu(Menu menu) { 122 mMenu = Objects.requireNonNull(menu); 123 return this; 124 } 125 126 /** 127 * Sets the custom listener for invocation of menu items in this floating toolbar. 128 */ setOnMenuItemClickListener( MenuItem.OnMenuItemClickListener menuItemClickListener)129 public FloatingToolbar setOnMenuItemClickListener( 130 MenuItem.OnMenuItemClickListener menuItemClickListener) { 131 if (menuItemClickListener != null) { 132 mMenuItemClickListener = menuItemClickListener; 133 } else { 134 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 135 } 136 return this; 137 } 138 139 /** 140 * Sets the content rectangle. This is the area of the interesting content that this toolbar 141 * should avoid obstructing. 142 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 143 * toolbar. 144 */ setContentRect(Rect rect)145 public FloatingToolbar setContentRect(Rect rect) { 146 mContentRect.set(Objects.requireNonNull(rect)); 147 return this; 148 } 149 150 /** 151 * Sets the suggested width of this floating toolbar. 152 * The actual width will be about this size but there are no guarantees that it will be exactly 153 * the suggested width. 154 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 155 * toolbar. 156 */ setSuggestedWidth(int suggestedWidth)157 public FloatingToolbar setSuggestedWidth(int suggestedWidth) { 158 mPopup.setSuggestedWidth(suggestedWidth); 159 return this; 160 } 161 162 /** 163 * Shows this floating toolbar. 164 */ show()165 public FloatingToolbar show() { 166 registerOrientationHandler(); 167 doShow(); 168 return this; 169 } 170 171 /** 172 * Updates this floating toolbar to reflect recent position and view updates. 173 * NOTE: This method is a no-op if the toolbar isn't showing. 174 */ updateLayout()175 public FloatingToolbar updateLayout() { 176 if (mPopup.isShowing()) { 177 doShow(); 178 } 179 return this; 180 } 181 182 /** 183 * Dismisses this floating toolbar. 184 */ dismiss()185 public void dismiss() { 186 unregisterOrientationHandler(); 187 mPopup.dismiss(); 188 } 189 190 /** 191 * Hides this floating toolbar. This is a no-op if the toolbar is not showing. 192 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar. 193 */ hide()194 public void hide() { 195 mPopup.hide(); 196 } 197 198 /** 199 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise. 200 */ isShowing()201 public boolean isShowing() { 202 return mPopup.isShowing(); 203 } 204 205 /** 206 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise. 207 */ isHidden()208 public boolean isHidden() { 209 return mPopup.isHidden(); 210 } 211 212 /** 213 * If this is set to true, the action mode view will dismiss itself on touch events outside of 214 * its window. The setting takes effect immediately. 215 * 216 * @param outsideTouchable whether or not this action mode is "outside touchable" 217 * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself 218 */ setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)219 public void setOutsideTouchable( 220 boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { 221 mPopup.setOutsideTouchable(outsideTouchable, onDismiss); 222 } 223 doShow()224 private void doShow() { 225 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); 226 menuItems.sort(mMenuItemComparator); 227 mPopup.show(menuItems, mMenuItemClickListener, mContentRect); 228 } 229 230 /** 231 * Returns the visible and enabled menu items in the specified menu. 232 * This method is recursive. 233 */ getVisibleAndEnabledMenuItems(Menu menu)234 private static List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) { 235 List<MenuItem> menuItems = new ArrayList<>(); 236 for (int i = 0; (menu != null) && (i < menu.size()); i++) { 237 MenuItem menuItem = menu.getItem(i); 238 if (menuItem.isVisible() && menuItem.isEnabled()) { 239 Menu subMenu = menuItem.getSubMenu(); 240 if (subMenu != null) { 241 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu)); 242 } else { 243 menuItems.add(menuItem); 244 } 245 } 246 } 247 return menuItems; 248 } 249 registerOrientationHandler()250 private void registerOrientationHandler() { 251 unregisterOrientationHandler(); 252 mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler); 253 } 254 unregisterOrientationHandler()255 private void unregisterOrientationHandler() { 256 mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler); 257 } 258 } 259