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