ActionMenuPresenter.java revision 696cba573e651b0e4f18a4718627c8ccecb3bda0
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 OverflowMenuButton button = new OverflowMenuButton(mContext); 77 mOverflowButton = button; 78 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 79 mOverflowButton.measure(spec, spec); 80 width -= mOverflowButton.getMeasuredWidth(); 81 } else { 82 mOverflowButton = null; 83 } 84 85 mActionItemWidthLimit = width; 86 87 // Drop a scrap view as it may no longer reflect the proper context/config. 88 mScrapActionButtonView = null; 89 } 90 91 @Override 92 public MenuView getMenuView(ViewGroup root) { 93 MenuView result = super.getMenuView(root); 94 ((ActionMenuView) result).setPresenter(this); 95 return result; 96 } 97 98 @Override 99 public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) { 100 final View actionView = item.getActionView(); 101 return actionView != null ? actionView : super.getItemView(item, convertView, parent); 102 } 103 104 @Override 105 public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) { 106 itemView.initialize(item, 0); 107 ((ActionMenuItemView) itemView).setItemInvoker((ActionMenuView) mMenuView); 108 } 109 110 @Override 111 public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { 112 return item.isActionButton(); 113 } 114 115 @Override 116 public void updateMenuView(boolean cleared) { 117 super.updateMenuView(cleared); 118 119 if (mReserveOverflow && mMenu.getNonActionItems().size() > 0) { 120 if (mOverflowButton == null) { 121 mOverflowButton = new OverflowMenuButton(mContext); 122 } 123 ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); 124 if (parent != mMenuView) { 125 if (parent != null) { 126 parent.removeView(mOverflowButton); 127 } 128 ((ViewGroup) mMenuView).addView(mOverflowButton); 129 } 130 } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) { 131 ((ViewGroup) mMenuView).removeView(mOverflowButton); 132 } 133 } 134 135 @Override 136 public boolean filterLeftoverView(ViewGroup parent, int childIndex) { 137 if (parent.getChildAt(childIndex) == mOverflowButton) return false; 138 return super.filterLeftoverView(parent, childIndex); 139 } 140 141 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 142 if (!subMenu.hasVisibleItems()) return false; 143 144 SubMenuBuilder topSubMenu = subMenu; 145 while (topSubMenu.getParentMenu() != mMenu) { 146 topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu(); 147 } 148 View anchor = findViewForItem(topSubMenu.getItem()); 149 if (anchor == null) return false; 150 151 mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu); 152 mActionButtonPopup.setAnchorView(anchor); 153 mActionButtonPopup.show(); 154 super.onSubMenuSelected(subMenu); 155 return true; 156 } 157 158 private View findViewForItem(MenuItem item) { 159 final ViewGroup parent = (ViewGroup) mMenuView; 160 if (parent == null) return null; 161 162 final int count = parent.getChildCount(); 163 for (int i = 0; i < count; i++) { 164 final View child = parent.getChildAt(i); 165 if (child instanceof MenuView.ItemView && 166 ((MenuView.ItemView) child).getItemData() == item) { 167 return child; 168 } 169 } 170 return null; 171 } 172 173 /** 174 * Display the overflow menu if one is present. 175 * @return true if the overflow menu was shown, false otherwise. 176 */ 177 public boolean showOverflowMenu() { 178 if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null && 179 mPostedOpenRunnable == null) { 180 Log.d("ActionMenuPresenter", "showOverflowMenu"); 181 OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true); 182 mPostedOpenRunnable = new OpenOverflowRunnable(popup); 183 // Post this for later; we might still need a layout for the anchor to be right. 184 ((View) mMenuView).post(mPostedOpenRunnable); 185 186 // ActionMenuPresenter uses null as a callback argument here 187 // to indicate overflow is opening. 188 super.onSubMenuSelected(null); 189 190 return true; 191 } 192 return false; 193 } 194 195 /** 196 * Hide the overflow menu if it is currently showing. 197 * 198 * @return true if the overflow menu was hidden, false otherwise. 199 */ 200 public boolean hideOverflowMenu() { 201 if (mPostedOpenRunnable != null && mMenuView != null) { 202 ((View) mMenuView).removeCallbacks(mPostedOpenRunnable); 203 return true; 204 } 205 206 MenuPopupHelper popup = mOverflowPopup; 207 if (popup != null) { 208 popup.dismiss(); 209 return true; 210 } 211 return false; 212 } 213 214 /** 215 * Dismiss all popup menus - overflow and submenus. 216 * @return true if popups were dismissed, false otherwise. (This can be because none were open.) 217 */ 218 public boolean dismissPopupMenus() { 219 boolean result = hideOverflowMenu(); 220 result |= hideSubMenus(); 221 return result; 222 } 223 224 /** 225 * Dismiss all submenu popups. 226 * 227 * @return true if popups were dismissed, false otherwise. (This can be because none were open.) 228 */ 229 public boolean hideSubMenus() { 230 if (mActionButtonPopup != null) { 231 mActionButtonPopup.dismiss(); 232 return true; 233 } 234 return false; 235 } 236 237 /** 238 * @return true if the overflow menu is currently showing 239 */ 240 public boolean isOverflowMenuShowing() { 241 return mOverflowPopup != null && mOverflowPopup.isShowing(); 242 } 243 244 /** 245 * @return true if space has been reserved in the action menu for an overflow item. 246 */ 247 public boolean isOverflowReserved() { 248 return mReserveOverflow; 249 } 250 251 public boolean flagActionItems() { 252 final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); 253 final int itemsSize = visibleItems.size(); 254 int maxActions = mMaxItems; 255 int widthLimit = mActionItemWidthLimit; 256 final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 257 final ViewGroup parent = (ViewGroup) mMenuView; 258 259 int requiredItems = 0; 260 int requestedItems = 0; 261 int firstActionWidth = 0; 262 boolean hasOverflow = false; 263 for (int i = 0; i < itemsSize; i++) { 264 MenuItemImpl item = visibleItems.get(i); 265 if (item.requiresActionButton()) { 266 requiredItems++; 267 } else if (item.requestsActionButton()) { 268 requestedItems++; 269 } else { 270 hasOverflow = true; 271 } 272 } 273 274 // Reserve a spot for the overflow item if needed. 275 if (mReserveOverflow && 276 (hasOverflow || requiredItems + requestedItems > maxActions)) { 277 maxActions--; 278 } 279 maxActions -= requiredItems; 280 281 final SparseBooleanArray seenGroups = mActionButtonGroups; 282 seenGroups.clear(); 283 284 // Flag as many more requested items as will fit. 285 for (int i = 0; i < itemsSize; i++) { 286 MenuItemImpl item = visibleItems.get(i); 287 288 if (item.requiresActionButton()) { 289 View v = item.getActionView(); 290 if (v == null) { 291 v = getItemView(item, mScrapActionButtonView, parent); 292 if (mScrapActionButtonView == null) { 293 mScrapActionButtonView = v; 294 } 295 } 296 v.measure(querySpec, querySpec); 297 final int measuredWidth = v.getMeasuredWidth(); 298 widthLimit -= measuredWidth; 299 if (firstActionWidth == 0) { 300 firstActionWidth = measuredWidth; 301 } 302 final int groupId = item.getGroupId(); 303 if (groupId != 0) { 304 seenGroups.put(groupId, true); 305 } 306 } else if (item.requestsActionButton()) { 307 // Items in a group with other items that already have an action slot 308 // can break the max actions rule, but not the width limit. 309 final int groupId = item.getGroupId(); 310 final boolean inGroup = seenGroups.get(groupId); 311 boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0; 312 maxActions--; 313 314 if (isAction) { 315 View v = item.getActionView(); 316 if (v == null) { 317 v = getItemView(item, mScrapActionButtonView, parent); 318 if (mScrapActionButtonView == null) { 319 mScrapActionButtonView = v; 320 } 321 } 322 v.measure(querySpec, querySpec); 323 final int measuredWidth = v.getMeasuredWidth(); 324 widthLimit -= measuredWidth; 325 if (firstActionWidth == 0) { 326 firstActionWidth = measuredWidth; 327 } 328 329 // Did this push the entire first item past halfway? 330 if (widthLimit + firstActionWidth <= 0) { 331 isAction = false; 332 } 333 } 334 335 if (isAction && groupId != 0) { 336 seenGroups.put(groupId, true); 337 } else if (inGroup) { 338 // We broke the width limit. Demote the whole group, they all overflow now. 339 seenGroups.put(groupId, false); 340 for (int j = 0; j < i; j++) { 341 MenuItemImpl areYouMyGroupie = visibleItems.get(j); 342 if (areYouMyGroupie.getGroupId() == groupId) { 343 areYouMyGroupie.setIsActionButton(false); 344 } 345 } 346 } 347 348 item.setIsActionButton(isAction); 349 } 350 } 351 return true; 352 } 353 354 @Override 355 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 356 dismissPopupMenus(); 357 super.onCloseMenu(menu, allMenusAreClosing); 358 } 359 360 private class OverflowMenuButton extends ImageButton implements ActionMenuChildView { 361 public OverflowMenuButton(Context context) { 362 super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); 363 364 setClickable(true); 365 setFocusable(true); 366 setVisibility(VISIBLE); 367 setEnabled(true); 368 } 369 370 @Override 371 public boolean performClick() { 372 if (super.performClick()) { 373 return true; 374 } 375 376 playSoundEffect(SoundEffectConstants.CLICK); 377 showOverflowMenu(); 378 return true; 379 } 380 381 public boolean needsDividerBefore() { 382 return true; 383 } 384 385 public boolean needsDividerAfter() { 386 return false; 387 } 388 } 389 390 private class OverflowPopup extends MenuPopupHelper { 391 public OverflowPopup(Context context, MenuBuilder menu, View anchorView, 392 boolean overflowOnly) { 393 super(context, menu, anchorView, overflowOnly); 394 } 395 396 @Override 397 public void onDismiss() { 398 super.onDismiss(); 399 mMenu.close(); 400 mOverflowPopup = null; 401 } 402 } 403 404 private class ActionButtonSubmenu extends MenuPopupHelper { 405 private SubMenuBuilder mSubMenu; 406 407 public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) { 408 super(context, subMenu); 409 mSubMenu = subMenu; 410 411 MenuItemImpl item = (MenuItemImpl) subMenu.getItem(); 412 if (!item.isActionButton()) { 413 // Give a reasonable anchor to nested submenus. 414 setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton); 415 } 416 } 417 418 @Override 419 public void onDismiss() { 420 super.onDismiss(); 421 mSubMenu.close(); 422 mActionButtonPopup = null; 423 } 424 } 425 426 private class OpenOverflowRunnable implements Runnable { 427 private OverflowPopup mPopup; 428 429 public OpenOverflowRunnable(OverflowPopup popup) { 430 mPopup = popup; 431 } 432 433 public void run() { 434 mMenu.changeMenuMode(); 435 if (mPopup.tryShow()) { 436 mOverflowPopup = mPopup; 437 mPostedOpenRunnable = null; 438 } 439 } 440 } 441} 442