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