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