ActionMenuView.java revision fa18d182a3f37505940e73ae6cd76c2e939f7f7c
1/* 2 * Copyright (C) 2010 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 */ 16package android.widget; 17 18import android.content.Context; 19import android.content.res.Configuration; 20import android.util.AttributeSet; 21import android.view.Gravity; 22import android.view.Menu; 23import android.view.View; 24import android.view.ViewDebug; 25import android.view.ViewGroup; 26import android.view.accessibility.AccessibilityEvent; 27import com.android.internal.view.menu.ActionMenuItemView; 28import com.android.internal.view.menu.MenuBuilder; 29import com.android.internal.view.menu.MenuItemImpl; 30import com.android.internal.view.menu.MenuView; 31 32/** 33 * ActionMenuView is a presentation of a series of menu options as a View. It provides 34 * several top level options as action buttons while spilling remaining options over as 35 * items in an overflow menu. This allows applications to present packs of actions inline with 36 * specific or repeating content. 37 */ 38public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView { 39 private static final String TAG = "ActionMenuView"; 40 41 static final int MIN_CELL_SIZE = 56; // dips 42 static final int GENERATED_ITEM_PADDING = 4; // dips 43 44 private MenuBuilder mMenu; 45 46 private boolean mReserveOverflow; 47 private ActionMenuPresenter mPresenter; 48 private boolean mFormatItems; 49 private int mFormatItemsWidth; 50 private int mMinCellSize; 51 private int mGeneratedItemPadding; 52 53 public ActionMenuView(Context context) { 54 this(context, null); 55 } 56 57 public ActionMenuView(Context context, AttributeSet attrs) { 58 super(context, attrs); 59 setBaselineAligned(false); 60 final float density = context.getResources().getDisplayMetrics().density; 61 mMinCellSize = (int) (MIN_CELL_SIZE * density); 62 mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density); 63 } 64 65 /** @hide */ 66 public void setPresenter(ActionMenuPresenter presenter) { 67 mPresenter = presenter; 68 } 69 70 @Override 71 public void onConfigurationChanged(Configuration newConfig) { 72 super.onConfigurationChanged(newConfig); 73 mPresenter.updateMenuView(false); 74 75 if (mPresenter != null && mPresenter.isOverflowMenuShowing()) { 76 mPresenter.hideOverflowMenu(); 77 mPresenter.showOverflowMenu(); 78 } 79 } 80 81 @Override 82 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 83 // If we've been given an exact size to match, apply special formatting during layout. 84 final boolean wasFormatted = mFormatItems; 85 mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY; 86 87 if (wasFormatted != mFormatItems) { 88 mFormatItemsWidth = 0; // Reset this when switching modes 89 } 90 91 // Special formatting can change whether items can fit as action buttons. 92 // Kick the menu and update presenters when this changes. 93 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 94 if (mFormatItems && mMenu != null && widthSize != mFormatItemsWidth) { 95 mFormatItemsWidth = widthSize; 96 mMenu.onItemsChanged(true); 97 } 98 99 if (mFormatItems) { 100 onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec); 101 } else { 102 // Previous measurement at exact format may have set margins - reset them. 103 final int childCount = getChildCount(); 104 for (int i = 0; i < childCount; i++) { 105 final View child = getChildAt(i); 106 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 107 lp.leftMargin = lp.rightMargin = 0; 108 } 109 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 110 } 111 } 112 113 private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) { 114 // We already know the width mode is EXACTLY if we're here. 115 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 116 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 117 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 118 119 final int widthPadding = getPaddingLeft() + getPaddingRight(); 120 final int heightPadding = getPaddingTop() + getPaddingBottom(); 121 122 final int itemHeightSpec = getChildMeasureSpec(heightMeasureSpec, heightPadding, 123 ViewGroup.LayoutParams.WRAP_CONTENT); 124 125 widthSize -= widthPadding; 126 127 // Divide the view into cells. 128 final int cellCount = widthSize / mMinCellSize; 129 final int cellSizeRemaining = widthSize % mMinCellSize; 130 131 if (cellCount == 0) { 132 // Give up, nothing fits. 133 setMeasuredDimension(widthSize, 0); 134 return; 135 } 136 137 final int cellSize = mMinCellSize + cellSizeRemaining / cellCount; 138 139 int cellsRemaining = cellCount; 140 int maxChildHeight = 0; 141 int maxCellsUsed = 0; 142 int expandableItemCount = 0; 143 int visibleItemCount = 0; 144 boolean hasOverflow = false; 145 146 // This is used as a bitfield to locate the smallest items present. Assumes childCount < 64. 147 long smallestItemsAt = 0; 148 149 final int childCount = getChildCount(); 150 for (int i = 0; i < childCount; i++) { 151 final View child = getChildAt(i); 152 if (child.getVisibility() == GONE) continue; 153 154 final boolean isGeneratedItem = child instanceof ActionMenuItemView; 155 visibleItemCount++; 156 157 if (isGeneratedItem) { 158 // Reset padding for generated menu item views; it may change below 159 // and views are recycled. 160 child.setPadding(mGeneratedItemPadding, 0, mGeneratedItemPadding, 0); 161 } 162 163 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 164 lp.expanded = false; 165 lp.extraPixels = 0; 166 lp.cellsUsed = 0; 167 lp.expandable = false; 168 lp.leftMargin = 0; 169 lp.rightMargin = 0; 170 lp.preventEdgeOffset = isGeneratedItem && ((ActionMenuItemView) child).hasText(); 171 172 // Overflow always gets 1 cell. No more, no less. 173 final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining; 174 175 final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable, 176 itemHeightSpec, heightPadding); 177 178 maxCellsUsed = Math.max(maxCellsUsed, cellsUsed); 179 if (lp.expandable) expandableItemCount++; 180 if (lp.isOverflowButton) hasOverflow = true; 181 182 cellsRemaining -= cellsUsed; 183 maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()); 184 if (cellsUsed == 1) smallestItemsAt |= (1 << i); 185 } 186 187 // When we have overflow and a single expanded (text) item, we want to try centering it 188 // visually in the available space even though overflow consumes some of it. 189 final boolean centerSingleExpandedItem = hasOverflow && visibleItemCount == 2; 190 191 // Divide space for remaining cells if we have items that can expand. 192 // Try distributing whole leftover cells to smaller items first. 193 194 boolean needsExpansion = false; 195 while (expandableItemCount > 0 && cellsRemaining > 0) { 196 int minCells = Integer.MAX_VALUE; 197 long minCellsAt = 0; // Bit locations are indices of relevant child views 198 int minCellsItemCount = 0; 199 for (int i = 0; i < childCount; i++) { 200 final View child = getChildAt(i); 201 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 202 203 // Don't try to expand items that shouldn't. 204 if (!lp.expandable) continue; 205 206 // Mark indices of children that can receive an extra cell. 207 if (lp.cellsUsed < minCells) { 208 minCells = lp.cellsUsed; 209 minCellsAt = 1 << i; 210 minCellsItemCount = 1; 211 } else if (lp.cellsUsed == minCells) { 212 minCellsAt |= 1 << i; 213 minCellsItemCount++; 214 } 215 } 216 217 // Items that get expanded will always be in the set of smallest items when we're done. 218 smallestItemsAt |= minCellsAt; 219 220 if (minCellsItemCount > cellsRemaining) break; // Couldn't expand anything evenly. Stop. 221 222 // We have enough cells, all minimum size items will be incremented. 223 minCells++; 224 225 for (int i = 0; i < childCount; i++) { 226 final View child = getChildAt(i); 227 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 228 if ((minCellsAt & (1 << i)) == 0) { 229 // If this item is already at our small item count, mark it for later. 230 if (lp.cellsUsed == minCells) smallestItemsAt |= 1 << i; 231 continue; 232 } 233 234 if (centerSingleExpandedItem && lp.preventEdgeOffset && cellsRemaining == 1) { 235 // Add padding to this item such that it centers. 236 child.setPadding(mGeneratedItemPadding + cellSize, 0, mGeneratedItemPadding, 0); 237 } 238 lp.cellsUsed++; 239 lp.expanded = true; 240 cellsRemaining--; 241 } 242 243 needsExpansion = true; 244 } 245 246 // Divide any space left that wouldn't divide along cell boundaries 247 // evenly among the smallest items 248 249 final boolean singleItem = !hasOverflow && visibleItemCount == 1; 250 if (cellsRemaining > 0 && smallestItemsAt != 0 && 251 (cellsRemaining < visibleItemCount - 1 || singleItem || maxCellsUsed > 1)) { 252 float expandCount = Long.bitCount(smallestItemsAt); 253 254 if (!singleItem) { 255 // The items at the far edges may only expand by half in order to pin to either side. 256 if ((smallestItemsAt & 1) != 0) { 257 LayoutParams lp = (LayoutParams) getChildAt(0).getLayoutParams(); 258 if (!lp.preventEdgeOffset) expandCount -= 0.5f; 259 } 260 if ((smallestItemsAt & (1 << (childCount - 1))) != 0) { 261 LayoutParams lp = ((LayoutParams) getChildAt(childCount - 1).getLayoutParams()); 262 if (!lp.preventEdgeOffset) expandCount -= 0.5f; 263 } 264 } 265 266 final int extraPixels = expandCount > 0 ? 267 (int) (cellsRemaining * cellSize / expandCount) : 0; 268 269 for (int i = 0; i < childCount; i++) { 270 if ((smallestItemsAt & (1 << i)) == 0) continue; 271 272 final View child = getChildAt(i); 273 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 274 if (child instanceof ActionMenuItemView) { 275 // If this is one of our views, expand and measure at the larger size. 276 lp.extraPixels = extraPixels; 277 lp.expanded = true; 278 if (i == 0 && !lp.preventEdgeOffset) { 279 // First item gets part of its new padding pushed out of sight. 280 // The last item will get this implicitly from layout. 281 lp.leftMargin = -extraPixels / 2; 282 } 283 needsExpansion = true; 284 } else if (lp.isOverflowButton) { 285 lp.extraPixels = extraPixels; 286 lp.expanded = true; 287 lp.rightMargin = -extraPixels / 2; 288 needsExpansion = true; 289 } else { 290 // If we don't know what it is, give it some margins instead 291 // and let it center within its space. We still want to pin 292 // against the edges. 293 if (i != 0) { 294 lp.leftMargin = extraPixels / 2; 295 } 296 if (i != childCount - 1) { 297 lp.rightMargin = extraPixels / 2; 298 } 299 } 300 } 301 302 cellsRemaining = 0; 303 } 304 305 // Remeasure any items that have had extra space allocated to them. 306 if (needsExpansion) { 307 for (int i = 0; i < childCount; i++) { 308 final View child = getChildAt(i); 309 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 310 311 if (!lp.expanded) continue; 312 313 final int width = lp.cellsUsed * cellSize + lp.extraPixels; 314 child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 315 itemHeightSpec); 316 } 317 } 318 319 if (heightMode != MeasureSpec.EXACTLY) { 320 heightSize = maxChildHeight; 321 } 322 323 setMeasuredDimension(widthSize, heightSize); 324 } 325 326 /** 327 * Measure a child view to fit within cell-based formatting. The child's width 328 * will be measured to a whole multiple of cellSize. 329 * 330 * <p>Sets the expandable and cellsUsed fields of LayoutParams. 331 * 332 * @param child Child to measure 333 * @param cellSize Size of one cell 334 * @param cellsRemaining Number of cells remaining that this view can expand to fill 335 * @param parentHeightMeasureSpec MeasureSpec used by the parent view 336 * @param parentHeightPadding Padding present in the parent view 337 * @return Number of cells this child was measured to occupy 338 */ 339 static int measureChildForCells(View child, int cellSize, int cellsRemaining, 340 int parentHeightMeasureSpec, int parentHeightPadding) { 341 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 342 343 final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) - 344 parentHeightPadding; 345 final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec); 346 final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode); 347 348 final ActionMenuItemView itemView = child instanceof ActionMenuItemView ? 349 (ActionMenuItemView) child : null; 350 final boolean hasText = itemView != null && itemView.hasText(); 351 352 int cellsUsed = 0; 353 if (cellsRemaining > 0 && (!hasText || cellsRemaining >= 2)) { 354 final int childWidthSpec = MeasureSpec.makeMeasureSpec( 355 cellSize * cellsRemaining, MeasureSpec.AT_MOST); 356 child.measure(childWidthSpec, childHeightSpec); 357 358 final int measuredWidth = child.getMeasuredWidth(); 359 cellsUsed = measuredWidth / cellSize; 360 if (measuredWidth % cellSize != 0) cellsUsed++; 361 if (hasText && cellsUsed < 2) cellsUsed = 2; 362 } 363 364 final boolean expandable = !lp.isOverflowButton && hasText; 365 lp.expandable = expandable; 366 367 lp.cellsUsed = cellsUsed; 368 final int targetWidth = cellsUsed * cellSize; 369 child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY), 370 childHeightSpec); 371 return cellsUsed; 372 } 373 374 @Override 375 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 376 if (!mFormatItems) { 377 super.onLayout(changed, left, top, right, bottom); 378 return; 379 } 380 381 final int childCount = getChildCount(); 382 final int midVertical = (top + bottom) / 2; 383 final int dividerWidth = getDividerWidth(); 384 int overflowWidth = 0; 385 int nonOverflowWidth = 0; 386 int nonOverflowCount = 0; 387 int widthRemaining = right - left - getPaddingRight() - getPaddingLeft(); 388 boolean hasOverflow = false; 389 final boolean isLayoutRtl = isLayoutRtl(); 390 for (int i = 0; i < childCount; i++) { 391 final View v = getChildAt(i); 392 if (v.getVisibility() == GONE) { 393 continue; 394 } 395 396 LayoutParams p = (LayoutParams) v.getLayoutParams(); 397 if (p.isOverflowButton) { 398 overflowWidth = v.getMeasuredWidth(); 399 if (hasDividerBeforeChildAt(i)) { 400 overflowWidth += dividerWidth; 401 } 402 403 int height = v.getMeasuredHeight(); 404 int r; 405 int l; 406 if (isLayoutRtl) { 407 l = getPaddingLeft() + p.leftMargin; 408 r = l + overflowWidth; 409 } else { 410 r = getWidth() - getPaddingRight() - p.rightMargin; 411 l = r - overflowWidth; 412 } 413 int t = midVertical - (height / 2); 414 int b = t + height; 415 v.layout(l, t, r, b); 416 417 widthRemaining -= overflowWidth; 418 hasOverflow = true; 419 } else { 420 final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin; 421 nonOverflowWidth += size; 422 widthRemaining -= size; 423 if (hasDividerBeforeChildAt(i)) { 424 nonOverflowWidth += dividerWidth; 425 } 426 nonOverflowCount++; 427 } 428 } 429 430 if (childCount == 1 && !hasOverflow) { 431 // Center a single child 432 final View v = getChildAt(0); 433 final int width = v.getMeasuredWidth(); 434 final int height = v.getMeasuredHeight(); 435 final int midHorizontal = (right - left) / 2; 436 final int l = midHorizontal - width / 2; 437 final int t = midVertical - height / 2; 438 v.layout(l, t, l + width, t + height); 439 return; 440 } 441 442 final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1); 443 final int spacerSize = Math.max(0, spacerCount > 0 ? widthRemaining / spacerCount : 0); 444 445 if (isLayoutRtl) { 446 int startRight = getWidth() - getPaddingRight(); 447 for (int i = 0; i < childCount; i++) { 448 final View v = getChildAt(i); 449 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 450 if (v.getVisibility() == GONE || lp.isOverflowButton) { 451 continue; 452 } 453 454 startRight -= lp.rightMargin; 455 int width = v.getMeasuredWidth(); 456 int height = v.getMeasuredHeight(); 457 int t = midVertical - height / 2; 458 v.layout(startRight - width, t, startRight, t + height); 459 startRight -= width + lp.leftMargin + spacerSize; 460 } 461 } else { 462 int startLeft = getPaddingLeft(); 463 for (int i = 0; i < childCount; i++) { 464 final View v = getChildAt(i); 465 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 466 if (v.getVisibility() == GONE || lp.isOverflowButton) { 467 continue; 468 } 469 470 startLeft += lp.leftMargin; 471 int width = v.getMeasuredWidth(); 472 int height = v.getMeasuredHeight(); 473 int t = midVertical - height / 2; 474 v.layout(startLeft, t, startLeft + width, t + height); 475 startLeft += width + lp.rightMargin + spacerSize; 476 } 477 } 478 } 479 480 @Override 481 public void onDetachedFromWindow() { 482 super.onDetachedFromWindow(); 483 mPresenter.dismissPopupMenus(); 484 } 485 486 /** @hide */ 487 public boolean isOverflowReserved() { 488 return mReserveOverflow; 489 } 490 491 /** @hide */ 492 public void setOverflowReserved(boolean reserveOverflow) { 493 mReserveOverflow = reserveOverflow; 494 } 495 496 @Override 497 protected LayoutParams generateDefaultLayoutParams() { 498 LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, 499 LayoutParams.WRAP_CONTENT); 500 params.gravity = Gravity.CENTER_VERTICAL; 501 return params; 502 } 503 504 @Override 505 public LayoutParams generateLayoutParams(AttributeSet attrs) { 506 return new LayoutParams(getContext(), attrs); 507 } 508 509 @Override 510 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 511 if (p != null) { 512 final LayoutParams result = p instanceof LayoutParams 513 ? new LayoutParams((LayoutParams) p) 514 : new LayoutParams(p); 515 if (result.gravity <= Gravity.NO_GRAVITY) { 516 result.gravity = Gravity.CENTER_VERTICAL; 517 } 518 return result; 519 } 520 return generateDefaultLayoutParams(); 521 } 522 523 @Override 524 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 525 return p != null && p instanceof LayoutParams; 526 } 527 528 /** @hide */ 529 public LayoutParams generateOverflowButtonLayoutParams() { 530 LayoutParams result = generateDefaultLayoutParams(); 531 result.isOverflowButton = true; 532 return result; 533 } 534 535 /** @hide */ 536 public boolean invokeItem(MenuItemImpl item) { 537 return mMenu.performItemAction(item, 0); 538 } 539 540 /** @hide */ 541 public int getWindowAnimations() { 542 return 0; 543 } 544 545 /** @hide */ 546 public void initialize(MenuBuilder menu) { 547 mMenu = menu; 548 } 549 550 /** 551 * Returns the Menu object that this ActionMenuView is currently presenting. 552 * 553 * <p>Applications should use this method to obtain the ActionMenuView's Menu object 554 * and inflate or add content to it as necessary.</p> 555 * 556 * @return the Menu presented by this view 557 */ 558 public Menu getMenu() { 559 if (mMenu == null) { 560 final Context context = getContext(); 561 mMenu = new MenuBuilder(context); 562 mPresenter = new ActionMenuPresenter(context); 563 mPresenter.initForMenu(context, mMenu); 564 mPresenter.setMenuView(this); 565 } 566 567 return mMenu; 568 } 569 570 /** 571 * @hide Private LinearLayout (superclass) API. Un-hide if LinearLayout API is made public. 572 */ 573 @Override 574 protected boolean hasDividerBeforeChildAt(int childIndex) { 575 if (childIndex == 0) { 576 return false; 577 } 578 final View childBefore = getChildAt(childIndex - 1); 579 final View child = getChildAt(childIndex); 580 boolean result = false; 581 if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) { 582 result |= ((ActionMenuChildView) childBefore).needsDividerAfter(); 583 } 584 if (childIndex > 0 && child instanceof ActionMenuChildView) { 585 result |= ((ActionMenuChildView) child).needsDividerBefore(); 586 } 587 return result; 588 } 589 590 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 591 return false; 592 } 593 594 /** @hide */ 595 public interface ActionMenuChildView { 596 public boolean needsDividerBefore(); 597 public boolean needsDividerAfter(); 598 } 599 600 public static class LayoutParams extends LinearLayout.LayoutParams { 601 /** @hide */ 602 @ViewDebug.ExportedProperty(category = "layout") 603 public boolean isOverflowButton; 604 605 /** @hide */ 606 @ViewDebug.ExportedProperty(category = "layout") 607 public int cellsUsed; 608 609 /** @hide */ 610 @ViewDebug.ExportedProperty(category = "layout") 611 public int extraPixels; 612 613 /** @hide */ 614 @ViewDebug.ExportedProperty(category = "layout") 615 public boolean expandable; 616 617 /** @hide */ 618 @ViewDebug.ExportedProperty(category = "layout") 619 public boolean preventEdgeOffset; 620 621 /** @hide */ 622 public boolean expanded; 623 624 public LayoutParams(Context c, AttributeSet attrs) { 625 super(c, attrs); 626 } 627 628 public LayoutParams(ViewGroup.LayoutParams other) { 629 super(other); 630 } 631 632 public LayoutParams(LayoutParams other) { 633 super((LinearLayout.LayoutParams) other); 634 isOverflowButton = other.isOverflowButton; 635 } 636 637 public LayoutParams(int width, int height) { 638 super(width, height); 639 isOverflowButton = false; 640 } 641 642 /** @hide */ 643 public LayoutParams(int width, int height, boolean isOverflowButton) { 644 super(width, height); 645 this.isOverflowButton = isOverflowButton; 646 } 647 } 648} 649