FileDocCategorySizeDatePackage
IconMenuView.javaAPI DocAndroid 1.5 API29752Wed May 06 22:41:56 BST 2009com.android.internal.view.menu

IconMenuView

public final class IconMenuView extends android.view.ViewGroup implements Runnable, com.android.internal.view.menu.MenuBuilder.ItemInvoker, MenuView
The icon menu view is an icon-based menu usually with a subset of all the menu items. It is opened as the default menu, and shows either the first five or all six of the menu items with text and icon. In the situation of there being more than six items, the first five items will be accompanied with a 'More' button that opens an {@link ExpandedMenuView} which lists all the menu items.
attr
ref android.R.styleable#IconMenuView_rowHeight
attr
ref android.R.styleable#IconMenuView_maxRows
attr
ref android.R.styleable#IconMenuView_maxItemsPerRow
hide

Fields Summary
private static final int
ITEM_CAPTION_CYCLE_DELAY
private MenuBuilder
mMenu
private int
mRowHeight
Height of each row
private int
mMaxRows
Maximum number of rows to be shown
private int
mMaxItems
Maximum number of items to show in the icon menu.
private int
mMaxItemsPerRow
Maximum number of items per row
private int
mNumActualItemsShown
Actual number of items (the 'More' view does not count as an item) shown
private android.graphics.drawable.Drawable
mHorizontalDivider
Divider that is drawn between all rows
private int
mHorizontalDividerHeight
Height of the horizontal divider
private ArrayList
mHorizontalDividerRects
Set of horizontal divider positions where the horizontal divider will be drawn
private android.graphics.drawable.Drawable
mVerticalDivider
Divider that is drawn between all columns
private int
mVerticalDividerWidth
Width of the vertical divider
private ArrayList
mVerticalDividerRects
Set of vertical divider positions where the vertical divider will be drawn
private android.graphics.drawable.Drawable
mMoreIcon
Icon for the 'More' button
private IconMenuItemView
mMoreItemView
Item view for the 'More' button
private android.graphics.drawable.Drawable
mItemBackground
Background of each item (should contain the selected and focused states)
private int
mAnimations
Default animations for this menu
private boolean
mHasStaleChildren
Whether this IconMenuView has stale children and needs to update them. Set true by {@link #markStaleChildren()} and reset to false by {@link #onMeasure(int, int)}
private boolean
mMenuBeingLongpressed
Longpress on MENU (while this is shown) switches to shortcut caption mode. When the user releases the longpress, we do not want to pass the key-up event up since that will dismiss the menu.
private boolean
mLastChildrenCaptionMode
While {@link #mMenuBeingLongpressed}, we toggle the children's caption mode between each's title and its shortcut. This is the last caption mode we broadcasted to children.
private int[]
mLayout
The layout to use for menu items. Each index is the row number (0 is the top-most). Each value contains the number of items in that row.

The length of this array should not be used to get the number of rows in the current layout, instead use {@link #mLayoutNumRows}.

private int
mLayoutNumRows
The number of rows in the current layout.
Constructors Summary
public IconMenuView(android.content.Context context, android.util.AttributeSet attrs)
Instantiates the IconMenuView that is linked with the provided MenuBuilder.

    
                   
         
        super(context, attrs);

        TypedArray a = 
            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.IconMenuView, 0, 0);
        mRowHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.IconMenuView_rowHeight, 64);
        mMaxRows = a.getInt(com.android.internal.R.styleable.IconMenuView_maxRows, 2);
        mMaxItems = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItems, 6);
        mMaxItemsPerRow = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItemsPerRow, 3);
        mMoreIcon = a.getDrawable(com.android.internal.R.styleable.IconMenuView_moreIcon);
        a.recycle();
        
        a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuView, 0, 0);
        mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
        mHorizontalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider);
        mHorizontalDividerRects = new ArrayList<Rect>();
        mVerticalDivider =  a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider);
        mVerticalDividerRects = new ArrayList<Rect>();
        mAnimations = a.getResourceId(com.android.internal.R.styleable.MenuView_windowAnimationStyle, 0);
        a.recycle();
        
        if (mHorizontalDivider != null) {
            mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight();
            // Make sure to have some height for the divider
            if (mHorizontalDividerHeight == -1) mHorizontalDividerHeight = 1;
        }
        
        if (mVerticalDivider != null) {
            mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth();
            // Make sure to have some width for the divider
            if (mVerticalDividerWidth == -1) mVerticalDividerWidth = 1;
        }
        
        mLayout = new int[mMaxRows];
        
        // This view will be drawing the dividers        
        setWillNotDraw(false);
        
        // This is so we'll receive the MENU key in touch mode
        setFocusableInTouchMode(true);
        // This is so our children can still be arrow-key focused
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
    
