IconMenuView.java revision 9db3d07b9620b4269ab33f78604a36327e536ce1
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 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     */
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     */
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     */
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     */
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     */
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     */
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
327    public void initialize(MenuBuilder menu, int menuType) {
328        mMenu = menu;
329        updateChildren(true);
330    }
331
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     */
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
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
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
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
524    public boolean invokeItem(MenuItemImpl item) {
525        return mMenu.performItemAction(item, 0);
526    }
527
528    @Override
529    public LayoutParams generateLayoutParams(AttributeSet attrs) {
530        return new IconMenuView.LayoutParams(getContext(), attrs);
531    }
532
533    @Override
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     */
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     */
554    int getNumActualItemsShown() {
555        return mNumActualItemsShown;
556    }
557
558
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     */
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     */
585    public int getLayoutNumRows() {
586        return mLayoutNumRows;
587    }
588
589    @Override
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
616    protected void onAttachedToWindow() {
617        super.onAttachedToWindow();
618
619        requestFocus();
620    }
621
622    @Override
623    protected void onDetachedFromWindow() {
624        setCycleShortcutCaptionMode(false);
625        super.onDetachedFromWindow();
626    }
627
628    @Override
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     */
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     */
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     */
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     */
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
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
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         */
766        public SavedState(Parcelable superState, int focusedPosition) {
767            super(superState);
768            this.focusedPosition = focusedPosition;
769        }
770
771        /**
772         * Constructor called from {@link #CREATOR}
773         */
774        private SavedState(Parcel in) {
775            super(in);
776            focusedPosition = in.readInt();
777        }
778
779        @Override
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
807        public LayoutParams(Context c, AttributeSet attrs) {
808            super(c, attrs);
809        }
810
811        public LayoutParams(int width, int height) {
812            super(width, height);
813        }
814    }
815}
816