IconMenuView.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
1/* 2 * Copyright (C) 2006 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 */ 16 17package com.android.internal.view.menu; 18 19import com.android.internal.view.menu.MenuBuilder.ItemInvoker; 20 21import android.content.Context; 22import android.content.res.Resources; 23import android.content.res.TypedArray; 24import android.graphics.Canvas; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.os.Parcel; 28import android.os.Parcelable; 29import android.util.AttributeSet; 30import android.view.KeyEvent; 31import android.view.View; 32import android.view.ViewConfiguration; 33import android.view.ViewGroup; 34import android.view.LayoutInflater; 35 36import java.util.ArrayList; 37 38/** 39 * The icon menu view is an icon-based menu usually with a subset of all the menu items. 40 * It is opened as the default menu, and shows either the first five or all six of the menu items 41 * with text and icon. In the situation of there being more than six items, the first five items 42 * will be accompanied with a 'More' button that opens an {@link ExpandedMenuView} which lists 43 * all the menu items. 44 * 45 * @attr ref android.R.styleable#IconMenuView_rowHeight 46 * @attr ref android.R.styleable#IconMenuView_maxRows 47 * @attr ref android.R.styleable#IconMenuView_maxItemsPerRow 48 * 49 * @hide 50 */ 51public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuView, Runnable { 52 private static final int ITEM_CAPTION_CYCLE_DELAY = 1000; 53 54 private MenuBuilder mMenu; 55 56 /** Height of each row */ 57 private int mRowHeight; 58 /** Maximum number of rows to be shown */ 59 private int mMaxRows; 60 /** Maximum number of items per row */ 61 private int mMaxItemsPerRow; 62 /** Actual number of items (the 'More' view does not count as an item) shown */ 63 private int mNumActualItemsShown; 64 65 /** Divider that is drawn between all rows */ 66 private Drawable mHorizontalDivider; 67 /** Height of the horizontal divider */ 68 private int mHorizontalDividerHeight; 69 /** Set of horizontal divider positions where the horizontal divider will be drawn */ 70 private ArrayList<Rect> mHorizontalDividerRects; 71 72 /** Divider that is drawn between all columns */ 73 private Drawable mVerticalDivider; 74 /** Width of the vertical divider */ 75 private int mVerticalDividerWidth; 76 /** Set of vertical divider positions where the vertical divider will be drawn */ 77 private ArrayList<Rect> mVerticalDividerRects; 78 79 /** Item view for the 'More' button */ 80 private IconMenuItemView mMoreItemView; 81 82 /** Background of each item (should contain the selected and focused states) */ 83 private Drawable mItemBackground; 84 85 /** Icon for the 'More' button */ 86 private Drawable mMoreIcon; 87 88 /** Default animations for this menu */ 89 private int mAnimations; 90 91 /** 92 * Whether this IconMenuView has stale children and needs to update them. 93 * Set true by {@link #markStaleChildren()} and reset to false by 94 * {@link #onMeasure(int, int)} 95 */ 96 private boolean mHasStaleChildren; 97 98 /** 99 * Longpress on MENU (while this is shown) switches to shortcut caption 100 * mode. When the user releases the longpress, we do not want to pass the 101 * key-up event up since that will dismiss the menu. 102 */ 103 private boolean mMenuBeingLongpressed = false; 104 105 /** 106 * While {@link #mMenuBeingLongpressed}, we toggle the children's caption 107 * mode between each's title and its shortcut. This is the last caption mode 108 * we broadcasted to children. 109 */ 110 private boolean mLastChildrenCaptionMode; 111 112 /** 113 * Instantiates the IconMenuView that is linked with the provided MenuBuilder. 114 */ 115 public IconMenuView(Context context, AttributeSet attrs) { 116 super(context, attrs); 117 118 TypedArray a = 119 context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.IconMenuView, 0, 0); 120 mRowHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.IconMenuView_rowHeight, 64); 121 mMaxRows = a.getInt(com.android.internal.R.styleable.IconMenuView_maxRows, 2); 122 mMaxItemsPerRow = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItemsPerRow, 3); 123 mMoreIcon = a.getDrawable(com.android.internal.R.styleable.IconMenuView_moreIcon); 124 a.recycle(); 125 126 a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuView, 0, 0); 127 mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground); 128 mHorizontalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider); 129 mHorizontalDividerRects = new ArrayList<Rect>(); 130 mVerticalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider); 131 mVerticalDividerRects = new ArrayList<Rect>(); 132 mAnimations = a.getResourceId(com.android.internal.R.styleable.MenuView_windowAnimationStyle, 0); 133 a.recycle(); 134 135 if (mHorizontalDivider != null) { 136 mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight(); 137 // Make sure to have some height for the divider 138 if (mHorizontalDividerHeight == -1) mHorizontalDividerHeight = 1; 139 } 140 141 if (mVerticalDivider != null) { 142 mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth(); 143 // Make sure to have some width for the divider 144 if (mVerticalDividerWidth == -1) mVerticalDividerWidth = 1; 145 } 146 147 // This view will be drawing the dividers 148 setWillNotDraw(false); 149 150 // This is so we'll receive the MENU key in touch mode 151 setFocusableInTouchMode(true); 152 // This is so our children can still be arrow-key focused 153 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 154 } 155 156 /** 157 * Calculates the minimum number of rows needed to the items to be shown. 158 * @return the minimum number of rows 159 */ 160 private int calculateNumberOfRows() { 161 return Math.min((int) Math.ceil(getChildCount() / (double) mMaxItemsPerRow), mMaxRows); 162 } 163 164 /** 165 * Adds an IconMenuItemView to this icon menu view. 166 * @param itemView The item's view to add 167 */ 168 private void addItemView(IconMenuItemView itemView) { 169 ViewGroup.LayoutParams lp = itemView.getLayoutParams(); 170 171 if (lp == null) { 172 // Default layout parameters 173 lp = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); 174 } 175 176 // Set ourselves on the item view 177 itemView.setIconMenuView(this); 178 179 // Apply the background to the item view 180 itemView.setBackgroundDrawable(mItemBackground.getConstantState().newDrawable()); 181 182 // This class is the invoker for all its item views 183 itemView.setItemInvoker(this); 184 185 addView(itemView, lp); 186 } 187 188 /** 189 * Creates the item view for the 'More' button which is used to switch to 190 * the expanded menu view. This button is a special case since it does not 191 * have a MenuItemData backing it. 192 * @return The IconMenuItemView for the 'More' button 193 */ 194 private IconMenuItemView createMoreItemView() { 195 LayoutInflater inflater = mMenu.getMenuType(MenuBuilder.TYPE_ICON).getInflater(); 196 197 final IconMenuItemView itemView = (IconMenuItemView) inflater.inflate( 198 com.android.internal.R.layout.icon_menu_item_layout, null); 199 200 Resources r = getContext().getResources(); 201 itemView.initialize(r.getText(com.android.internal.R.string.more_item_label), mMoreIcon); 202 203 // Set up a click listener on the view since there will be no invocation sequence 204 // due to the lack of a MenuItemData this view 205 itemView.setOnClickListener(new OnClickListener() { 206 public void onClick(View v) { 207 // Switches the menu to expanded mode 208 MenuBuilder.Callback cb = mMenu.getCallback(); 209 if (cb != null) { 210 // Call callback 211 cb.onMenuModeChange(mMenu); 212 } 213 } 214 }); 215 216 return itemView; 217 } 218 219 220 public void initialize(MenuBuilder menu, int menuType) { 221 mMenu = menu; 222 updateChildren(true); 223 } 224 225 public void updateChildren(boolean cleared) { 226 // This method does a clear refresh of children 227 removeAllViews(); 228 229 final ArrayList<MenuItemImpl> itemsToShow = mMenu.getVisibleItems(); 230 final int numItems = itemsToShow.size(); 231 final int numItemsThatCanFit = mMaxItemsPerRow * mMaxRows; 232 // Minimum of the num that can fit and the num that we have 233 final int minFitMinus1AndNumItems = Math.min(numItemsThatCanFit - 1, numItems); 234 235 MenuItemImpl itemData; 236 // Traverse through all but the last item that can fit since that last item can either 237 // be a 'More' button or a sixth item 238 for (int i = 0; i < minFitMinus1AndNumItems; i++) { 239 itemData = itemsToShow.get(i); 240 addItemView((IconMenuItemView) itemData.getItemView(MenuBuilder.TYPE_ICON, this)); 241 } 242 243 if (numItems > numItemsThatCanFit) { 244 // If there are more items than we can fit, show the 'More' button to 245 // switch to expanded mode 246 if (mMoreItemView == null) { 247 mMoreItemView = createMoreItemView(); 248 } 249 250 addItemView(mMoreItemView); 251 252 // The last view is the more button, so the actual number of items is one less than 253 // the number that can fit 254 mNumActualItemsShown = numItemsThatCanFit - 1; 255 } else if (numItems == numItemsThatCanFit) { 256 // There are exactly the number we can show, so show the last item 257 final MenuItemImpl lastItemData = itemsToShow.get(numItemsThatCanFit - 1); 258 addItemView((IconMenuItemView) lastItemData.getItemView(MenuBuilder.TYPE_ICON, this)); 259 260 // The items shown fit exactly 261 mNumActualItemsShown = numItemsThatCanFit; 262 } 263 } 264 265 /** 266 * Calculates the number of items that should go on each row of this menu view. 267 * @param numRows the total number of rows for the menu view 268 * @param numItems the total number of items (across all rows) contained in the menu view 269 * @return int[] where index i contains the number of items for row i 270 */ 271 private int[] calculateNumberOfItemsPerRow(final int numRows, final int numItems) { 272 // TODO: get from theme? or write a best-fit algorithm? either way, this hard-coding needs 273 // to be dropped (946635). Right now, this is according to UI spec. 274 final int numItemsForRow[] = new int[numRows]; 275 if (numRows == 2) { 276 if (numItems <= 5) { 277 numItemsForRow[0] = 2; 278 numItemsForRow[1] = numItems - 2; 279 } else { 280 numItemsForRow[0] = numItemsForRow[1] = mMaxItemsPerRow; 281 } 282 } else if (numRows == 1) { 283 numItemsForRow[0] = numItems; 284 } 285 286 return numItemsForRow; 287 } 288 289 /** 290 * The positioning algorithm that gets called from onMeasure. It 291 * just computes positions for each child, and then stores them in the child's layout params. 292 * @param menuWidth The width of this menu to assume for positioning 293 * @param menuHeight The height of this menu to assume for positioning 294 */ 295 private void positionChildren(int menuWidth, int menuHeight) { 296 // Clear the containers for the positions where the dividers should be drawn 297 if (mHorizontalDivider != null) mHorizontalDividerRects.clear(); 298 if (mVerticalDivider != null) mVerticalDividerRects.clear(); 299 300 // Get the minimum number of rows needed 301 final int numRows = calculateNumberOfRows(); 302 final int numRowsMinus1 = numRows - 1; 303 final int numItems = getChildCount(); 304 final int numItemsForRow[] = calculateNumberOfItemsPerRow(numRows, numItems); 305 306 // The item position across all rows 307 int itemPos = 0; 308 View child; 309 IconMenuView.LayoutParams childLayoutParams = null; 310 311 // Use float for this to get precise positions (uniform item widths 312 // instead of last one taking any slack), and then convert to ints at last opportunity 313 float itemLeft; 314 float itemTop = 0; 315 // Since each row can have a different number of items, this will be computed per row 316 float itemWidth; 317 // Subtract the space needed for the horizontal dividers 318 final float itemHeight = (menuHeight - mHorizontalDividerHeight * (numRows - 1)) 319 / (float)numRows; 320 321 for (int row = 0; row < numRows; row++) { 322 // Start at the left 323 itemLeft = 0; 324 325 // Subtract the space needed for the vertical dividers, and divide by the number of items 326 itemWidth = (menuWidth - mVerticalDividerWidth * (numItemsForRow[row] - 1)) 327 / (float)numItemsForRow[row]; 328 329 for (int itemPosOnRow = 0; itemPosOnRow < numItemsForRow[row]; itemPosOnRow++) { 330 // Tell the child to be exactly this size 331 child = getChildAt(itemPos); 332 child.measure(MeasureSpec.makeMeasureSpec((int) itemWidth, MeasureSpec.EXACTLY), 333 MeasureSpec.makeMeasureSpec((int) itemHeight, MeasureSpec.EXACTLY)); 334 335 // Remember the child's position for layout 336 childLayoutParams = (IconMenuView.LayoutParams) child.getLayoutParams(); 337 childLayoutParams.left = (int) itemLeft; 338 childLayoutParams.right = (int) (itemLeft + itemWidth); 339 childLayoutParams.top = (int) itemTop; 340 childLayoutParams.bottom = (int) (itemTop + itemHeight); 341 342 // Increment by item width 343 itemLeft += itemWidth; 344 itemPos++; 345 346 // Add a vertical divider to draw 347 if (mVerticalDivider != null) { 348 mVerticalDividerRects.add(new Rect((int) itemLeft, 349 (int) itemTop, (int) (itemLeft + mVerticalDividerWidth), 350 (int) (itemTop + itemHeight))); 351 } 352 353 // Increment by divider width (even if we're not computing 354 // dividers, since we need to leave room for them when 355 // calculating item positions) 356 itemLeft += mVerticalDividerWidth; 357 } 358 359 // Last child on each row should extend to very right edge 360 if (childLayoutParams != null) { 361 childLayoutParams.right = menuWidth; 362 } 363 364 itemTop += itemHeight; 365 366 // Add a horizontal divider to draw 367 if ((mHorizontalDivider != null) && (row < numRowsMinus1)) { 368 mHorizontalDividerRects.add(new Rect(0, (int) itemTop, menuWidth, 369 (int) (itemTop + mHorizontalDividerHeight))); 370 371 itemTop += mHorizontalDividerHeight; 372 } 373 } 374 } 375 376 @Override 377 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 378 if (mHasStaleChildren) { 379 mHasStaleChildren = false; 380 381 // If we have stale data, resync with the menu 382 updateChildren(false); 383 } 384 385 // Get the desired height of the icon menu view (last row of items does 386 // not have a divider below) 387 final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) * calculateNumberOfRows() 388 - mHorizontalDividerHeight; 389 390 // Maximum possible width and desired height 391 setMeasuredDimension(resolveSize(Integer.MAX_VALUE, widthMeasureSpec), 392 resolveSize(desiredHeight, heightMeasureSpec)); 393 394 // Position the children 395 positionChildren(mMeasuredWidth, mMeasuredHeight); 396 } 397 398 399 @Override 400 protected void onLayout(boolean changed, int l, int t, int r, int b) { 401 View child; 402 IconMenuView.LayoutParams childLayoutParams; 403 404 for (int i = getChildCount() - 1; i >= 0; i--) { 405 child = getChildAt(i); 406 childLayoutParams = (IconMenuView.LayoutParams)child 407 .getLayoutParams(); 408 409 // Layout children according to positions set during the measure 410 child.layout(childLayoutParams.left, childLayoutParams.top, childLayoutParams.right, 411 childLayoutParams.bottom); 412 } 413 } 414 415 @Override 416 protected void onDraw(Canvas canvas) { 417 if (mHorizontalDivider != null) { 418 // If we have a horizontal divider to draw, draw it at the remembered positions 419 for (int i = mHorizontalDividerRects.size() - 1; i >= 0; i--) { 420 mHorizontalDivider.setBounds(mHorizontalDividerRects.get(i)); 421 mHorizontalDivider.draw(canvas); 422 } 423 } 424 425 if (mVerticalDivider != null) { 426 // If we have a vertical divider to draw, draw it at the remembered positions 427 for (int i = mVerticalDividerRects.size() - 1; i >= 0; i--) { 428 mVerticalDivider.setBounds(mVerticalDividerRects.get(i)); 429 mVerticalDivider.draw(canvas); 430 } 431 } 432 } 433 434 public boolean invokeItem(MenuItemImpl item) { 435 return mMenu.performItemAction(item, 0); 436 } 437 438 @Override 439 public LayoutParams generateLayoutParams(AttributeSet attrs) 440 { 441 return new IconMenuView.LayoutParams(getContext(), attrs); 442 } 443 444 @Override 445 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) 446 { 447 // Override to allow type-checking of LayoutParams. 448 return p instanceof IconMenuView.LayoutParams; 449 } 450 451 /** 452 * Marks as having stale children. 453 */ 454 void markStaleChildren() { 455 if (!mHasStaleChildren) { 456 mHasStaleChildren = true; 457 requestLayout(); 458 } 459 } 460 461 /** 462 * @return The number of actual items shown (those that are backed by an 463 * {@link MenuView.ItemView} implementation--eg: excludes More 464 * item). 465 */ 466 int getNumActualItemsShown() { 467 return mNumActualItemsShown; 468 } 469 470 471 public int getWindowAnimations() { 472 return mAnimations; 473 } 474 475 @Override 476 public boolean dispatchKeyEvent(KeyEvent event) { 477 478 if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) { 479 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 480 removeCallbacks(this); 481 postDelayed(this, ViewConfiguration.getLongPressTimeout()); 482 } else if (event.getAction() == KeyEvent.ACTION_UP) { 483 484 if (mMenuBeingLongpressed) { 485 // It was in cycle mode, so reset it (will also remove us 486 // from being called back) 487 setCycleShortcutCaptionMode(false); 488 return true; 489 490 } else { 491 // Just remove us from being called back 492 removeCallbacks(this); 493 // Fall through to normal processing too 494 } 495 } 496 } 497 498 return super.dispatchKeyEvent(event); 499 } 500 501 @Override 502 protected void onDetachedFromWindow() { 503 setCycleShortcutCaptionMode(false); 504 super.onDetachedFromWindow(); 505 } 506 507 @Override 508 public void onWindowFocusChanged(boolean hasWindowFocus) { 509 510 if (!hasWindowFocus) { 511 setCycleShortcutCaptionMode(false); 512 } 513 514 super.onWindowFocusChanged(hasWindowFocus); 515 } 516 517 /** 518 * Sets the shortcut caption mode for IconMenuView. This mode will 519 * continuously cycle between a child's shortcut and its title. 520 * 521 * @param cycleShortcutAndNormal Whether to go into cycling shortcut mode, 522 * or to go back to normal. 523 */ 524 private void setCycleShortcutCaptionMode(boolean cycleShortcutAndNormal) { 525 526 if (!cycleShortcutAndNormal) { 527 /* 528 * We're setting back to title, so remove any callbacks for setting 529 * to shortcut 530 */ 531 removeCallbacks(this); 532 setChildrenCaptionMode(false); 533 mMenuBeingLongpressed = false; 534 535 } else { 536 537 // Set it the first time (the cycle will be started in run()). 538 setChildrenCaptionMode(true); 539 } 540 541 } 542 543 /** 544 * When this method is invoked if the menu is currently not being 545 * longpressed, it means that the longpress has just been reached (so we set 546 * longpress flag, and start cycling). If it is being longpressed, we cycle 547 * to the next mode. 548 */ 549 public void run() { 550 551 if (mMenuBeingLongpressed) { 552 553 // Cycle to other caption mode on the children 554 setChildrenCaptionMode(!mLastChildrenCaptionMode); 555 556 } else { 557 558 // Switch ourselves to continuously cycle the items captions 559 mMenuBeingLongpressed = true; 560 setCycleShortcutCaptionMode(true); 561 } 562 563 // We should run again soon to cycle to the other caption mode 564 postDelayed(this, ITEM_CAPTION_CYCLE_DELAY); 565 } 566 567 /** 568 * Iterates children and sets the desired shortcut mode. Only 569 * {@link #setCycleShortcutCaptionMode(boolean)} and {@link #run()} should call 570 * this. 571 * 572 * @param shortcut Whether to show shortcut or the title. 573 */ 574 private void setChildrenCaptionMode(boolean shortcut) { 575 576 // Set the last caption mode pushed to children 577 mLastChildrenCaptionMode = shortcut; 578 579 for (int i = getChildCount() - 1; i >= 0; i--) { 580 ((IconMenuItemView) getChildAt(i)).setCaptionMode(shortcut); 581 } 582 } 583 584 @Override 585 protected Parcelable onSaveInstanceState() { 586 Parcelable superState = super.onSaveInstanceState(); 587 588 View focusedView = getFocusedChild(); 589 590 for (int i = getChildCount() - 1; i >= 0; i--) { 591 if (getChildAt(i) == focusedView) { 592 return new SavedState(superState, i); 593 } 594 } 595 596 return new SavedState(superState, -1); 597 } 598 599 @Override 600 protected void onRestoreInstanceState(Parcelable state) { 601 SavedState ss = (SavedState) state; 602 super.onRestoreInstanceState(ss.getSuperState()); 603 604 if (ss.focusedPosition >= getChildCount()) { 605 return; 606 } 607 608 View v = getChildAt(ss.focusedPosition); 609 if (v != null) { 610 v.requestFocus(); 611 } 612 } 613 614 private static class SavedState extends BaseSavedState { 615 int focusedPosition; 616 617 /** 618 * Constructor called from {@link IconMenuView#onSaveInstanceState()} 619 */ 620 public SavedState(Parcelable superState, int focusedPosition) { 621 super(superState); 622 this.focusedPosition = focusedPosition; 623 } 624 625 /** 626 * Constructor called from {@link #CREATOR} 627 */ 628 private SavedState(Parcel in) { 629 super(in); 630 focusedPosition = in.readInt(); 631 } 632 633 @Override 634 public void writeToParcel(Parcel dest, int flags) { 635 super.writeToParcel(dest, flags); 636 dest.writeInt(focusedPosition); 637 } 638 639 public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { 640 public SavedState createFromParcel(Parcel in) { 641 return new SavedState(in); 642 } 643 644 public SavedState[] newArray(int size) { 645 return new SavedState[size]; 646 } 647 }; 648 649 } 650 651 /** 652 * Layout parameters specific to IconMenuView (stores the left, top, right, bottom from the 653 * measure pass). 654 */ 655 public static class LayoutParams extends ViewGroup.MarginLayoutParams 656 { 657 int left, top, right, bottom; 658 659 public LayoutParams(Context c, AttributeSet attrs) { 660 super(c, attrs); 661 } 662 663 public LayoutParams(int width, int height) { 664 super(width, height); 665 } 666 } 667} 668