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