• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.appmenu;
6 
7 import android.animation.Animator;
8 import android.animation.AnimatorSet;
9 import android.content.Context;
10 import android.content.res.Resources;
11 import android.graphics.Rect;
12 import android.graphics.drawable.Drawable;
13 import android.view.KeyEvent;
14 import android.view.LayoutInflater;
15 import android.view.Menu;
16 import android.view.MenuItem;
17 import android.view.Surface;
18 import android.view.View;
19 import android.view.View.OnKeyListener;
20 import android.view.ViewGroup;
21 import android.widget.AdapterView;
22 import android.widget.AdapterView.OnItemClickListener;
23 import android.widget.ImageButton;
24 import android.widget.ListPopupWindow;
25 import android.widget.PopupWindow;
26 import android.widget.PopupWindow.OnDismissListener;
27 
28 import org.chromium.base.SysUtils;
29 import org.chromium.chrome.R;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 /**
35  * Shows a popup of menuitems anchored to a host view. When a item is selected we call
36  * Activity.onOptionsItemSelected with the appropriate MenuItem.
37  *   - Only visible MenuItems are shown.
38  *   - Disabled items are grayed out.
39  */
40 public class AppMenu implements OnItemClickListener, OnKeyListener {
41     /** Whether or not to show the software menu button in the menu. */
42     private static final boolean SHOW_SW_MENU_BUTTON = true;
43 
44     private static final float LAST_ITEM_SHOW_FRACTION = 0.5f;
45 
46     private final Menu mMenu;
47     private final int mItemRowHeight;
48     private final int mItemDividerHeight;
49     private final int mVerticalFadeDistance;
50     private final int mNegativeSoftwareVerticalOffset;
51     private ListPopupWindow mPopup;
52     private AppMenuAdapter mAdapter;
53     private AppMenuHandler mHandler;
54     private int mCurrentScreenRotation = -1;
55     private boolean mIsByHardwareButton;
56 
57     /**
58      * Creates and sets up the App Menu.
59      * @param menu Original menu created by the framework.
60      * @param itemRowHeight Desired height for each app menu row.
61      * @param itemDividerHeight Desired height for the divider between app menu items.
62      * @param handler AppMenuHandler receives callbacks from AppMenu.
63      * @param res Resources object used to get dimensions and style attributes.
64      */
AppMenu(Menu menu, int itemRowHeight, int itemDividerHeight, AppMenuHandler handler, Resources res)65     AppMenu(Menu menu, int itemRowHeight, int itemDividerHeight, AppMenuHandler handler,
66             Resources res) {
67         mMenu = menu;
68 
69         mItemRowHeight = itemRowHeight;
70         assert mItemRowHeight > 0;
71 
72         mHandler = handler;
73 
74         mItemDividerHeight = itemDividerHeight;
75         assert mItemDividerHeight >= 0;
76 
77         mNegativeSoftwareVerticalOffset =
78                 res.getDimensionPixelSize(R.dimen.menu_negative_software_vertical_offset);
79         mVerticalFadeDistance = res.getDimensionPixelSize(R.dimen.menu_vertical_fade_distance);
80     }
81 
82     /**
83      * Creates and shows the app menu anchored to the specified view.
84      *
85      * @param context             The context of the AppMenu (ensure the proper theme is set on
86      *                            this context).
87      * @param anchorView          The anchor {@link View} of the {@link ListPopupWindow}.
88      * @param isByHardwareButton  Whether or not hardware button triggered it. (oppose to software
89      *                            button)
90      * @param screenRotation      Current device screen rotation.
91      * @param visibleDisplayFrame The display area rect in which AppMenu is supposed to fit in.
92      */
show(Context context, View anchorView, boolean isByHardwareButton, int screenRotation, Rect visibleDisplayFrame)93     void show(Context context, View anchorView, boolean isByHardwareButton, int screenRotation,
94             Rect visibleDisplayFrame) {
95         mPopup = new ListPopupWindow(context, null, android.R.attr.popupMenuStyle);
96         mPopup.setModal(true);
97         mPopup.setAnchorView(anchorView);
98         mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
99         mPopup.setOnDismissListener(new OnDismissListener() {
100             @Override
101             public void onDismiss() {
102                 if (mPopup.getAnchorView() instanceof ImageButton) {
103                     ((ImageButton) mPopup.getAnchorView()).setSelected(false);
104                 }
105                 mHandler.onMenuVisibilityChanged(false);
106             }
107         });
108 
109         // Some OEMs don't actually let us change the background... but they still return the
110         // padding of the new background, which breaks the menu height.  If we still have a
111         // drawable here even though our style says @null we should use this padding instead...
112         Drawable originalBgDrawable = mPopup.getBackground();
113 
114         // Need to explicitly set the background here.  Relying on it being set in the style caused
115         // an incorrectly drawn background.
116         if (isByHardwareButton) {
117             mPopup.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.menu_bg));
118         } else {
119             mPopup.setBackgroundDrawable(
120                     context.getResources().getDrawable(R.drawable.edge_menu_bg));
121             mPopup.setAnimationStyle(R.style.OverflowMenuAnim);
122         }
123 
124         // Turn off window animations for low end devices.
125         if (SysUtils.isLowEndDevice()) mPopup.setAnimationStyle(0);
126 
127         Rect bgPadding = new Rect();
128         mPopup.getBackground().getPadding(bgPadding);
129 
130         int popupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_width) +
131                 bgPadding.left + bgPadding.right;
132 
133         mPopup.setWidth(popupWidth);
134 
135         mCurrentScreenRotation = screenRotation;
136         mIsByHardwareButton = isByHardwareButton;
137 
138         // Extract visible items from the Menu.
139         int numItems = mMenu.size();
140         List<MenuItem> menuItems = new ArrayList<MenuItem>();
141         for (int i = 0; i < numItems; ++i) {
142             MenuItem item = mMenu.getItem(i);
143             if (item.isVisible()) {
144                 menuItems.add(item);
145             }
146         }
147 
148         Rect sizingPadding = new Rect(bgPadding);
149         if (isByHardwareButton && originalBgDrawable != null) {
150             Rect originalPadding = new Rect();
151             originalBgDrawable.getPadding(originalPadding);
152             sizingPadding.top = originalPadding.top;
153             sizingPadding.bottom = originalPadding.bottom;
154         }
155 
156         boolean showMenuButton = !mIsByHardwareButton;
157         if (!SHOW_SW_MENU_BUTTON) showMenuButton = false;
158         // A List adapter for visible items in the Menu. The first row is added as a header to the
159         // list view.
160         mAdapter = new AppMenuAdapter(
161                 this, menuItems, LayoutInflater.from(context), showMenuButton);
162         mPopup.setAdapter(mAdapter);
163 
164         setMenuHeight(menuItems.size(), visibleDisplayFrame, sizingPadding);
165         setPopupOffset(mPopup, mCurrentScreenRotation, visibleDisplayFrame, sizingPadding);
166         mPopup.setOnItemClickListener(this);
167         mPopup.show();
168         mPopup.getListView().setItemsCanFocus(true);
169         mPopup.getListView().setOnKeyListener(this);
170 
171         mHandler.onMenuVisibilityChanged(true);
172 
173         if (mVerticalFadeDistance > 0) {
174             mPopup.getListView().setVerticalFadingEdgeEnabled(true);
175             mPopup.getListView().setFadingEdgeLength(mVerticalFadeDistance);
176         }
177 
178         // Don't animate the menu items for low end devices.
179         if (!SysUtils.isLowEndDevice()) {
180             mPopup.getListView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
181                 @Override
182                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
183                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
184                     mPopup.getListView().removeOnLayoutChangeListener(this);
185                     runMenuItemEnterAnimations();
186                 }
187             });
188         }
189     }
190 
setPopupOffset( ListPopupWindow popup, int screenRotation, Rect appRect, Rect padding)191     private void setPopupOffset(
192             ListPopupWindow popup, int screenRotation, Rect appRect, Rect padding) {
193         int[] anchorLocation = new int[2];
194         popup.getAnchorView().getLocationInWindow(anchorLocation);
195         int anchorHeight = popup.getAnchorView().getHeight();
196 
197         // If we have a hardware menu button, locate the app menu closer to the estimated
198         // hardware menu button location.
199         if (mIsByHardwareButton) {
200             int horizontalOffset = -anchorLocation[0];
201             switch (screenRotation) {
202                 case Surface.ROTATION_0:
203                 case Surface.ROTATION_180:
204                     horizontalOffset += (appRect.width() - mPopup.getWidth()) / 2;
205                     break;
206                 case Surface.ROTATION_90:
207                     horizontalOffset += appRect.width() - mPopup.getWidth();
208                     break;
209                 case Surface.ROTATION_270:
210                     break;
211                 default:
212                     assert false;
213                     break;
214             }
215             popup.setHorizontalOffset(horizontalOffset);
216             // The menu is displayed above the anchored view, so shift the menu up by the bottom
217             // padding of the background.
218             popup.setVerticalOffset(-padding.bottom);
219         } else {
220             // The menu is displayed over and below the anchored view, so shift the menu up by the
221             // height of the anchor view.
222             popup.setVerticalOffset(-mNegativeSoftwareVerticalOffset - anchorHeight);
223         }
224     }
225 
226     /**
227      * Handles clicks on the AppMenu popup.
228      * @param menuItem The menu item in the popup that was clicked.
229      */
onItemClick(MenuItem menuItem)230     void onItemClick(MenuItem menuItem) {
231         if (menuItem.isEnabled()) {
232             dismiss();
233             mHandler.onOptionsItemSelected(menuItem);
234         }
235     }
236 
237     @Override
onItemClick(AdapterView<?> parent, View view, int position, long id)238     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
239         onItemClick(mAdapter.getItem(position));
240     }
241 
242     @Override
onKey(View v, int keyCode, KeyEvent event)243     public boolean onKey(View v, int keyCode, KeyEvent event) {
244         if (mPopup == null || mPopup.getListView() == null) return false;
245 
246         if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
247             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
248                 event.startTracking();
249                 v.getKeyDispatcherState().startTracking(event, this);
250                 return true;
251             } else if (event.getAction() == KeyEvent.ACTION_UP) {
252                 v.getKeyDispatcherState().handleUpEvent(event);
253                 if (event.isTracking() && !event.isCanceled()) {
254                     dismiss();
255                     return true;
256                 }
257             }
258         }
259         return false;
260     }
261 
262     /**
263      * Dismisses the app menu and cancels the drag-to-scroll if it is taking place.
264      */
dismiss()265     void dismiss() {
266         mHandler.appMenuDismissed();
267         if (isShowing()) {
268             mPopup.dismiss();
269         }
270     }
271 
272     /**
273      * @return Whether the app menu is currently showing.
274      */
isShowing()275     boolean isShowing() {
276         if (mPopup == null) {
277             return false;
278         }
279         return mPopup.isShowing();
280     }
281 
282     /**
283      * @return ListPopupWindow that displays all the menu options.
284      */
getPopup()285     ListPopupWindow getPopup() {
286         return mPopup;
287     }
288 
setMenuHeight(int numMenuItems, Rect appDimensions, Rect padding)289     private void setMenuHeight(int numMenuItems, Rect appDimensions, Rect padding) {
290         assert mPopup.getAnchorView() != null;
291         View anchorView = mPopup.getAnchorView();
292         int[] anchorViewLocation = new int[2];
293         anchorView.getLocationOnScreen(anchorViewLocation);
294         anchorViewLocation[1] -= appDimensions.top;
295         int anchorViewImpactHeight = mIsByHardwareButton ? anchorView.getHeight() : 0;
296 
297         int availableScreenSpace = Math.max(anchorViewLocation[1],
298                 appDimensions.height() - anchorViewLocation[1] - anchorViewImpactHeight);
299 
300         availableScreenSpace -= padding.bottom;
301         if (mIsByHardwareButton) availableScreenSpace -= padding.top;
302 
303         int numCanFit = availableScreenSpace / (mItemRowHeight + mItemDividerHeight);
304 
305         // Fade out the last item if we cannot fit all items.
306         if (numCanFit < numMenuItems) {
307             int spaceForFullItems = numCanFit * (mItemRowHeight + mItemDividerHeight);
308             int spaceForPartialItem = (int) (LAST_ITEM_SHOW_FRACTION * mItemRowHeight);
309             // Determine which item needs hiding.
310             if (spaceForFullItems + spaceForPartialItem < availableScreenSpace) {
311                 mPopup.setHeight(spaceForFullItems + spaceForPartialItem +
312                         padding.top + padding.bottom);
313             } else {
314                 mPopup.setHeight(spaceForFullItems - mItemRowHeight + spaceForPartialItem +
315                         padding.top + padding.bottom);
316             }
317         } else {
318             mPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
319         }
320     }
321 
runMenuItemEnterAnimations()322     private void runMenuItemEnterAnimations() {
323         AnimatorSet animation = new AnimatorSet();
324         AnimatorSet.Builder builder = null;
325 
326         ViewGroup list = mPopup.getListView();
327         for (int i = 0; i < list.getChildCount(); i++) {
328             View view = list.getChildAt(i);
329             Object animatorObject = view.getTag(R.id.menu_item_enter_anim_id);
330             if (animatorObject != null) {
331                 if (builder == null) {
332                     builder = animation.play((Animator) animatorObject);
333                 } else {
334                     builder.with((Animator) animatorObject);
335                 }
336             }
337         }
338 
339         animation.start();
340     }
341 }
342