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