• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.internal.view.menu;
18 
19 import com.android.internal.view.menu.MenuBuilder.ItemInvoker;
20 
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.util.AttributeSet;
30 import android.view.KeyEvent;
31 import android.view.View;
32 import android.view.ViewConfiguration;
33 import android.view.ViewGroup;
34 import android.view.LayoutInflater;
35 
36 import 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  */
51 public 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 to show in the icon menu. */
61     private int mMaxItems;
62     /** Maximum number of items per row */
63     private int mMaxItemsPerRow;
64     /** Actual number of items (the 'More' view does not count as an item) shown */
65     private int mNumActualItemsShown;
66 
67     /** Divider that is drawn between all rows */
68     private Drawable mHorizontalDivider;
69     /** Height of the horizontal divider */
70     private int mHorizontalDividerHeight;
71     /** Set of horizontal divider positions where the horizontal divider will be drawn */
72     private ArrayList<Rect> mHorizontalDividerRects;
73 
74     /** Divider that is drawn between all columns */
75     private Drawable mVerticalDivider;
76     /** Width of the vertical divider */
77     private int mVerticalDividerWidth;
78     /** Set of vertical divider positions where the vertical divider will be drawn */
79     private ArrayList<Rect> mVerticalDividerRects;
80 
81     /** Icon for the 'More' button */
82     private Drawable mMoreIcon;
83 
84     /** Item view for the 'More' button */
85     private IconMenuItemView mMoreItemView;
86 
87     /** Background of each item (should contain the selected and focused states) */
88     private Drawable mItemBackground;
89 
90     /** Default animations for this menu */
91     private int mAnimations;
92 
93     /**
94      * Whether this IconMenuView has stale children and needs to update them.
95      * Set true by {@link #markStaleChildren()} and reset to false by
96      * {@link #onMeasure(int, int)}
97      */
98     private boolean mHasStaleChildren;
99 
100     /**
101      * Longpress on MENU (while this is shown) switches to shortcut caption
102      * mode. When the user releases the longpress, we do not want to pass the
103      * key-up event up since that will dismiss the menu.
104      */
105     private boolean mMenuBeingLongpressed = false;
106 
107     /**
108      * While {@link #mMenuBeingLongpressed}, we toggle the children's caption
109      * mode between each's title and its shortcut. This is the last caption mode
110      * we broadcasted to children.
111      */
112     private boolean mLastChildrenCaptionMode;
113 
114     /**
115      * The layout to use for menu items. Each index is the row number (0 is the
116      * top-most). Each value contains the number of items in that row.
117      * <p>
118      * The length of this array should not be used to get the number of rows in
119      * the current layout, instead use {@link #mLayoutNumRows}.
120      */
121     private int[] mLayout;
122 
123     /**
124      * The number of rows in the current layout.
125      */
126     private int mLayoutNumRows;
127 
128     /**
129      * Instantiates the IconMenuView that is linked with the provided MenuBuilder.
130      */
IconMenuView(Context context, AttributeSet attrs)131     public IconMenuView(Context context, AttributeSet attrs) {
132         super(context, attrs);
133 
134         TypedArray a =
135             context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.IconMenuView, 0, 0);
136         mRowHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.IconMenuView_rowHeight, 64);
137         mMaxRows = a.getInt(com.android.internal.R.styleable.IconMenuView_maxRows, 2);
138         mMaxItems = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItems, 6);
139         mMaxItemsPerRow = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItemsPerRow, 3);
140         mMoreIcon = a.getDrawable(com.android.internal.R.styleable.IconMenuView_moreIcon);
141         a.recycle();
142 
143         a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuView, 0, 0);
144         mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
145         mHorizontalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider);
146         mHorizontalDividerRects = new ArrayList<Rect>();
147         mVerticalDivider =  a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider);
148         mVerticalDividerRects = new ArrayList<Rect>();
149         mAnimations = a.getResourceId(com.android.internal.R.styleable.MenuView_windowAnimationStyle, 0);
150         a.recycle();
151 
152         if (mHorizontalDivider != null) {
153             mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight();
154             // Make sure to have some height for the divider
155             if (mHorizontalDividerHeight == -1) mHorizontalDividerHeight = 1;
156         }
157 
158         if (mVerticalDivider != null) {
159             mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth();
160             // Make sure to have some width for the divider
161             if (mVerticalDividerWidth == -1) mVerticalDividerWidth = 1;
162         }
163 
164         mLayout = new int[mMaxRows];
165 
166         // This view will be drawing the dividers
167         setWillNotDraw(false);
168 
169         // This is so we'll receive the MENU key in touch mode
170         setFocusableInTouchMode(true);
171         // This is so our children can still be arrow-key focused
172         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
173     }
174 
175     /**
176      * Figures out the layout for the menu items.
177      *
178      * @param width The available width for the icon menu.
179      */
layoutItems(int width)180     private void layoutItems(int width) {
181         int numItems = getChildCount();
182 
183         // Start with the least possible number of rows
184         int curNumRows =
185                 Math.min((int) Math.ceil(numItems / (float) mMaxItemsPerRow), mMaxRows);
186 
187         /*
188          * Increase the number of rows until we find a configuration that fits
189          * all of the items' titles. Worst case, we use mMaxRows.
190          */
191         for (; curNumRows <= mMaxRows; curNumRows++) {
192             layoutItemsUsingGravity(curNumRows, numItems);
193 
194             if (curNumRows >= numItems) {
195                 // Can't have more rows than items
196                 break;
197             }
198 
199             if (doItemsFit()) {
200                 // All the items fit, so this is a good configuration
201                 break;
202             }
203         }
204     }
205 
206     /**
207      * Figures out the layout for the menu items by equally distributing, and
208      * adding any excess items equally to lower rows.
209      *
210      * @param numRows The total number of rows for the menu view
211      * @param numItems The total number of items (across all rows) contained in
212      *            the menu view
213      * @return int[] Where the value of index i contains the number of items for row i
214      */
layoutItemsUsingGravity(int numRows, int numItems)215     private void layoutItemsUsingGravity(int numRows, int numItems) {
216         int numBaseItemsPerRow = numItems / numRows;
217         int numLeftoverItems = numItems % numRows;
218         /**
219          * The bottom rows will each get a leftover item. Rows (indexed at 0)
220          * that are >= this get a leftover item. Note: if there are 0 leftover
221          * items, no rows will get them since this value will be greater than
222          * the last row.
223          */
224         int rowsThatGetALeftoverItem = numRows - numLeftoverItems;
225 
226         int[] layout = mLayout;
227         for (int i = 0; i < numRows; i++) {
228             layout[i] = numBaseItemsPerRow;
229 
230             // Fill the bottom rows with a leftover item each
231             if (i >= rowsThatGetALeftoverItem) {
232                 layout[i]++;
233             }
234         }
235 
236         mLayoutNumRows = numRows;
237     }
238 
239     /**
240      * Checks whether each item's title is fully visible using the current
241      * layout.
242      *
243      * @return True if the items fit (each item's text is fully visible), false
244      *         otherwise.
245      */
doItemsFit()246     private boolean doItemsFit() {
247         int itemPos = 0;
248 
249         int[] layout = mLayout;
250         int numRows = mLayoutNumRows;
251         for (int row = 0; row < numRows; row++) {
252             int numItemsOnRow = layout[row];
253 
254             /*
255              * If there is only one item on this row, increasing the
256              * number of rows won't help.
257              */
258             if (numItemsOnRow == 1) {
259                 itemPos++;
260                 continue;
261             }
262 
263             for (int itemsOnRowCounter = numItemsOnRow; itemsOnRowCounter > 0;
264                     itemsOnRowCounter--) {
265                 View child = getChildAt(itemPos++);
266                 LayoutParams lp = (LayoutParams) child.getLayoutParams();
267                 if (lp.maxNumItemsOnRow < numItemsOnRow) {
268                     return false;
269                 }
270             }
271         }
272 
273         return true;
274     }
275 
276     /**
277      * Adds an IconMenuItemView to this icon menu view.
278      * @param itemView The item's view to add
279      */
addItemView(IconMenuItemView itemView)280     private void addItemView(IconMenuItemView itemView) {
281         // Set ourselves on the item view
282         itemView.setIconMenuView(this);
283 
284         // Apply the background to the item view
285         itemView.setBackgroundDrawable(
286                 mItemBackground.getConstantState().newDrawable(
287                         getContext().getResources()));
288 
289         // This class is the invoker for all its item views
290         itemView.setItemInvoker(this);
291 
292         addView(itemView, itemView.getTextAppropriateLayoutParams());
293     }
294 
295     /**
296      * Creates the item view for the 'More' button which is used to switch to
297      * the expanded menu view. This button is a special case since it does not
298      * have a MenuItemData backing it.
299      * @return The IconMenuItemView for the 'More' button
300      */
createMoreItemView()301     private IconMenuItemView createMoreItemView() {
302         LayoutInflater inflater = mMenu.getMenuType(MenuBuilder.TYPE_ICON).getInflater();
303 
304         final IconMenuItemView itemView = (IconMenuItemView) inflater.inflate(
305                 com.android.internal.R.layout.icon_menu_item_layout, null);
306 
307         Resources r = getContext().getResources();
308         itemView.initialize(r.getText(com.android.internal.R.string.more_item_label), mMoreIcon);
309 
310         // Set up a click listener on the view since there will be no invocation sequence
311         // due to the lack of a MenuItemData this view
312         itemView.setOnClickListener(new OnClickListener() {
313             public void onClick(View v) {
314                 // Switches the menu to expanded mode
315                 MenuBuilder.Callback cb = mMenu.getCallback();
316                 if (cb != null) {
317                     // Call callback
318                     cb.onMenuModeChange(mMenu);
319                 }
320             }
321         });
322 
323         return itemView;
324     }
325 
326 
initialize(MenuBuilder menu, int menuType)327     public void initialize(MenuBuilder menu, int menuType) {
328         mMenu = menu;
329         updateChildren(true);
330     }
331 
updateChildren(boolean cleared)332     public void updateChildren(boolean cleared) {
333         // This method does a clear refresh of children
334         removeAllViews();
335 
336         final ArrayList<MenuItemImpl> itemsToShow = mMenu.getVisibleItems();
337         final int numItems = itemsToShow.size();
338         final int numItemsThatCanFit = mMaxItems;
339         // Minimum of the num that can fit and the num that we have
340         final int minFitMinus1AndNumItems = Math.min(numItemsThatCanFit - 1, numItems);
341 
342         MenuItemImpl itemData;
343         // Traverse through all but the last item that can fit since that last item can either
344         // be a 'More' button or a sixth item
345         for (int i = 0; i < minFitMinus1AndNumItems; i++) {
346             itemData = itemsToShow.get(i);
347             addItemView((IconMenuItemView) itemData.getItemView(MenuBuilder.TYPE_ICON, this));
348         }
349 
350         if (numItems > numItemsThatCanFit) {
351             // If there are more items than we can fit, show the 'More' button to
352             // switch to expanded mode
353             if (mMoreItemView == null) {
354                 mMoreItemView = createMoreItemView();
355             }
356 
357             addItemView(mMoreItemView);
358 
359             // The last view is the more button, so the actual number of items is one less than
360             // the number that can fit
361             mNumActualItemsShown = numItemsThatCanFit - 1;
362         } else if (numItems == numItemsThatCanFit) {
363             // There are exactly the number we can show, so show the last item
364             final MenuItemImpl lastItemData = itemsToShow.get(numItemsThatCanFit - 1);
365             addItemView((IconMenuItemView) lastItemData.getItemView(MenuBuilder.TYPE_ICON, this));
366 
367             // The items shown fit exactly
368             mNumActualItemsShown = numItemsThatCanFit;
369         }
370     }
371 
372     /**
373      * The positioning algorithm that gets called from onMeasure.  It
374      * just computes positions for each child, and then stores them in the child's layout params.
375      * @param menuWidth The width of this menu to assume for positioning
376      * @param menuHeight The height of this menu to assume for positioning
377      */
positionChildren(int menuWidth, int menuHeight)378     private void positionChildren(int menuWidth, int menuHeight) {
379         // Clear the containers for the positions where the dividers should be drawn
380         if (mHorizontalDivider != null) mHorizontalDividerRects.clear();
381         if (mVerticalDivider != null) mVerticalDividerRects.clear();
382 
383         // Get the minimum number of rows needed
384         final int numRows = mLayoutNumRows;
385         final int numRowsMinus1 = numRows - 1;
386         final int numItemsForRow[] = mLayout;
387 
388         // The item position across all rows
389         int itemPos = 0;
390         View child;
391         IconMenuView.LayoutParams childLayoutParams = null;
392 
393         // Use float for this to get precise positions (uniform item widths
394         // instead of last one taking any slack), and then convert to ints at last opportunity
395         float itemLeft;
396         float itemTop = 0;
397         // Since each row can have a different number of items, this will be computed per row
398         float itemWidth;
399         // Subtract the space needed for the horizontal dividers
400         final float itemHeight = (menuHeight - mHorizontalDividerHeight * (numRows - 1))
401                 / (float)numRows;
402 
403         for (int row = 0; row < numRows; row++) {
404             // Start at the left
405             itemLeft = 0;
406 
407             // Subtract the space needed for the vertical dividers, and divide by the number of items
408             itemWidth = (menuWidth - mVerticalDividerWidth * (numItemsForRow[row] - 1))
409                     / (float)numItemsForRow[row];
410 
411             for (int itemPosOnRow = 0; itemPosOnRow < numItemsForRow[row]; itemPosOnRow++) {
412                 // Tell the child to be exactly this size
413                 child = getChildAt(itemPos);
414                 child.measure(MeasureSpec.makeMeasureSpec((int) itemWidth, MeasureSpec.EXACTLY),
415                         MeasureSpec.makeMeasureSpec((int) itemHeight, MeasureSpec.EXACTLY));
416 
417                 // Remember the child's position for layout
418                 childLayoutParams = (IconMenuView.LayoutParams) child.getLayoutParams();
419                 childLayoutParams.left = (int) itemLeft;
420                 childLayoutParams.right = (int) (itemLeft + itemWidth);
421                 childLayoutParams.top = (int) itemTop;
422                 childLayoutParams.bottom = (int) (itemTop + itemHeight);
423 
424                 // Increment by item width
425                 itemLeft += itemWidth;
426                 itemPos++;
427 
428                 // Add a vertical divider to draw
429                 if (mVerticalDivider != null) {
430                     mVerticalDividerRects.add(new Rect((int) itemLeft,
431                             (int) itemTop, (int) (itemLeft + mVerticalDividerWidth),
432                             (int) (itemTop + itemHeight)));
433                 }
434 
435                 // Increment by divider width (even if we're not computing
436                 // dividers, since we need to leave room for them when
437                 // calculating item positions)
438                 itemLeft += mVerticalDividerWidth;
439             }
440 
441             // Last child on each row should extend to very right edge
442             if (childLayoutParams != null) {
443                 childLayoutParams.right = menuWidth;
444             }
445 
446             itemTop += itemHeight;
447 
448             // Add a horizontal divider to draw
449             if ((mHorizontalDivider != null) && (row < numRowsMinus1)) {
450                 mHorizontalDividerRects.add(new Rect(0, (int) itemTop, menuWidth,
451                         (int) (itemTop + mHorizontalDividerHeight)));
452 
453                 itemTop += mHorizontalDividerHeight;
454             }
455         }
456     }
457 
458     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)459     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
460         if (mHasStaleChildren) {
461             mHasStaleChildren = false;
462 
463             // If we have stale data, resync with the menu
464             updateChildren(false);
465         }
466 
467         int measuredWidth = resolveSize(Integer.MAX_VALUE, widthMeasureSpec);
468         calculateItemFittingMetadata(measuredWidth);
469         layoutItems(measuredWidth);
470 
471         // Get the desired height of the icon menu view (last row of items does
472         // not have a divider below)
473         final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) *
474                 mLayoutNumRows - mHorizontalDividerHeight;
475 
476         // Maximum possible width and desired height
477         setMeasuredDimension(measuredWidth,
478                 resolveSize(desiredHeight, heightMeasureSpec));
479 
480         // Position the children
481         positionChildren(mMeasuredWidth, mMeasuredHeight);
482     }
483 
484 
485     @Override
onLayout(boolean changed, int l, int t, int r, int b)486     protected void onLayout(boolean changed, int l, int t, int r, int b) {
487         View child;
488         IconMenuView.LayoutParams childLayoutParams;
489 
490         for (int i = getChildCount() - 1; i >= 0; i--) {
491             child = getChildAt(i);
492             childLayoutParams = (IconMenuView.LayoutParams)child
493                     .getLayoutParams();
494 
495             // Layout children according to positions set during the measure
496             child.layout(childLayoutParams.left, childLayoutParams.top, childLayoutParams.right,
497                     childLayoutParams.bottom);
498         }
499     }
500 
501     @Override
onDraw(Canvas canvas)502     protected void onDraw(Canvas canvas) {
503         Drawable drawable = mHorizontalDivider;
504         if (drawable != null) {
505             // If we have a horizontal divider to draw, draw it at the remembered positions
506             final ArrayList<Rect> rects = mHorizontalDividerRects;
507             for (int i = rects.size() - 1; i >= 0; i--) {
508                 drawable.setBounds(rects.get(i));
509                 drawable.draw(canvas);
510             }
511         }
512 
513         drawable = mVerticalDivider;
514         if (drawable != null) {
515             // If we have a vertical divider to draw, draw it at the remembered positions
516             final ArrayList<Rect> rects = mVerticalDividerRects;
517             for (int i = rects.size() - 1; i >= 0; i--) {
518                 drawable.setBounds(rects.get(i));
519                 drawable.draw(canvas);
520             }
521         }
522     }
523 
invokeItem(MenuItemImpl item)524     public boolean invokeItem(MenuItemImpl item) {
525         return mMenu.performItemAction(item, 0);
526     }
527 
528     @Override
generateLayoutParams(AttributeSet attrs)529     public LayoutParams generateLayoutParams(AttributeSet attrs) {
530         return new IconMenuView.LayoutParams(getContext(), attrs);
531     }
532 
533     @Override
checkLayoutParams(ViewGroup.LayoutParams p)534     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
535         // Override to allow type-checking of LayoutParams.
536         return p instanceof IconMenuView.LayoutParams;
537     }
538 
539     /**
540      * Marks as having stale children.
541      */
markStaleChildren()542     void markStaleChildren() {
543         if (!mHasStaleChildren) {
544             mHasStaleChildren = true;
545             requestLayout();
546         }
547     }
548 
549     /**
550      * @return The number of actual items shown (those that are backed by an
551      *         {@link MenuView.ItemView} implementation--eg: excludes More
552      *         item).
553      */
getNumActualItemsShown()554     int getNumActualItemsShown() {
555         return mNumActualItemsShown;
556     }
557 
558 
getWindowAnimations()559     public int getWindowAnimations() {
560         return mAnimations;
561     }
562 
563     /**
564      * Returns the number of items per row.
565      * <p>
566      * This should only be used for testing.
567      *
568      * @return The length of the array is the number of rows. A value at a
569      *         position is the number of items in that row.
570      * @hide
571      */
getLayout()572     public int[] getLayout() {
573         return mLayout;
574     }
575 
576     /**
577      * Returns the number of rows in the layout.
578      * <p>
579      * This should only be used for testing.
580      *
581      * @return The length of the array is the number of rows. A value at a
582      *         position is the number of items in that row.
583      * @hide
584      */
getLayoutNumRows()585     public int getLayoutNumRows() {
586         return mLayoutNumRows;
587     }
588 
589     @Override
dispatchKeyEvent(KeyEvent event)590     public boolean dispatchKeyEvent(KeyEvent event) {
591 
592         if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
593             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
594                 removeCallbacks(this);
595                 postDelayed(this, ViewConfiguration.getLongPressTimeout());
596             } else if (event.getAction() == KeyEvent.ACTION_UP) {
597 
598                 if (mMenuBeingLongpressed) {
599                     // It was in cycle mode, so reset it (will also remove us
600                     // from being called back)
601                     setCycleShortcutCaptionMode(false);
602                     return true;
603 
604                 } else {
605                     // Just remove us from being called back
606                     removeCallbacks(this);
607                     // Fall through to normal processing too
608                 }
609             }
610         }
611 
612         return super.dispatchKeyEvent(event);
613     }
614 
615     @Override
onAttachedToWindow()616     protected void onAttachedToWindow() {
617         super.onAttachedToWindow();
618 
619         requestFocus();
620     }
621 
622     @Override
onDetachedFromWindow()623     protected void onDetachedFromWindow() {
624         setCycleShortcutCaptionMode(false);
625         super.onDetachedFromWindow();
626     }
627 
628     @Override
onWindowFocusChanged(boolean hasWindowFocus)629     public void onWindowFocusChanged(boolean hasWindowFocus) {
630 
631         if (!hasWindowFocus) {
632             setCycleShortcutCaptionMode(false);
633         }
634 
635         super.onWindowFocusChanged(hasWindowFocus);
636     }
637 
638     /**
639      * Sets the shortcut caption mode for IconMenuView. This mode will
640      * continuously cycle between a child's shortcut and its title.
641      *
642      * @param cycleShortcutAndNormal Whether to go into cycling shortcut mode,
643      *        or to go back to normal.
644      */
setCycleShortcutCaptionMode(boolean cycleShortcutAndNormal)645     private void setCycleShortcutCaptionMode(boolean cycleShortcutAndNormal) {
646 
647         if (!cycleShortcutAndNormal) {
648             /*
649              * We're setting back to title, so remove any callbacks for setting
650              * to shortcut
651              */
652             removeCallbacks(this);
653             setChildrenCaptionMode(false);
654             mMenuBeingLongpressed = false;
655 
656         } else {
657 
658             // Set it the first time (the cycle will be started in run()).
659             setChildrenCaptionMode(true);
660         }
661 
662     }
663 
664     /**
665      * When this method is invoked if the menu is currently not being
666      * longpressed, it means that the longpress has just been reached (so we set
667      * longpress flag, and start cycling). If it is being longpressed, we cycle
668      * to the next mode.
669      */
run()670     public void run() {
671 
672         if (mMenuBeingLongpressed) {
673 
674             // Cycle to other caption mode on the children
675             setChildrenCaptionMode(!mLastChildrenCaptionMode);
676 
677         } else {
678 
679             // Switch ourselves to continuously cycle the items captions
680             mMenuBeingLongpressed = true;
681             setCycleShortcutCaptionMode(true);
682         }
683 
684         // We should run again soon to cycle to the other caption mode
685         postDelayed(this, ITEM_CAPTION_CYCLE_DELAY);
686     }
687 
688     /**
689      * Iterates children and sets the desired shortcut mode. Only
690      * {@link #setCycleShortcutCaptionMode(boolean)} and {@link #run()} should call
691      * this.
692      *
693      * @param shortcut Whether to show shortcut or the title.
694      */
setChildrenCaptionMode(boolean shortcut)695     private void setChildrenCaptionMode(boolean shortcut) {
696 
697         // Set the last caption mode pushed to children
698         mLastChildrenCaptionMode = shortcut;
699 
700         for (int i = getChildCount() - 1; i >= 0; i--) {
701             ((IconMenuItemView) getChildAt(i)).setCaptionMode(shortcut);
702         }
703     }
704 
705     /**
706      * For each item, calculates the most dense row that fully shows the item's
707      * title.
708      *
709      * @param width The available width of the icon menu.
710      */
calculateItemFittingMetadata(int width)711     private void calculateItemFittingMetadata(int width) {
712         int maxNumItemsPerRow = mMaxItemsPerRow;
713         int numItems = getChildCount();
714         for (int i = 0; i < numItems; i++) {
715             LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
716             // Start with 1, since that case does not get covered in the loop below
717             lp.maxNumItemsOnRow = 1;
718             for (int curNumItemsPerRow = maxNumItemsPerRow; curNumItemsPerRow > 0;
719                     curNumItemsPerRow--) {
720                 // Check whether this item can fit into a row containing curNumItemsPerRow
721                 if (lp.desiredWidth < width / curNumItemsPerRow) {
722                     // It can, mark this value as the most dense row it can fit into
723                     lp.maxNumItemsOnRow = curNumItemsPerRow;
724                     break;
725                 }
726             }
727         }
728     }
729 
730     @Override
onSaveInstanceState()731     protected Parcelable onSaveInstanceState() {
732         Parcelable superState = super.onSaveInstanceState();
733 
734         View focusedView = getFocusedChild();
735 
736         for (int i = getChildCount() - 1; i >= 0; i--) {
737             if (getChildAt(i) == focusedView) {
738                 return new SavedState(superState, i);
739             }
740         }
741 
742         return new SavedState(superState, -1);
743     }
744 
745     @Override
onRestoreInstanceState(Parcelable state)746     protected void onRestoreInstanceState(Parcelable state) {
747         SavedState ss = (SavedState) state;
748         super.onRestoreInstanceState(ss.getSuperState());
749 
750         if (ss.focusedPosition >= getChildCount()) {
751             return;
752         }
753 
754         View v = getChildAt(ss.focusedPosition);
755         if (v != null) {
756             v.requestFocus();
757         }
758     }
759 
760     private static class SavedState extends BaseSavedState {
761         int focusedPosition;
762 
763         /**
764          * Constructor called from {@link IconMenuView#onSaveInstanceState()}
765          */
SavedState(Parcelable superState, int focusedPosition)766         public SavedState(Parcelable superState, int focusedPosition) {
767             super(superState);
768             this.focusedPosition = focusedPosition;
769         }
770 
771         /**
772          * Constructor called from {@link #CREATOR}
773          */
SavedState(Parcel in)774         private SavedState(Parcel in) {
775             super(in);
776             focusedPosition = in.readInt();
777         }
778 
779         @Override
writeToParcel(Parcel dest, int flags)780         public void writeToParcel(Parcel dest, int flags) {
781             super.writeToParcel(dest, flags);
782             dest.writeInt(focusedPosition);
783         }
784 
785         public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
786             public SavedState createFromParcel(Parcel in) {
787                 return new SavedState(in);
788             }
789 
790             public SavedState[] newArray(int size) {
791                 return new SavedState[size];
792             }
793         };
794 
795     }
796 
797     /**
798      * Layout parameters specific to IconMenuView (stores the left, top, right, bottom from the
799      * measure pass).
800      */
801     public static class LayoutParams extends ViewGroup.MarginLayoutParams
802     {
803         int left, top, right, bottom;
804         int desiredWidth;
805         int maxNumItemsOnRow;
806 
LayoutParams(Context c, AttributeSet attrs)807         public LayoutParams(Context c, AttributeSet attrs) {
808             super(c, attrs);
809         }
810 
LayoutParams(int width, int height)811         public LayoutParams(int width, int height) {
812             super(width, height);
813         }
814     }
815 }
816