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