FileDocCategorySizeDatePackage
InCallMenuView.javaAPI DocAndroid 1.5 API20104Wed May 06 22:42:46 BST 2009com.android.phone

InCallMenuView.java

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.phone;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;


/**
 * Custom View used as the "options panel" for the InCallScreen
 * (i.e. the standard menu triggered by the MENU button.)
 *
 * This class purely handles the layout and display of the in-call menu
 * items, *not* the actual contents of the menu or the states of the
 * items.  (See InCallMenu for the corresponding "model" class.)

 */
class InCallMenuView extends ViewGroup {
    private static final String LOG_TAG = "PHONE/InCallMenuView";
    private static final boolean DBG = false;

    private int mRowHeight;

    /** Divider that is drawn between all rows */
    private Drawable mHorizontalDivider;
    /** Height of the horizontal divider */
    private int mHorizontalDividerHeight;
    /** Set of horizontal divider positions where the horizontal divider will be drawn */
    private ArrayList<Rect> mHorizontalDividerRects;

    /** Divider that is drawn between all columns */
    private Drawable mVerticalDivider;
    /** Width of the vertical divider */
    private int mVerticalDividerWidth;
    /** Set of vertical divider positions where the vertical divider will be drawn */
    private ArrayList<Rect> mVerticalDividerRects;

    /** Background of each item (should contain the selected and focused states) */
    private Drawable mItemBackground;

    /**
     * The actual layout of items in the menu, organized into 3 rows.
     *
     * Row 0 is the topmost row onscreen, item 0 is the leftmost item in a row.
     *
     * Individual items may be disabled or hidden, but never move between
     * rows or change their order within a row.
     */
    private static final int NUM_ROWS = 3;
    private static final int MAX_ITEMS_PER_ROW = 10;
    private InCallMenuItemView[][] mItems = new InCallMenuItemView[NUM_ROWS][MAX_ITEMS_PER_ROW];

    private int mNumItemsForRow[] = new int[NUM_ROWS];

    /**
     * Number of visible items per row, given the current state of all the
     * menu items.
     * A row with zero visible items isn't drawn at all.
     */
    private int mNumVisibleItemsForRow[] = new int[NUM_ROWS];
    private int mNumVisibleRows;

    /**
     * Reference to the InCallScreen activity that owns us.  This will be
     * null if we haven't been initialized yet *or* after the InCallScreen
     * activity has been destroyed.
     */
    private InCallScreen mInCallScreen;


