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