/* * 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 //System.out.println("itemview is :" + mItemBackground); //System.out.println("item is :" + mItemBackground + ":" + mItemBackground.getConstantState()); // Should not like this, maybe it's because of v.2.0, there is no such attribute in v.2.01 by cytown if (mItemBackground == null) { TypedArray a = mContext.obtainStyledAttributes(com.android.internal.R.styleable.IconMenuView); mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground); } 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); } }