Methods Summary
private voidaddItemView(IconMenuItemView itemView)
Adds an IconMenuItemView to this icon menu view.

param
itemView The item's view to add

   
        // Set ourselves on the item view
        itemView.setIconMenuView(this);
        
        // Apply the background to the item view
        itemView.setBackgroundDrawable(mItemBackground.getConstantState().newDrawable());

        // This class is the invoker for all its item views 
        itemView.setItemInvoker(this);
        
        addView(itemView, itemView.getTextAppropriateLayoutParams());
    
private voidcalculateItemFittingMetadata(int width)
For each item, calculates the most dense row that fully shows the item's title.

param
width The available width of the icon menu.

        int maxNumItemsPerRow = mMaxItemsPerRow;
        int numItems = getChildCount();
        for (int i = 0; i < numItems; i++) {
            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
            // Start with 1, since that case does not get covered in the loop below
            lp.maxNumItemsOnRow = 1;
            for (int curNumItemsPerRow = maxNumItemsPerRow; curNumItemsPerRow > 0;
                    curNumItemsPerRow--) {
                // Check whether this item can fit into a row containing curNumItemsPerRow
                if (lp.desiredWidth < width / curNumItemsPerRow) {
                    // It can, mark this value as the most dense row it can fit into
                    lp.maxNumItemsOnRow = curNumItemsPerRow;
                    break;
                }
            }
        }
    
protected booleancheckLayoutParams(ViewGroup.LayoutParams p)

        // Override to allow type-checking of LayoutParams. 
        return p instanceof IconMenuView.LayoutParams;
    
private IconMenuItemViewcreateMoreItemView()
Creates the item view for the 'More' button which is used to switch to the expanded menu view. This button is a special case since it does not have a MenuItemData backing it.

return
The IconMenuItemView for the 'More' button

        LayoutInflater inflater = mMenu.getMenuType(MenuBuilder.TYPE_ICON).getInflater();
        
        final IconMenuItemView itemView = (IconMenuItemView) inflater.inflate(
                com.android.internal.R.layout.icon_menu_item_layout, null);
        
        Resources r = getContext().getResources();
        itemView.initialize(r.getText(com.android.internal.R.string.more_item_label), mMoreIcon);
        
        // Set up a click listener on the view since there will be no invocation sequence
        // due to the lack of a MenuItemData this view
        itemView.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                // Switches the menu to expanded mode
                MenuBuilder.Callback cb = mMenu.getCallback();
                if (cb != null) {
                    // Call callback
                    cb.onMenuModeChange(mMenu);
                }
            }
        });
        
        return itemView;
    
public booleandispatchKeyEvent(android.view.KeyEvent event)


        if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
                removeCallbacks(this);
                postDelayed(this, ViewConfiguration.getLongPressTimeout());
            } else if (event.getAction() == KeyEvent.ACTION_UP) {
                
                if (mMenuBeingLongpressed) {
                    // It was in cycle mode, so reset it (will also remove us
                    // from being called back)
                    setCycleShortcutCaptionMode(false);
                    return true;
                    
                } else {
                    // Just remove us from being called back
                    removeCallbacks(this);
                    // Fall through to normal processing too
                }
            }
        }
        
        return super.dispatchKeyEvent(event);
    
private booleandoItemsFit()
Checks whether each item's title is fully visible using the current layout.

