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.
5 package org.chromium.chrome.browser.appmenu;
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;
27 import org.chromium.chrome.R;
29 import java.util.ArrayList;
30 import java.util.List;
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.
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;
42 private static final float LAST_ITEM_SHOW_FRACTION = 0.5f;
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;
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.
62 AppMenu(Menu menu, int itemRowHeight, int itemDividerHeight, AppMenuHandler handler,
66 mItemRowHeight = itemRowHeight;
67 assert mItemRowHeight > 0;
71 mItemDividerHeight = itemDividerHeight;
72 assert mItemDividerHeight >= 0;
74 mVerticalFadeDistance = res.getDimensionPixelSize(R.dimen.menu_vertical_fade_distance);
78 * Creates and shows the app menu anchored to the specified view.
80 * @param context The context of the AppMenu (ensure the proper theme is set on
82 * @param anchorView The anchor {@link View} of the {@link ListPopupWindow}.
83 * @param isByHardwareButton Whether or not hardware button triggered it. (oppose to software
85 * @param screenRotation Current device screen rotation.
86 * @param visibleDisplayFrame The display area rect in which AppMenu is supposed to fit in.
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() {
96 public void onDismiss() {
97 if (mPopup.getAnchorView() instanceof ImageButton) {
98 ((ImageButton) mPopup.getAnchorView()).setSelected(false);
100 mHandler.onMenuVisibilityChanged(false);
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));
109 mPopup.setBackgroundDrawable(
110 context.getResources().getDrawable(R.drawable.edge_menu_bg));
111 mPopup.setAnimationStyle(R.style.OverflowMenuAnim);
114 Rect bgPadding = new Rect();
115 mPopup.getBackground().getPadding(bgPadding);
117 int popupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_width) +
118 bgPadding.left + bgPadding.right;
120 mPopup.setWidth(popupWidth);
122 mCurrentScreenRotation = screenRotation;
123 mIsByHardwareButton = isByHardwareButton;
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()) {
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
139 mAdapter = new AppMenuAdapter(
140 this, menuItems, LayoutInflater.from(context), showMenuButton);
141 mPopup.setAdapter(mAdapter);
143 setMenuHeight(menuItems.size(), visibleDisplayFrame);
144 setPopupOffset(mPopup, mCurrentScreenRotation, visibleDisplayFrame);
145 mPopup.setOnItemClickListener(this);
147 mPopup.getListView().setItemsCanFocus(true);
148 mPopup.getListView().setOnKeyListener(this);
150 mHandler.onMenuVisibilityChanged(true);
152 if (mVerticalFadeDistance > 0) {
153 mPopup.getListView().setVerticalFadingEdgeEnabled(true);
154 mPopup.getListView().setFadingEdgeLength(mVerticalFadeDistance);
157 mPopup.getListView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
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();
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();
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;
183 case Surface.ROTATION_90:
184 horizontalOffset += appRect.width() - mPopup.getWidth();
186 case Surface.ROTATION_270:
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);
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);
204 * Handles clicks on the AppMenu popup.
205 * @param menuItem The menu item in the popup that was clicked.
207 void onItemClick(MenuItem menuItem) {
208 if (menuItem.isEnabled()) {
210 mHandler.onOptionsItemSelected(menuItem);
215 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
216 onItemClick(mAdapter.getItem(position));
220 public boolean onKey(View v, int keyCode, KeyEvent event) {
221 if (mPopup == null || mPopup.getListView() == null) return false;
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);
228 } else if (event.getAction() == KeyEvent.ACTION_UP) {
229 v.getKeyDispatcherState().handleUpEvent(event);
230 if (event.isTracking() && !event.isCanceled()) {
240 * Dismisses the app menu and cancels the drag-to-scroll if it is taking place.
243 mHandler.appMenuDismissed();
250 * @return Whether the app menu is currently showing.
252 boolean isShowing() {
253 if (mPopup == null) {
256 return mPopup.isShowing();
260 * @return ListPopupWindow that displays all the menu options.
262 ListPopupWindow getPopup() {
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;
274 int availableScreenSpace = Math.max(anchorViewLocation[1],
275 appDimensions.height() - anchorViewLocation[1] - anchorViewImpactHeight);
277 Rect padding = new Rect();
278 mPopup.getBackground().getPadding(padding);
279 availableScreenSpace -= mIsByHardwareButton ? padding.top : padding.bottom;
281 int numCanFit = availableScreenSpace / (mItemRowHeight + mItemDividerHeight);
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);
292 mPopup.setHeight(spaceForFullItems - mItemRowHeight + spaceForPartialItem +
293 padding.top + padding.bottom);
296 mPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
300 private void runMenuItemEnterAnimations() {
301 AnimatorSet animation = new AnimatorSet();
302 AnimatorSet.Builder builder = null;
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);
312 builder.with((Animator) animatorObject);