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