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