blob: 560a082b6da4dd9879912ca7fe4e917682c80600 [file] [log] [blame]
/*
* 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==MATCH_PARENT.
ViewGroup.LayoutParams lp =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_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.MATCH_PARENT, android.view.ViewGroup.LayoutParams.MATCH_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);
}
}