ActionMenuPresenter.java revision 9b4bee0f14bbd137b0797127aff2df46a6321ec5
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.internal.view.menu; 18 19import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView; 20 21import android.content.Context; 22import android.content.res.Configuration; 23import android.content.res.Resources; 24import android.util.Log; 25import android.util.SparseBooleanArray; 26import android.view.MenuItem; 27import android.view.SoundEffectConstants; 28import android.view.View; 29import android.view.View.MeasureSpec; 30import android.view.ViewGroup; 31import android.widget.ImageButton; 32 33import java.util.ArrayList; 34 35/** 36 * MenuPresenter for building action menus as seen in the action bar and action modes. 37 */ 38public class ActionMenuPresenter extends BaseMenuPresenter { 39 private View mOverflowButton; 40 private boolean mReserveOverflow; 41 private int mWidthLimit; 42 private int mActionItemWidthLimit; 43 private int mMaxItems; 44 45 // Group IDs that have been added as actions - used temporarily, allocated here for reuse. 46 private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray(); 47 48 private View mScrapActionButtonView; 49 50 private OverflowPopup mOverflowPopup; 51 private ActionButtonSubmenu mActionButtonPopup; 52 53 private OpenOverflowRunnable mPostedOpenRunnable; 54 55 public ActionMenuPresenter() { 56 super(com.android.internal.R.layout.action_menu_layout, 57 com.android.internal.R.layout.action_menu_item_layout); 58 } 59 60 @Override 61 public void initForMenu(Context context, MenuBuilder menu) { 62 super.initForMenu(context, menu); 63 64 final Resources res = context.getResources(); 65 final int screen = res.getConfiguration().screenLayout; 66 // TODO Use the no-buttons specifier instead here 67 mReserveOverflow = (screen & Configuration.SCREENLAYOUT_SIZE_MASK) == 68 Configuration.SCREENLAYOUT_SIZE_XLARGE; 69 mWidthLimit = res.getDisplayMetrics().widthPixels / 2; 70 71 // Measure for initial configuration 72 mMaxItems = res.getInteger(com.android.internal.R.integer.max_action_buttons); 73 74 int width = mWidthLimit; 75 if (mReserveOverflow) { 76 if (mOverflowButton == null) { 77 mOverflowButton = new OverflowMenuButton(mContext); 78 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 79 mOverflowButton.measure(spec, spec); 80 } 81 width -= mOverflowButton.getMeasuredWidth(); 82 } else { 83 mOverflowButton = null; 84 } 85 86 mActionItemWidthLimit = width; 87 88 // Drop a scrap view as it may no longer reflect the proper context/config. 89 mScrapActionButtonView = null; 90 } 91 92 public void setWidthLimit(int width) { 93 if (mReserveOverflow) { 94 width -= mOverflowButton.getMeasuredWidth(); 95 } 96 mActionItemWidthLimit = width; 97 } 98 99 public void setItemLimit(int itemCount) { 100 mMaxItems = itemCount; 101 } 102 103 @Override 104 public MenuView getMenuView(ViewGroup root) { 105 MenuView result = super.getMenuView(root); 106 ((ActionMenuView) result).setPresenter(this); 107 return result; 108 } 109 110 @Override 111 public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) { 112 final View actionView = item.getActionView(); 113 return actionView != null ? actionView : super.getItemView(item, convertView, parent); 114 } 115 116 @Override 117 public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) { 118 itemView.initialize(item, 0); 119 ((ActionMenuItemView) itemView).setItemInvoker((ActionMenuView) mMenuView); 120 } 121 122 @Override 123 public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { 124 return item.isActionButton(); 125 } 126 127 @Override 128 public void updateMenuView(boolean cleared) { 129 super.updateMenuView(cleared); 130 131 if (mReserveOverflow && mMenu.getNonActionItems().size() > 0) { 132 if (mOverflowButton == null) { 133 mOverflowButton = new OverflowMenuButton(mContext); 134 } 135 ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); 136 if (parent != mMenuView) { 137 if (parent != null) { 138 parent.removeView(mOverflowButton); 139 } 140 ((ViewGroup) mMenuView).addView(mOverflowButton); 141 } 142 } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) { 143 ((ViewGroup) mMenuView).removeView(mOverflowButton); 144 } 145 } 146 147 @Override 148 public boolean filterLeftoverView(ViewGroup parent, int childIndex) { 149 if (parent.getChildAt(childIndex) == mOverflowButton) return false; 150 return super.filterLeftoverView(parent, childIndex); 151 } 152 153 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 154 if (!subMenu.hasVisibleItems()) return false; 155 156 SubMenuBuilder topSubMenu = subMenu; 157 while (topSubMenu.getParentMenu() != mMenu) { 158 topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu(); 159 } 160 View anchor = findViewForItem(topSubMenu.getItem()); 161 if (anchor == null) return false; 162 163 mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu); 164 mActionButtonPopup.setAnchorView(anchor); 165 mActionButtonPopup.show(); 166 super.onSubMenuSelected(subMenu); 167 return true; 168 } 169 170 private View findViewForItem(MenuItem item) { 171 final ViewGroup parent = (ViewGroup) mMenuView; 172 if (parent == null) return null; 173 174 final int count = parent.getChildCount(); 175 for (int i = 0; i < count; i++) { 176 final View child = parent.getChildAt(i); 177 if (child instanceof MenuView.ItemView && 178 ((MenuView.ItemView) child).getItemData() == item) { 179 return child; 180 } 181 } 182 return null; 183 } 184 185 /** 186 * Display the overflow menu if one is present. 187 * @return true if the overflow menu was shown, false otherwise. 188 */ 189 public boolean showOverflowMenu() { 190 if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null && 191 mPostedOpenRunnable == null) { 192 Log.d("ActionMenuPresenter", "showOverflowMenu"); 193 OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true); 194 mPostedOpenRunnable = new OpenOverflowRunnable(popup); 195 // Post this for later; we might still need a layout for the anchor to be right. 196 ((View) mMenuView).post(mPostedOpenRunnable); 197 198 // ActionMenuPresenter uses null as a callback argument here 199 // to indicate overflow is opening. 200 super.onSubMenuSelected(null); 201 202 return true; 203 } 204 return false; 205 } 206 207 /** 208 * Hide the overflow menu if it is currently showing. 209 * 210 * @return true if the overflow menu was hidden, false otherwise. 211 */ 212 public boolean hideOverflowMenu() { 213 if (mPostedOpenRunnable != null && mMenuView != null) { 214 ((View) mMenuView).removeCallbacks(mPostedOpenRunnable); 215 return true; 216 } 217 218 MenuPopupHelper popup = mOverflowPopup; 219 if (popup != null) { 220 popup.dismiss(); 221 return true; 222 } 223 return false; 224 } 225 226 /** 227 * Dismiss all popup menus - overflow and submenus. 228 * @return true if popups were dismissed, false otherwise. (This can be because none were open.) 229 */ 230 public boolean dismissPopupMenus() { 231 boolean result = hideOverflowMenu(); 232 result |= hideSubMenus(); 233 return result; 234 } 235 236 /** 237 * Dismiss all submenu popups. 238 * 239 * @return true if popups were dismissed, false otherwise. (This can be because none were open.) 240 */ 241 public boolean hideSubMenus() { 242 if (mActionButtonPopup != null) { 243 mActionButtonPopup.dismiss(); 244 return true; 245 } 246 return false; 247 } 248 249 /** 250 * @return true if the overflow menu is currently showing 251 */ 252 public boolean isOverflowMenuShowing() { 253 return mOverflowPopup != null && mOverflowPopup.isShowing(); 254 } 255 256 /** 257 * @return true if space has been reserved in the action menu for an overflow item. 258 */ 259 public boolean isOverflowReserved() { 260 return mReserveOverflow; 261 } 262 263 public boolean flagActionItems() { 264 final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); 265 final int itemsSize = visibleItems.size(); 266 int maxActions = mMaxItems; 267 int widthLimit = mActionItemWidthLimit; 268 final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 269 final ViewGroup parent = (ViewGroup) mMenuView; 270 271 int requiredItems = 0; 272 int requestedItems = 0; 273 int firstActionWidth = 0; 274 boolean hasOverflow = false; 275 for (int i = 0; i < itemsSize; i++) { 276 MenuItemImpl item = visibleItems.get(i); 277 if (item.requiresActionButton()) { 278 requiredItems++; 279 } else if (item.requestsActionButton()) { 280 requestedItems++; 281 } else { 282 hasOverflow = true; 283 } 284 } 285 286 // Reserve a spot for the overflow item if needed. 287 if (mReserveOverflow && 288 (hasOverflow || requiredItems + requestedItems > maxActions)) { 289 maxActions--; 290 } 291 maxActions -= requiredItems; 292 293 final SparseBooleanArray seenGroups = mActionButtonGroups; 294 seenGroups.clear(); 295 296 // Flag as many more requested items as will fit. 297 for (int i = 0; i < itemsSize; i++) { 298 MenuItemImpl item = visibleItems.get(i); 299 300 if (item.requiresActionButton()) { 301 View v = item.getActionView(); 302 if (v == null) { 303 v = getItemView(item, mScrapActionButtonView, parent); 304 if (mScrapActionButtonView == null) { 305 mScrapActionButtonView = v; 306 } 307 } 308 v.measure(querySpec, querySpec); 309 final int measuredWidth = v.getMeasuredWidth(); 310 widthLimit -= measuredWidth; 311 if (firstActionWidth == 0) { 312 firstActionWidth = measuredWidth; 313 } 314 final int groupId = item.getGroupId(); 315 if (groupId != 0) { 316 seenGroups.put(groupId, true); 317 } 318 } else if (item.requestsActionButton()) { 319 // Items in a group with other items that already have an action slot 320 // can break the max actions rule, but not the width limit. 321 final int groupId = item.getGroupId(); 322 final boolean inGroup = seenGroups.get(groupId); 323 boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0; 324 maxActions--; 325 326 if (isAction) { 327 View v = item.getActionView(); 328 if (v == null) { 329 v = getItemView(item, mScrapActionButtonView, parent); 330 if (mScrapActionButtonView == null) { 331 mScrapActionButtonView = v; 332 } 333 } 334 v.measure(querySpec, querySpec); 335 final int measuredWidth = v.getMeasuredWidth(); 336 widthLimit -= measuredWidth; 337 if (firstActionWidth == 0) { 338 firstActionWidth = measuredWidth; 339 } 340 341 // Did this push the entire first item past halfway? 342 if (widthLimit + firstActionWidth <= 0) { 343 isAction = false; 344 } 345 } 346 347 if (isAction && groupId != 0) { 348 seenGroups.put(groupId, true); 349 } else if (inGroup) { 350 // We broke the width limit. Demote the whole group, they all overflow now. 351 seenGroups.put(groupId, false); 352 for (int j = 0; j < i; j++) { 353 MenuItemImpl areYouMyGroupie = visibleItems.get(j); 354 if (areYouMyGroupie.getGroupId() == groupId) { 355 areYouMyGroupie.setIsActionButton(false); 356 } 357 } 358 } 359 360 item.setIsActionButton(isAction); 361 } 362 } 363 return true; 364 } 365 366 @Override 367 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 368 dismissPopupMenus(); 369 super.onCloseMenu(menu, allMenusAreClosing); 370 } 371 372 private class OverflowMenuButton extends ImageButton implements ActionMenuChildView { 373 public OverflowMenuButton(Context context) { 374 super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); 375 376 setClickable(true); 377 setFocusable(true); 378 setVisibility(VISIBLE); 379 setEnabled(true); 380 } 381 382 @Override 383 public boolean performClick() { 384 if (super.performClick()) { 385 return true; 386 } 387 388 playSoundEffect(SoundEffectConstants.CLICK); 389 showOverflowMenu(); 390 return true; 391 } 392 393 public boolean needsDividerBefore() { 394 return true; 395 } 396 397 public boolean needsDividerAfter() { 398 return false; 399 } 400 } 401 402 private class OverflowPopup extends MenuPopupHelper { 403 public OverflowPopup(Context context, MenuBuilder menu, View anchorView, 404 boolean overflowOnly) { 405 super(context, menu, anchorView, overflowOnly); 406 } 407 408 @Override 409 public void onDismiss() { 410 super.onDismiss(); 411 mMenu.close(); 412 mOverflowPopup = null; 413 } 414 } 415 416 private class ActionButtonSubmenu extends MenuPopupHelper { 417 private SubMenuBuilder mSubMenu; 418 419 public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) { 420 super(context, subMenu); 421 mSubMenu = subMenu; 422 423 MenuItemImpl item = (MenuItemImpl) subMenu.getItem(); 424 if (!item.isActionButton()) { 425 // Give a reasonable anchor to nested submenus. 426 setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton); 427 } 428 } 429 430 @Override 431 public void onDismiss() { 432 super.onDismiss(); 433 mSubMenu.close(); 434 mActionButtonPopup = null; 435 } 436 } 437 438 private class OpenOverflowRunnable implements Runnable { 439 private OverflowPopup mPopup; 440 441 public OpenOverflowRunnable(OverflowPopup popup) { 442 mPopup = popup; 443 } 444 445 public void run() { 446 mMenu.changeMenuMode(); 447 if (mPopup.tryShow()) { 448 mOverflowPopup = mPopup; 449 mPostedOpenRunnable = null; 450 } 451 } 452 } 453} 454