ActionMenuView.java revision 160bb7fa60e8ece654e6ce999b6c16af50ee7357
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.widget.LinearLayout; 26 27/** 28 * @hide 29 */ 30public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView { 31 private static final String TAG = "ActionMenuView"; 32 33 static final int MIN_CELL_SIZE = 56; // dips 34 35 private MenuBuilder mMenu; 36 37 private boolean mReserveOverflow; 38 private ActionMenuPresenter mPresenter; 39 private boolean mUpdateContentsBeforeMeasure; 40 private boolean mFormatItems; 41 private int mMinCellSize; 42 private int mMeasuredExtraWidth; 43 44 public ActionMenuView(Context context) { 45 this(context, null); 46 } 47 48 public ActionMenuView(Context context, AttributeSet attrs) { 49 super(context, attrs); 50 setBaselineAligned(false); 51 mMinCellSize = (int) (MIN_CELL_SIZE * context.getResources().getDisplayMetrics().density); 52 } 53 54 public void setPresenter(ActionMenuPresenter presenter) { 55 mPresenter = presenter; 56 } 57 58 public boolean isExpandedFormat() { 59 return mFormatItems; 60 } 61 62 @Override 63 public void onConfigurationChanged(Configuration newConfig) { 64 super.onConfigurationChanged(newConfig); 65 mPresenter.updateMenuView(false); 66 67 if (mPresenter != null && mPresenter.isOverflowMenuShowing()) { 68 mPresenter.hideOverflowMenu(); 69 mPresenter.showOverflowMenu(); 70 } 71 } 72 73 @Override 74 public void requestLayout() { 75 // Layout can influence how many action items fit. 76 mUpdateContentsBeforeMeasure = true; 77 super.requestLayout(); 78 } 79 80 @Override 81 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 82 // If we've been given an exact size to match, apply special formatting during layout. 83 mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY; 84 if (mUpdateContentsBeforeMeasure && mMenu != null) { 85 mMenu.onItemsChanged(true); 86 mUpdateContentsBeforeMeasure = false; 87 } 88 89 if (mFormatItems) { 90 onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec); 91 } else { 92 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 93 } 94 } 95 96 private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) { 97 // We already know the width mode is EXACTLY if we're here. 98 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 99 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 100 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 101 102 final int widthPadding = getPaddingLeft() + getPaddingRight(); 103 final int heightPadding = getPaddingTop() + getPaddingBottom(); 104 105 widthSize -= widthPadding; 106 107 // Divide the view into cells. 108 final int cellCount = widthSize / mMinCellSize; 109 final int cellSizeRemaining = widthSize % mMinCellSize; 110 final int cellSize = mMinCellSize + cellSizeRemaining / cellCount; 111 112 int cellsRemaining = cellCount; 113 int maxChildHeight = 0; 114 int maxCellsUsed = 0; 115 int expandableItemCount = 0; 116 117 if (mReserveOverflow) cellsRemaining--; 118 119 final int childCount = getChildCount(); 120 for (int i = 0; i < childCount; i++) { 121 final View child = getChildAt(i); 122 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 123 lp.expanded = false; 124 lp.extraPixels = 0; 125 lp.cellsUsed = 0; 126 lp.expandable = false; 127 128 // Overflow always gets 1 cell. No more, no less. 129 final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining; 130 131 final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable, 132 heightMeasureSpec, heightPadding); 133 134 maxCellsUsed = Math.max(maxCellsUsed, cellsUsed); 135 if (lp.expandable) expandableItemCount++; 136 137 cellsRemaining -= cellsUsed; 138 maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()); 139 } 140 141 // Divide space for remaining cells if we have items that can expand. 142 // Try distributing whole leftover cells to smaller items first. 143 144 boolean needsExpansion = false; 145 long smallestExpandableItemsAt = 0; 146 while (expandableItemCount > 0 && cellsRemaining > 0) { 147 int minCells = Integer.MAX_VALUE; 148 long minCellsAt = 0; // Bit locations are indices of relevant child views 149 int minCellsItemCount = 0; 150 for (int i = 0; i < childCount; i++) { 151 final View child = getChildAt(i); 152 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 153 154 // Don't try to expand items that shouldn't. 155 if (!lp.expandable) continue; 156 157 // Mark indices of children that can receive an extra cell. 158 if (lp.cellsUsed < minCells) { 159 minCells = lp.cellsUsed; 160 minCellsAt = 1 << i; 161 minCellsItemCount = 1; 162 } else if (lp.cellsUsed == minCells) { 163 minCellsAt |= 1 << i; 164 minCellsItemCount++; 165 } 166 } 167 168 if (minCellsItemCount > cellsRemaining) break; // Couldn't expand anything evenly. Stop. 169 170 // Items that get expanded will always be in the set of smallest items when we're done. 171 smallestExpandableItemsAt |= minCellsAt; 172 173 for (int i = 0; i < childCount; i++) { 174 if ((minCellsAt & (1 << i)) == 0) continue; 175 176 final View child = getChildAt(i); 177 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 178 lp.cellsUsed++; 179 lp.expanded = true; 180 cellsRemaining--; 181 } 182 183 needsExpansion = true; 184 } 185 186 // Divide any space left that wouldn't divide along cell boundaries 187 // evenly among the smallest multi-cell (expandable) items. 188 189 if (cellsRemaining > 0 && smallestExpandableItemsAt != 0) { 190 final int expandCount = Long.bitCount(smallestExpandableItemsAt); 191 final int extraPixels = cellsRemaining * cellSize / expandCount; 192 193 for (int i = 0; i < childCount; i++) { 194 if ((smallestExpandableItemsAt & (1 << i)) == 0) continue; 195 196 final View child = getChildAt(i); 197 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 198 lp.extraPixels = extraPixels; 199 lp.expanded = true; 200 } 201 202 needsExpansion = true; 203 cellsRemaining = 0; 204 } 205 206 // Remeasure any items that have had extra space allocated to them. 207 if (needsExpansion) { 208 int heightSpec = MeasureSpec.makeMeasureSpec(heightSize - heightPadding, heightMode); 209 for (int i = 0; i < childCount; i++) { 210 final View child = getChildAt(i); 211 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 212 213 if (!lp.expanded) continue; 214 215 final int width = lp.cellsUsed * cellSize + lp.extraPixels; 216 child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), heightSpec); 217 } 218 } 219 220 if (heightMode != MeasureSpec.EXACTLY) { 221 heightSize = maxChildHeight; 222 } 223 224 setMeasuredDimension(widthSize, heightSize); 225 mMeasuredExtraWidth = cellsRemaining * cellSize; 226 } 227 228 /** 229 * Measure a child view to fit within cell-based formatting. The child's width 230 * will be measured to a whole multiple of cellSize. 231 * 232 * <p>Sets the expandable and cellsUsed fields of LayoutParams. 233 * 234 * @param child Child to measure 235 * @param cellSize Size of one cell 236 * @param cellsRemaining Number of cells remaining that this view can expand to fill 237 * @param parentHeightMeasureSpec MeasureSpec used by the parent view 238 * @param parentHeightPadding Padding present in the parent view 239 * @return Number of cells this child was measured to occupy 240 */ 241 static int measureChildForCells(View child, int cellSize, int cellsRemaining, 242 int parentHeightMeasureSpec, int parentHeightPadding) { 243 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 244 245 final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) - 246 parentHeightPadding; 247 final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec); 248 final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode); 249 250 int cellsUsed = 0; 251 if (cellsRemaining > 0) { 252 final int childWidthSpec = MeasureSpec.makeMeasureSpec( 253 cellSize * cellsRemaining, MeasureSpec.AT_MOST); 254 child.measure(childWidthSpec, childHeightSpec); 255 256 final int measuredWidth = child.getMeasuredWidth(); 257 cellsUsed = measuredWidth / cellSize; 258 if (measuredWidth % cellSize != 0) cellsUsed++; 259 } 260 261 final ActionMenuItemView itemView = child instanceof ActionMenuItemView ? 262 (ActionMenuItemView) child : null; 263 final boolean expandable = !lp.isOverflowButton && itemView != null && itemView.hasText(); 264 lp.expandable = expandable; 265 266 lp.cellsUsed = cellsUsed; 267 final int targetWidth = cellsUsed * cellSize; 268 child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY), 269 childHeightSpec); 270 return cellsUsed; 271 } 272 273 @Override 274 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 275 if (!mFormatItems) { 276 super.onLayout(changed, left, top, right, bottom); 277 return; 278 } 279 280 final int childCount = getChildCount(); 281 final int midVertical = (top + bottom) / 2; 282 final int dividerWidth = getDividerWidth(); 283 int overflowWidth = 0; 284 int nonOverflowWidth = 0; 285 int nonOverflowCount = 0; 286 int widthRemaining = right - left - getPaddingRight() - getPaddingLeft(); 287 boolean hasOverflow = false; 288 for (int i = 0; i < childCount; i++) { 289 final View v = getChildAt(i); 290 if (v.getVisibility() == GONE) { 291 continue; 292 } 293 294 LayoutParams p = (LayoutParams) v.getLayoutParams(); 295 if (p.isOverflowButton) { 296 overflowWidth = v.getMeasuredWidth(); 297 if (hasDividerBeforeChildAt(i)) { 298 overflowWidth += dividerWidth; 299 } 300 301 int height = v.getMeasuredHeight(); 302 int r = getWidth() - getPaddingRight(); 303 int l = r - overflowWidth; 304 int t = midVertical - (height / 2); 305 int b = t + height; 306 v.layout(l, t, r, b); 307 308 widthRemaining -= overflowWidth; 309 hasOverflow = true; 310 } else { 311 final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin; 312 nonOverflowWidth += size; 313 widthRemaining -= size; 314 if (hasDividerBeforeChildAt(i)) { 315 nonOverflowWidth += dividerWidth; 316 } 317 nonOverflowCount++; 318 } 319 } 320 321 final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1); 322 final int spacerSize = spacerCount > 0 ? widthRemaining / spacerCount : 0; 323 324 int startLeft = getPaddingLeft(); 325 for (int i = 0; i < childCount; i++) { 326 final View v = getChildAt(i); 327 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 328 if (v.getVisibility() == GONE || lp.isOverflowButton) { 329 continue; 330 } 331 332 startLeft += lp.leftMargin; 333 int width = v.getMeasuredWidth(); 334 int height = v.getMeasuredHeight(); 335 int t = midVertical - (height / 2); 336 v.layout(startLeft, t, startLeft + width, t + height); 337 startLeft += width + lp.rightMargin + spacerSize; 338 } 339 } 340 341 @Override 342 public void onDetachedFromWindow() { 343 super.onDetachedFromWindow(); 344 mPresenter.dismissPopupMenus(); 345 } 346 347 public boolean isOverflowReserved() { 348 return mReserveOverflow; 349 } 350 351 public void setOverflowReserved(boolean reserveOverflow) { 352 mReserveOverflow = reserveOverflow; 353 } 354 355 @Override 356 protected LayoutParams generateDefaultLayoutParams() { 357 LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, 358 LayoutParams.WRAP_CONTENT); 359 params.gravity = Gravity.CENTER_VERTICAL; 360 return params; 361 } 362 363 @Override 364 public LayoutParams generateLayoutParams(AttributeSet attrs) { 365 return new LayoutParams(getContext(), attrs); 366 } 367 368 @Override 369 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 370 if (p instanceof LayoutParams) { 371 LayoutParams result = new LayoutParams((LayoutParams) p); 372 if (result.gravity <= Gravity.NO_GRAVITY) { 373 result.gravity = Gravity.CENTER_VERTICAL; 374 } 375 return result; 376 } 377 return generateDefaultLayoutParams(); 378 } 379 380 @Override 381 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 382 return p != null && p instanceof LayoutParams; 383 } 384 385 public LayoutParams generateOverflowButtonLayoutParams() { 386 LayoutParams result = generateDefaultLayoutParams(); 387 result.isOverflowButton = true; 388 return result; 389 } 390 391 public boolean invokeItem(MenuItemImpl item) { 392 return mMenu.performItemAction(item, 0); 393 } 394 395 public int getWindowAnimations() { 396 return 0; 397 } 398 399 public void initialize(MenuBuilder menu) { 400 mMenu = menu; 401 } 402 403 @Override 404 protected boolean hasDividerBeforeChildAt(int childIndex) { 405 final View childBefore = getChildAt(childIndex - 1); 406 final View child = getChildAt(childIndex); 407 boolean result = false; 408 if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) { 409 result |= ((ActionMenuChildView) childBefore).needsDividerAfter(); 410 } 411 if (childIndex > 0 && child instanceof ActionMenuChildView) { 412 result |= ((ActionMenuChildView) child).needsDividerBefore(); 413 } 414 return result; 415 } 416 417 public interface ActionMenuChildView { 418 public boolean needsDividerBefore(); 419 public boolean needsDividerAfter(); 420 } 421 422 public static class LayoutParams extends LinearLayout.LayoutParams { 423 @ViewDebug.ExportedProperty(category = "layout") 424 public boolean isOverflowButton; 425 @ViewDebug.ExportedProperty(category = "layout") 426 public int cellsUsed; 427 @ViewDebug.ExportedProperty(category = "layout") 428 public int extraPixels; 429 @ViewDebug.ExportedProperty(category = "layout") 430 public boolean expandable; 431 432 public boolean expanded; 433 434 public LayoutParams(Context c, AttributeSet attrs) { 435 super(c, attrs); 436 } 437 438 public LayoutParams(LayoutParams other) { 439 super((LinearLayout.LayoutParams) other); 440 isOverflowButton = other.isOverflowButton; 441 } 442 443 public LayoutParams(int width, int height) { 444 super(width, height); 445 isOverflowButton = false; 446 } 447 448 public LayoutParams(int width, int height, boolean isOverflowButton) { 449 super(width, height); 450 this.isOverflowButton = isOverflowButton; 451 } 452 } 453} 454