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.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;
28 import org.chromium.base.SysUtils;
29 import org.chromium.chrome.R;
31 import java.util.ArrayList;
32 import java.util.List;
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.
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;
44 private static final float LAST_ITEM_SHOW_FRACTION = 0.5f;
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;
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.
65 AppMenu(Menu menu, int itemRowHeight, int itemDividerHeight, AppMenuHandler handler,
69 mItemRowHeight = itemRowHeight;
70 assert mItemRowHeight > 0;
74 mItemDividerHeight = itemDividerHeight;
75 assert mItemDividerHeight >= 0;
77 mNegativeSoftwareVerticalOffset =
78 res.getDimensionPixelSize(R.dimen.menu_negative_software_vertical_offset);
79 mVerticalFadeDistance = res.getDimensionPixelSize(R.dimen.menu_vertical_fade_distance);
83 * Creates and shows the app menu anchored to the specified view.
85 * @param context The context of the AppMenu (ensure the proper theme is set on
87 * @param anchorView The anchor {@link View} of the {@link ListPopupWindow}.
88 * @param isByHardwareButton Whether or not hardware button triggered it. (oppose to software
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.
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() {
102 public void onDismiss() {
103 if (mPopup.getAnchorView() instanceof ImageButton) {
104 ((ImageButton) mPopup.getAnchorView()).setSelected(false);
106 mHandler.onMenuVisibilityChanged(false);
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();
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));
120 mPopup.setBackgroundDrawable(
121 context.getResources().getDrawable(R.drawable.edge_menu_bg));
122 mPopup.setAnimationStyle(R.style.OverflowMenuAnim);
125 // Turn off window animations for low end devices.
126 if (SysUtils.isLowEndDevice()) mPopup.setAnimationStyle(0);
128 Rect bgPadding = new Rect();
129 mPopup.getBackground().getPadding(bgPadding);
131 int popupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_width) +
132 bgPadding.left + bgPadding.right;
134 mPopup.setWidth(popupWidth);
136 mCurrentScreenRotation = screenRotation;
137 mIsByHardwareButton = isByHardwareButton;
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()) {
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;
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
161 mAdapter = new AppMenuAdapter(
162 this, menuItems, LayoutInflater.from(context), showMenuButton);
163 mPopup.setAdapter(mAdapter);
165 setMenuHeight(menuItems.size(), visibleDisplayFrame, screenHeight, sizingPadding);
166 setPopupOffset(mPopup, mCurrentScreenRotation, visibleDisplayFrame, sizingPadding);
167 mPopup.setOnItemClickListener(this);
169 mPopup.getListView().setItemsCanFocus(true);
170 mPopup.getListView().setOnKeyListener(this);
172 mHandler.onMenuVisibilityChanged(true);
174 if (mVerticalFadeDistance > 0) {
175 mPopup.getListView().setVerticalFadingEdgeEnabled(true);
176 mPopup.getListView().setFadingEdgeLength(mVerticalFadeDistance);
179 // Don't animate the menu items for low end devices.
180 if (!SysUtils.isLowEndDevice()) {
181 mPopup.getListView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
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();
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();
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;
207 case Surface.ROTATION_90:
208 horizontalOffset += appRect.width() - mPopup.getWidth();
210 case Surface.ROTATION_270:
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);
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);
228 * Handles clicks on the AppMenu popup.
229 * @param menuItem The menu item in the popup that was clicked.
231 void onItemClick(MenuItem menuItem) {
232 if (menuItem.isEnabled()) {
234 mHandler.onOptionsItemSelected(menuItem);
239 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
240 onItemClick(mAdapter.getItem(position));
244 public boolean onKey(View v, int keyCode, KeyEvent event) {
245 if (mPopup == null || mPopup.getListView() == null) return false;
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);
252 } else if (event.getAction() == KeyEvent.ACTION_UP) {
253 v.getKeyDispatcherState().handleUpEvent(event);
254 if (event.isTracking() && !event.isCanceled()) {
264 * Dismisses the app menu and cancels the drag-to-scroll if it is taking place.
267 mHandler.appMenuDismissed();
274 * @return Whether the app menu is currently showing.
276 boolean isShowing() {
277 if (mPopup == null) {
280 return mPopup.isShowing();
284 * @return ListPopupWindow that displays all the menu options.
286 ListPopupWindow getPopup() {
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;
299 // Set appDimensions.height() for abnormal anchorViewLocation.
300 if (anchorViewLocation[1] > screenHeight) {
301 anchorViewLocation[1] = appDimensions.height();
303 int availableScreenSpace = Math.max(anchorViewLocation[1],
304 appDimensions.height() - anchorViewLocation[1] - anchorViewImpactHeight);
306 availableScreenSpace -= padding.bottom;
307 if (mIsByHardwareButton) availableScreenSpace -= padding.top;
309 int numCanFit = availableScreenSpace / (mItemRowHeight + mItemDividerHeight);
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);
320 mPopup.setHeight(spaceForFullItems - mItemRowHeight + spaceForPartialItem +
321 padding.top + padding.bottom);
324 mPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
328 private void runMenuItemEnterAnimations() {
329 AnimatorSet animation = new AnimatorSet();
330 AnimatorSet.Builder builder = null;
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);
340 builder.with((Animator) animatorObject);