return
True if the items fit (each item's text is fully visible), false otherwise.

        int itemPos = 0;
        
        int[] layout = mLayout;
        int numRows = mLayoutNumRows;
        for (int row = 0; row < numRows; row++) {
            int numItemsOnRow = layout[row];

            /*
             * If there is only one item on this row, increasing the
             * number of rows won't help.
             */ 
            if (numItemsOnRow == 1) {
                itemPos++;
                continue;
            }
            
            for (int itemsOnRowCounter = numItemsOnRow; itemsOnRowCounter > 0;
                    itemsOnRowCounter--) {
                View child = getChildAt(itemPos++);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (lp.maxNumItemsOnRow < numItemsOnRow) {
                    return false;
                }
            }
        }
        
        return true;
    
public com.android.internal.view.menu.IconMenuView$LayoutParamsgenerateLayoutParams(android.util.AttributeSet attrs)

        return new IconMenuView.LayoutParams(getContext(), attrs);
    
public int[]getLayout()
Returns the number of items per row.

This should only be used for testing.

return
The length of the array is the number of rows. A value at a position is the number of items in that row.
hide

        return mLayout;
    
public intgetLayoutNumRows()
Returns the number of rows in the layout.

This should only be used for testing.

return
The length of the array is the number of rows. A value at a position is the number of items in that row.
hide

        return mLayoutNumRows;
    
intgetNumActualItemsShown()

return
The number of actual items shown (those that are backed by an {@link MenuView.ItemView} implementation--eg: excludes More item).

        return mNumActualItemsShown;
    
public intgetWindowAnimations()

        return mAnimations;
    
public voidinitialize(MenuBuilder menu, int menuType)

        mMenu = menu;
        updateChildren(true);
    
public booleaninvokeItem(MenuItemImpl item)

        return mMenu.performItemAction(item, 0);
    
private voidlayoutItems(int width)
Figures out the layout for the menu items.

param
width The available width for the icon menu.

        int numItems = getChildCount();
        
        // Start with the least possible number of rows
        int curNumRows =
                Math.min((int) Math.ceil(numItems / (float) mMaxItemsPerRow), mMaxRows);
        
        /*
         * Increase the number of rows until we find a configuration that fits
         * all of the items' titles. Worst case, we use mMaxRows.
         */
        for (; curNumRows <= mMaxRows; curNumRows++) {
            layoutItemsUsingGravity(curNumRows, numItems);
            
            if (curNumRows >= numItems) {
                // Can't have more rows than items
                break;
            }
            
            if (doItemsFit()) {
                // All the items fit, so this is a good configuration
                break;
            }
        }
    
private voidlayoutItemsUsingGravity(int numRows, int numItems)
Figures out the layout for the menu items by equally distributing, and adding any excess items equally to lower rows.

param
numRows The total number of rows for the menu view
param
numItems The total number of items (across all rows) contained in the menu view
return
int[] Where the value of index i contains the number of items for row i

        int numBaseItemsPerRow = numItems / numRows;
        int numLeftoverItems = numItems % numRows;
        /**
         * The bottom rows will each get a leftover item. Rows (indexed at 0)
         * that are >= this get a leftover item. Note: if there are 0 leftover
         * items, no rows will get them since this value will be greater than
         * the last row.
         */
        int rowsThatGetALeftoverItem = numRows - numLeftoverItems;
        
        int[] layout = mLayout;
        for (int i = 0; i < numRows; i++) {
            layout[i] = numBaseItemsPerRow;

            // Fill the bottom rows with a leftover item each
            if (i >= rowsThatGetALeftoverItem) {
                layout[i]++;
            }
        }
        
        mLayoutNumRows = numRows;
    
voidmarkStaleChildren()
Marks as having stale children.

        if (!mHasStaleChildren) {
            mHasStaleChildren = true;
            requestLayout();
        }
    
protected voidonAttachedToWindow()

        super.onAttachedToWindow();
        
        requestFocus();
    
protected voidonDetachedFromWindow()

        setCycleShortcutCaptionMode(false);
        super.onDetachedFromWindow();
    
protected voidonDraw(android.graphics.Canvas canvas)

        if (mHorizontalDivider != null) {
            // If we have a horizontal divider to draw, draw it at the remembered positions
            for (int i = mHorizontalDividerRects.size() - 1; i >= 0; i--) {
                mHorizontalDivider.setBounds(mHorizontalDividerRects.get(i));
                mHorizontalDivider.draw(canvas);
            }
        }
        
        if (mVerticalDivider != null) {
            // If we have a vertical divider to draw, draw it at the remembered positions
            for (int i = mVerticalDividerRects.size() - 1; i >= 0; i--) {
                mVerticalDivider.setBounds(mVerticalDividerRects.get(i));
                mVerticalDivider.draw(canvas);
            }
        }
    
protected voidonLayout(boolean changed, int l, int t, int r, int b)

        View child;
        IconMenuView.LayoutParams childLayoutParams;
        
        for (int i = getChildCount() - 1; i >= 0; i--) {
            child = getChildAt(i);
            childLayoutParams = (IconMenuView.LayoutParams)child
                    .getLayoutParams();

            // Layout children according to positions set during the measure
            child.layout(childLayoutParams.left, childLayoutParams.top, childLayoutParams.right,
                    childLayoutParams.bottom);
        }
    
protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

        if (mHasStaleChildren) {
            mHasStaleChildren = false;

            // If we have stale data, resync with the menu
            updateChildren(false);
        }
        
        int measuredWidth = resolveSize(Integer.MAX_VALUE, widthMeasureSpec);
        calculateItemFittingMetadata(measuredWidth);
        layoutItems(measuredWidth);
        
        // Get the desired height of the icon menu view (last row of items does
        // not have a divider below)
        final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) *
                mLayoutNumRows - mHorizontalDividerHeight;
        
        // Maximum possible width and desired height
        setMeasuredDimension(measuredWidth,
                resolveSize(desiredHeight, heightMeasureSpec));

        // Position the children
        positionChildren(mMeasuredWidth, mMeasuredHeight);
    