    InCallMenuView(Context context, InCallScreen inCallScreen) {
        super(context);
        if (DBG) log("InCallMenuView constructor...");

        mInCallScreen = inCallScreen;

        // Look up a few styled attrs from IconMenuView and/or MenuView
        // (to keep our look and feel at least *somewhat* consistent with
        // menus in other apps.)

        TypedArray a =
                mContext.obtainStyledAttributes(com.android.internal.R.styleable.IconMenuView);
        if (DBG) log("- IconMenuView styled attrs: " + a);
        mRowHeight = a.getDimensionPixelSize(
                com.android.internal.R.styleable.IconMenuView_rowHeight, 64);
        if (DBG) log("  - mRowHeight: " + mRowHeight);
        a.recycle();

        a = mContext.obtainStyledAttributes(com.android.internal.R.styleable.MenuView);
        if (DBG) log("- MenuView styled attrs: " + a);
        mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
        if (DBG) log("  - mItemBackground: " + mItemBackground);
        mHorizontalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider);
        if (DBG) log("  - mHorizontalDivider: " + mHorizontalDivider);
        mHorizontalDividerRects = new ArrayList<Rect>();
        mVerticalDivider =  a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider);
        if (DBG) log("  - mVerticalDivider: " + mVerticalDivider);
        mVerticalDividerRects = new ArrayList<Rect>();
        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;
        }

        // This view will be drawing the dividers.
        setWillNotDraw(false);

        // Arrange to get key events even when there's no focused item in
        // the in-call menu (i.e. when in touch mode).
        // (We *always* want key events whenever we're visible, so that we
        // can forward them to the InCallScreen activity; see dispatchKeyEvent().)
        setFocusableInTouchMode(true);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);

        // The default ViewGroup.LayoutParams width and height are
        // WRAP_CONTENT.  (This applies to us right now since we
        // initially have no LayoutParams at all.)
        // But in the Menu framework, when returning a view from
        // onCreatePanelView(), a layout width of WRAP_CONTENT indicates
        // that you want the smaller-sized "More" menu frame.  We want the
        // full-screen-width menu frame instead, though, so we need to
        // give ourselves a LayoutParams with width==FILL_PARENT.
        ViewGroup.LayoutParams lp =
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                                           ViewGroup.LayoutParams.WRAP_CONTENT);
        setLayoutParams(lp);
    }

    /**
     * Null out our reference to the InCallScreen activity.
     * This indicates that the InCallScreen activity has been destroyed.
     */
    void clearInCallScreenReference() {
        mInCallScreen = null;
    }

    /**
     * Adds an InCallMenuItemView to the specified row.
     */
    /* package */ void addItemView(InCallMenuItemView itemView, int row) {
        if (DBG) log("addItemView(" + itemView + ", row " + row + ")...");

        if (row >= NUM_ROWS) {
            throw new IllegalStateException("Row index " + row + " > NUM_ROWS");
        }

        int indexInRow = mNumItemsForRow[row];
        if (indexInRow >= MAX_ITEMS_PER_ROW) {
            throw new IllegalStateException("Too many items (" + indexInRow + ") in row " + row);
        }
        mNumItemsForRow[row]++;
        mItems[row][indexInRow] = itemView;

        //
        // Finally, add this item as a child.
        //

        ViewGroup.LayoutParams lp = itemView.getLayoutParams();

        if (lp == null) {
            // Default layout parameters
            lp = new LayoutParams(android.view.ViewGroup.LayoutParams.FILL_PARENT, android.view.ViewGroup.LayoutParams.FILL_PARENT);
        }

        // Apply the background to the item view
        itemView.setBackgroundDrawable(mItemBackground.getConstantState().newDrawable());

        addView(itemView, lp);
    }

    /**
     * Precomputes the number of visible items per row, and the total
     * number of visible rows.  (A row with zero visible items isn't
     * drawn at all.)
     */
    /* package */ void updateVisibility() {
        if (DBG) log("updateVisibility()...");

        mNumVisibleRows = 0;

        for (int row = 0; row < NUM_ROWS; row++) {
            InCallMenuItemView[] thisRow = mItems[row];
            int numItemsThisRow = mNumItemsForRow[row];
            int numVisibleThisRow = 0;
            for (int itemIndex = 0; itemIndex < numItemsThisRow; itemIndex++) {
                // if (DBG) log("  - Checking item: " + mItems[row][itemIndex]);
                if  (mItems[row][itemIndex].isVisible()) numVisibleThisRow++;
            }
            if (DBG) log("==> Num visible for row " + row + ": " + numVisibleThisRow);
            mNumVisibleItemsForRow[row] = numVisibleThisRow;
            if (numVisibleThisRow > 0) mNumVisibleRows++;
        }
        if (DBG) log("==> Num visible rows: " + mNumVisibleRows);
    }

    /* package */ void dumpState() {
        if (DBG) log("============ dumpState() ============");
        if (DBG) log("- mItems LENGTH: " + mItems.length);
        for (int row = 0; row < NUM_ROWS; row++) {
            if (DBG) log("-      Row " + row + ": length " + mItems[row].length
                         + ", num items " + mNumItemsForRow[row]
                         + ", num visible " + mNumVisibleItemsForRow[row]);
        }
    }

    /**
     * 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.
     *
     * At this point the visibility of each item in mItems[][] is correct,
     * and mNumVisibleRows and mNumVisibleItemsForRow[] have already been
     * precomputed.
     *
     * @param menuWidth The width of this menu to assume for positioning
     * @param menuHeight The height of this menu to assume for positioning
     *
     * TODO: This is a near-exact duplicate of IconMenuView.positionChildren().
     * Consider abstracting this out into a more general-purpose "grid layout
     * with dividers" container that both classes could use...
     */
    private void positionChildren(int menuWidth, int menuHeight) {
        if (DBG) log("positionChildren(" + menuWidth + " x " + menuHeight + ")...");

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

        InCallMenuItemView child;
        InCallMenuView.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 * (mNumVisibleRows - 1))
                / (float) mNumVisibleRows;

        // We add horizontal dividers between each visible row, so there should
        // be a total of mNumVisibleRows-1 of them.
        int numHorizDividersRemainingToDraw = mNumVisibleRows - 1;

        for (int row = 0; row < NUM_ROWS; row++) {
            int numItemsThisRow = mNumItemsForRow[row];
            int numVisibleThisRow = mNumVisibleItemsForRow[row];
            if (DBG) log("  - num visible for row " + row + ": " + numVisibleThisRow);
            if (numVisibleThisRow == 0) {
                continue;
            }

            InCallMenuItemView[] thisRow = mItems[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 * (numVisibleThisRow - 1))
                    / (float) numVisibleThisRow;

            for (int itemIndex = 0; itemIndex < numItemsThisRow; itemIndex++) {
                child = mItems[row][itemIndex];

                if (!child.isVisible()) continue;

                if (DBG) log("==> child [" + row + "][" + itemIndex + "]: " + child);

                // Tell the child to be exactly this size
                child.measure(MeasureSpec.makeMeasureSpec((int) itemWidth, MeasureSpec.EXACTLY),
                              MeasureSpec.makeMeasureSpec((int) itemHeight, MeasureSpec.EXACTLY));

                // Remember the child's position for layout
                childLayoutParams = (InCallMenuView.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;

                // 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 (if we need one under this row)
            if ((mHorizontalDivider != null) && (numHorizDividersRemainingToDraw-- > 0)) {
                mHorizontalDividerRects.add(new Rect(0, (int) itemTop, menuWidth,
                                                     (int) (itemTop + mHorizontalDividerHeight)));
                itemTop += mHorizontalDividerHeight;
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (DBG) log("onMeasure(" + widthMeasureSpec + " x " + heightMeasureSpec + ")...");

        // Get the desired height of the icon menu view (last row of items does
        // not have a divider below)
        final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) * mNumVisibleRows
                - mHorizontalDividerHeight;

        // Maximum possible width and desired height
        setMeasuredDimension(resolveSize(Integer.MAX_VALUE, widthMeasureSpec),
                             resolveSize(desiredHeight, heightMeasureSpec));

        // Position the children
        positionChildren(mMeasuredWidth, mMeasuredHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (DBG) log("onLayout(changed " + changed
                     + ", l " + l + " t " + t + " r " + r + " b " + b + ")...");

        View child;
        InCallMenuView.LayoutParams childLayoutParams;

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        if (DBG) log("onDraw()...");

        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);
            }
        }
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (DBG) log("dispatchKeyEvent(" + event + ")...");

        // In most other apps, when a menu is up, the menu itself handles
        // keypresses.  And keys that aren't handled by the menu do NOT
        // get dispatched to the current Activity.
        //
        // But in the in-call UI, we don't have any menu shortcuts, *and*
        // it's important for buttons like CALL to work normally even
        // while the menu is up.  So we handle ALL key events (with some
        // exceptions -- see below) by simply forwarding them to the
        // InCallScreen.

        int keyCode = event.getKeyCode();
        if (event.isDown()) {
            switch (keyCode) {
                // The BACK key dismisses the menu.
                case KeyEvent.KEYCODE_BACK:
                    if (DBG) log("==> BACK key!  handling it ourselves...");
                    // We don't need to do anything here (since BACK
                    // is magically handled by the framework); we just
                    // need to *not* forward it to the InCallScreen.
                    break;

                // Don't send KEYCODE_DPAD_CENTER/KEYCODE_ENTER to the
                // InCallScreen either, since the framework needs those to
                // activate the focused item when using the trackball.
                case KeyEvent.KEYCODE_DPAD_CENTER:
                case KeyEvent.KEYCODE_ENTER:
                    break;

                // Anything else gets forwarded to the InCallScreen.
                default:
                    if (DBG) log("==> dispatchKeyEvent: forwarding event to the InCallScreen");
                    if (mInCallScreen != null) {
                        return mInCallScreen.onKeyDown(keyCode, event);
                    }
                    break;
            }
        } else if (mInCallScreen != null &&
                (keyCode == KeyEvent.KEYCODE_CALL ||
                        mInCallScreen.isKeyEventAcceptableDTMF(event))) {

            // Forward the key-up for the call and dialer buttons to the
            // InCallScreen.  All other key-up events are NOT handled here,
            // but instead fall through to dispatchKeyEvent from the superclass.
            if (DBG) log("==> dispatchKeyEvent: forwarding key up event to the InCallScreen");
            return mInCallScreen.onKeyUp(keyCode, event);
        }
        return super.dispatchKeyEvent(event);
    }


    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new InCallMenuView.LayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        // Override to allow type-checking of LayoutParams.
        return p instanceof InCallMenuView.LayoutParams;
    }

    /**
     * Layout parameters specific to InCallMenuView (stores the left, top,
     * right, bottom from the measure pass).
     */
    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        int left, top, right, bottom;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }
    }

    private void log(String msg) {
        Log.d(LOG_TAG, msg);
    }
}