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