protected voidonRestoreInstanceState(android.os.Parcelable state)

        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        if (ss.focusedPosition >= getChildCount()) {
            return;
        }
        
        View v = getChildAt(ss.focusedPosition);
        if (v != null) {
            v.requestFocus();
        }
    
protected android.os.ParcelableonSaveInstanceState()

        Parcelable superState = super.onSaveInstanceState();
        
        View focusedView = getFocusedChild();
        
        for (int i = getChildCount() - 1; i >= 0; i--) {
            if (getChildAt(i) == focusedView) {
                return new SavedState(superState, i);
            }
        }
        
        return new SavedState(superState, -1);
    
public voidonWindowFocusChanged(boolean hasWindowFocus)


        if (!hasWindowFocus) {
            setCycleShortcutCaptionMode(false);
        }

        super.onWindowFocusChanged(hasWindowFocus);
    
private voidpositionChildren(int menuWidth, int menuHeight)
The positioning algorithm that gets called from onMeasure. It just computes positions for each child, and then stores them in the child's layout params.

param
menuWidth The width of this menu to assume for positioning
param
menuHeight The height of this menu to assume for positioning

        // Clear the containers for the positions where the dividers should be drawn
        if (mHorizontalDivider != null) mHorizontalDividerRects.clear();
        if (mVerticalDivider != null) mVerticalDividerRects.clear();

        // Get the minimum number of rows needed
        final int numRows = mLayoutNumRows;
        final int numRowsMinus1 = numRows - 1;
        final int numItemsForRow[] = mLayout;
        
        // The item position across all rows
        int itemPos = 0;
        View child;
        IconMenuView.LayoutParams childLayoutParams = null; 

        // Use float for this to get precise positions (uniform item widths
        // instead of last one taking any slack), and then convert to ints at last opportunity
        float itemLeft;
        float itemTop = 0;
        // Since each row can have a different number of items, this will be computed per row
        float itemWidth;
        // Subtract the space needed for the horizontal dividers
        final float itemHeight = (menuHeight - mHorizontalDividerHeight * (numRows - 1))
                / (float)numRows;
        
        for (int row = 0; row < numRows; row++) {
            // Start at the left
            itemLeft = 0;
            
            // Subtract the space needed for the vertical dividers, and divide by the number of items
            itemWidth = (menuWidth - mVerticalDividerWidth * (numItemsForRow[row] - 1))
                    / (float)numItemsForRow[row];
            
            for (int itemPosOnRow = 0; itemPosOnRow < numItemsForRow[row]; itemPosOnRow++) {
                // Tell the child to be exactly this size
                child = getChildAt(itemPos);
                child.measure(MeasureSpec.makeMeasureSpec((int) itemWidth, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec((int) itemHeight, MeasureSpec.EXACTLY));
                
                // Remember the child's position for layout
                childLayoutParams = (IconMenuView.LayoutParams) child.getLayoutParams();
                childLayoutParams.left = (int) itemLeft;
                childLayoutParams.right = (int) (itemLeft + itemWidth);
                childLayoutParams.top = (int) itemTop;
                childLayoutParams.bottom = (int) (itemTop + itemHeight); 
                
                // Increment by item width
                itemLeft += itemWidth;
                itemPos++;

                // Add a vertical divider to draw
                if (mVerticalDivider != null) {
                    mVerticalDividerRects.add(new Rect((int) itemLeft,
                            (int) itemTop, (int) (itemLeft + mVerticalDividerWidth),
                            (int) (itemTop + itemHeight)));
                }

                // Increment by divider width (even if we're not computing
                // dividers, since we need to leave room for them when
                // calculating item positions)
                itemLeft += mVerticalDividerWidth;
            }
            
            // Last child on each row should extend to very right edge
            if (childLayoutParams != null) {
                childLayoutParams.right = menuWidth;
            }
            
            itemTop += itemHeight;

            // Add a horizontal divider to draw
            if ((mHorizontalDivider != null) && (row < numRowsMinus1)) {
                mHorizontalDividerRects.add(new Rect(0, (int) itemTop, menuWidth,
                        (int) (itemTop + mHorizontalDividerHeight)));

                itemTop += mHorizontalDividerHeight;
            }
        }
    
public voidrun()
When this method is invoked if the menu is currently not being longpressed, it means that the longpress has just been reached (so we set longpress flag, and start cycling). If it is being longpressed, we cycle to the next mode.

        
        if (mMenuBeingLongpressed) {

            // Cycle to other caption mode on the children
            setChildrenCaptionMode(!mLastChildrenCaptionMode);

        } else {
            
            // Switch ourselves to continuously cycle the items captions
            mMenuBeingLongpressed = true;
            setCycleShortcutCaptionMode(true);
        }

        // We should run again soon to cycle to the other caption mode
        postDelayed(this, ITEM_CAPTION_CYCLE_DELAY);
    
private voidsetChildrenCaptionMode(boolean shortcut)
Iterates children and sets the desired shortcut mode. Only {@link #setCycleShortcutCaptionMode(boolean)} and {@link #run()} should call this.

param
shortcut Whether to show shortcut or the title.

        
        // Set the last caption mode pushed to children
        mLastChildrenCaptionMode = shortcut;
        
        for (int i = getChildCount() - 1; i >= 0; i--) {
            ((IconMenuItemView) getChildAt(i)).setCaptionMode(shortcut);
        }
    
private voidsetCycleShortcutCaptionMode(boolean cycleShortcutAndNormal)
Sets the shortcut caption mode for IconMenuView. This mode will continuously cycle between a child's shortcut and its title.

param
cycleShortcutAndNormal Whether to go into cycling shortcut mode, or to go back to normal.


        if (!cycleShortcutAndNormal) {
            /*
             * We're setting back to title, so remove any callbacks for setting
             * to shortcut
             */
            removeCallbacks(this);
            setChildrenCaptionMode(false);
            mMenuBeingLongpressed = false;
            
        } else {
            
            // Set it the first time (the cycle will be started in run()).
            setChildrenCaptionMode(true);
        }
        
    
public voidupdateChildren(boolean cleared)

        // This method does a clear refresh of children
        removeAllViews();
        
        final ArrayList<MenuItemImpl> itemsToShow = mMenu.getVisibleItems();
        final int numItems = itemsToShow.size();
        final int numItemsThatCanFit = mMaxItems;
        // Minimum of the num that can fit and the num that we have
        final int minFitMinus1AndNumItems = Math.min(numItemsThatCanFit - 1, numItems);
        
        MenuItemImpl itemData;
        // Traverse through all but the last item that can fit since that last item can either
        // be a 'More' button or a sixth item
        for (int i = 0; i < minFitMinus1AndNumItems; i++) {
            itemData = itemsToShow.get(i);
            addItemView((IconMenuItemView) itemData.getItemView(MenuBuilder.TYPE_ICON, this));
        }

        if (numItems > numItemsThatCanFit) {
            // If there are more items than we can fit, show the 'More' button to
            // switch to expanded mode
            if (mMoreItemView == null) {
                mMoreItemView = createMoreItemView();
            }
            
            addItemView(mMoreItemView);
            
            // The last view is the more button, so the actual number of items is one less than
            // the number that can fit
            mNumActualItemsShown = numItemsThatCanFit - 1;
        } else if (numItems == numItemsThatCanFit) {
            // There are exactly the number we can show, so show the last item 
            final MenuItemImpl lastItemData = itemsToShow.get(numItemsThatCanFit - 1);
            addItemView((IconMenuItemView) lastItemData.getItemView(MenuBuilder.TYPE_ICON, this));
            
            // The items shown fit exactly
            mNumActualItemsShown = numItemsThatCanFit;
        }