Upstream version 10.38.208.0
[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.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      */
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      * @param screenHeight        Current device screen height.
93      */
94     void show(Context context, View anchorView, boolean isByHardwareButton, int screenRotation,
95             Rect visibleDisplayFrame, int screenHeight) {
96         mPopup = new ListPopupWindow(context, null, android.R.attr.popupMenuStyle);
97         mPopup.setModal(true);
98         mPopup.setAnchorView(anchorView);
99         mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
100         mPopup.setOnDismissListener(new OnDismissListener() {
101             @Override
102             public void onDismiss() {
103                 if (mPopup.getAnchorView() instanceof ImageButton) {
104                     ((ImageButton) mPopup.getAnchorView()).setSelected(false);
105                 }
106                 mHandler.onMenuVisibilityChanged(false);
107             }
108         });
109
110         // Some OEMs don't actually let us change the background... but they still return the
111         // padding of the new background, which breaks the menu height.  If we still have a
112         // drawable here even though our style says @null we should use this padding instead...
113         Drawable originalBgDrawable = mPopup.getBackground();
114
115         // Need to explicitly set the background here.  Relying on it being set in the style caused
116         // an incorrectly drawn background.
117         if (isByHardwareButton) {
118             mPopup.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.menu_bg));
119         } else {
120             mPopup.setBackgroundDrawable(
121                     context.getResources().getDrawable(R.drawable.edge_menu_bg));
122             mPopup.setAnimationStyle(R.style.OverflowMenuAnim);
123         }
124
125         // Turn off window animations for low end devices.
126         if (SysUtils.isLowEndDevice()) mPopup.setAnimationStyle(0);
127
128         Rect bgPadding = new Rect();
129         mPopup.getBackground().getPadding(bgPadding);
130
131         int popupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_width) +
132                 bgPadding.left + bgPadding.right;
133
134         mPopup.setWidth(popupWidth);
135
136         mCurrentScreenRotation = screenRotation;
137         mIsByHardwareButton = isByHardwareButton;
138
139         // Extract visible items from the Menu.
140         int numItems = mMenu.size();
141         List<MenuItem> menuItems = new ArrayList<MenuItem>();
142         for (int i = 0; i < numItems; ++i) {
143             MenuItem item = mMenu.getItem(i);
144             if (item.isVisible()) {
145                 menuItems.add(item);
146             }
147         }
148
149         Rect sizingPadding = new Rect(bgPadding);
150         if (isByHardwareButton && originalBgDrawable != null) {
151             Rect originalPadding = new Rect();
152             originalBgDrawable.getPadding(originalPadding);
153             sizingPadding.top = originalPadding.top;
154             sizingPadding.bottom = originalPadding.bottom;
155         }
156
157         boolean showMenuButton = !mIsByHardwareButton;
158         if (!SHOW_SW_MENU_BUTTON) showMenuButton = false;
159         // A List adapter for visible items in the Menu. The first row is added as a header to the
160         // list view.
161         mAdapter = new AppMenuAdapter(
162                 this, menuItems, LayoutInflater.from(context), showMenuButton);
163         mPopup.setAdapter(mAdapter);
164
165         setMenuHeight(menuItems.size(), visibleDisplayFrame, screenHeight, sizingPadding);
166         setPopupOffset(mPopup, mCurrentScreenRotation, visibleDisplayFrame, sizingPadding);
167         mPopup.setOnItemClickListener(this);
168         mPopup.show();
169         mPopup.getListView().setItemsCanFocus(true);
170         mPopup.getListView().setOnKeyListener(this);
171
172         mHandler.onMenuVisibilityChanged(true);
173
174         if (mVerticalFadeDistance > 0) {
175             mPopup.getListView().setVerticalFadingEdgeEnabled(true);
176             mPopup.getListView().setFadingEdgeLength(mVerticalFadeDistance);
177         }
178
179         // Don't animate the menu items for low end devices.
180         if (!SysUtils.isLowEndDevice()) {
181             mPopup.getListView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
182                 @Override
183                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
184                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
185                     mPopup.getListView().removeOnLayoutChangeListener(this);
186                     runMenuItemEnterAnimations();
187                 }
188             });
189         }
190     }
191
192     private void setPopupOffset(
193             ListPopupWindow popup, int screenRotation, Rect appRect, Rect padding) {
194         int[] anchorLocation = new int[2];
195         popup.getAnchorView().getLocationInWindow(anchorLocation);
196         int anchorHeight = popup.getAnchorView().getHeight();
197
198         // If we have a hardware menu button, locate the app menu closer to the estimated
199         // hardware menu button location.
200         if (mIsByHardwareButton) {
201             int horizontalOffset = -anchorLocation[0];
202             switch (screenRotation) {
203                 case Surface.ROTATION_0:
204                 case Surface.ROTATION_180:
205                     horizontalOffset += (appRect.width() - mPopup.getWidth()) / 2;
206                     break;
207                 case Surface.ROTATION_90:
208                     horizontalOffset += appRect.width() - mPopup.getWidth();
209                     break;
210                 case Surface.ROTATION_270:
211                     break;
212                 default:
213                     assert false;
214                     break;
215             }
216             popup.setHorizontalOffset(horizontalOffset);
217             // The menu is displayed above the anchored view, so shift the menu up by the bottom
218             // padding of the background.
219             popup.setVerticalOffset(-padding.bottom);
220         } else {
221             // The menu is displayed over and below the anchored view, so shift the menu up by the
222             // height of the anchor view.
223             popup.setVerticalOffset(-mNegativeSoftwareVerticalOffset - anchorHeight);
224         }
225     }
226
227     /**
228      * Handles clicks on the AppMenu popup.
229      * @param menuItem The menu item in the popup that was clicked.
230      */
231     void onItemClick(MenuItem menuItem) {
232         if (menuItem.isEnabled()) {
233             dismiss();
234             mHandler.onOptionsItemSelected(menuItem);
235         }
236     }
237
238     @Override
239     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
240         onItemClick(mAdapter.getItem(position));
241     }
242
243     @Override
244     public boolean onKey(View v, int keyCode, KeyEvent event) {
245         if (mPopup == null || mPopup.getListView() == null) return false;
246
247         if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
248             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
249                 event.startTracking();
250                 v.getKeyDispatcherState().startTracking(event, this);
251                 return true;
252             } else if (event.getAction() == KeyEvent.ACTION_UP) {
253                 v.getKeyDispatcherState().handleUpEvent(event);
254                 if (event.isTracking() && !event.isCanceled()) {
255                     dismiss();
256                     return true;
257                 }
258             }
259         }
260         return false;
261     }
262
263     /**
264      * Dismisses the app menu and cancels the drag-to-scroll if it is taking place.
265      */
266     void dismiss() {
267         mHandler.appMenuDismissed();
268         if (isShowing()) {
269             mPopup.dismiss();
270         }
271     }
272
273     /**
274      * @return Whether the app menu is currently showing.
275      */
276     boolean isShowing() {
277         if (mPopup == null) {
278             return false;
279         }
280         return mPopup.isShowing();
281     }
282
283     /**
284      * @return ListPopupWindow that displays all the menu options.
285      */
286     ListPopupWindow getPopup() {
287         return mPopup;
288     }
289
290     private void setMenuHeight(
291             int numMenuItems, Rect appDimensions, int screenHeight, Rect padding) {
292         assert mPopup.getAnchorView() != null;
293         View anchorView = mPopup.getAnchorView();
294         int[] anchorViewLocation = new int[2];
295         anchorView.getLocationOnScreen(anchorViewLocation);
296         anchorViewLocation[1] -= appDimensions.top;
297         int anchorViewImpactHeight = mIsByHardwareButton ? anchorView.getHeight() : 0;
298
299         // Set appDimensions.height() for abnormal anchorViewLocation.
300         if (anchorViewLocation[1] > screenHeight) {
301             anchorViewLocation[1] = appDimensions.height();
302         }
303         int availableScreenSpace = Math.max(anchorViewLocation[1],
304                 appDimensions.height() - anchorViewLocation[1] - anchorViewImpactHeight);
305
306         availableScreenSpace -= padding.bottom;
307         if (mIsByHardwareButton) availableScreenSpace -= padding.top;
308
309         int numCanFit = availableScreenSpace / (mItemRowHeight + mItemDividerHeight);
310
311         // Fade out the last item if we cannot fit all items.
312         if (numCanFit < numMenuItems) {
313             int spaceForFullItems = numCanFit * (mItemRowHeight + mItemDividerHeight);
314             int spaceForPartialItem = (int) (LAST_ITEM_SHOW_FRACTION * mItemRowHeight);
315             // Determine which item needs hiding.
316             if (spaceForFullItems + spaceForPartialItem < availableScreenSpace) {
317                 mPopup.setHeight(spaceForFullItems + spaceForPartialItem +
318                         padding.top + padding.bottom);
319             } else {
320                 mPopup.setHeight(spaceForFullItems - mItemRowHeight + spaceForPartialItem +
321                         padding.top + padding.bottom);
322             }
323         } else {
324             mPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
325         }
326     }
327
328     private void runMenuItemEnterAnimations() {
329         AnimatorSet animation = new AnimatorSet();
330         AnimatorSet.Builder builder = null;
331
332         ViewGroup list = mPopup.getListView();
333         for (int i = 0; i < list.getChildCount(); i++) {
334             View view = list.getChildAt(i);
335             Object animatorObject = view.getTag(R.id.menu_item_enter_anim_id);
336             if (animatorObject != null) {
337                 if (builder == null) {
338                     builder = animation.play((Animator) animatorObject);
339                 } else {
340                     builder.with((Animator) animatorObject);
341                 }
342             }
343         }
344
345         animation.start();
346     }
347 }