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