1 /* 2 * Copyright (C) 2008 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 17 package com.android.phone; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.Canvas; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.KeyEvent; 27 import android.view.View; 28 import android.view.ViewGroup; 29 30 import java.util.ArrayList; 31 32 33 /** 34 * Custom View used as the "options panel" for the InCallScreen 35 * (i.e. the standard menu triggered by the MENU button.) 36 * 37 * This class purely handles the layout and display of the in-call menu 38 * items, *not* the actual contents of the menu or the states of the 39 * items. (See InCallMenu for the corresponding "model" class.) 40 41 */ 42 class InCallMenuView extends ViewGroup { 43 private static final String LOG_TAG = "PHONE/InCallMenuView"; 44 private static final boolean DBG = false; 45 46 private int mRowHeight; 47 48 /** Divider that is drawn between all rows */ 49 private Drawable mHorizontalDivider; 50 /** Height of the horizontal divider */ 51 private int mHorizontalDividerHeight; 52 /** Set of horizontal divider positions where the horizontal divider will be drawn */ 53 private ArrayList<Rect> mHorizontalDividerRects; 54 55 /** Divider that is drawn between all columns */ 56 private Drawable mVerticalDivider; 57 /** Width of the vertical divider */ 58 private int mVerticalDividerWidth; 59 /** Set of vertical divider positions where the vertical divider will be drawn */ 60 private ArrayList<Rect> mVerticalDividerRects; 61 62 /** Background of each item (should contain the selected and focused states) */ 63 private Drawable mItemBackground; 64 65 /** 66 * The actual layout of items in the menu, organized into 3 rows. 67 * 68 * Row 0 is the topmost row onscreen, item 0 is the leftmost item in a row. 69 * 70 * Individual items may be disabled or hidden, but never move between 71 * rows or change their order within a row. 72 */ 73 private static final int NUM_ROWS = 3; 74 private static final int MAX_ITEMS_PER_ROW = 10; 75 private InCallMenuItemView[][] mItems = new InCallMenuItemView[NUM_ROWS][MAX_ITEMS_PER_ROW]; 76 77 private int mNumItemsForRow[] = new int[NUM_ROWS]; 78 79 /** 80 * Number of visible items per row, given the current state of all the 81 * menu items. 82 * A row with zero visible items isn't drawn at all. 83 */ 84 private int mNumVisibleItemsForRow[] = new int[NUM_ROWS]; 85 private int mNumVisibleRows; 86 87 /** 88 * Reference to the InCallScreen activity that owns us. This will be 89 * null if we haven't been initialized yet *or* after the InCallScreen 90 * activity has been destroyed. 91 */ 92 private InCallScreen mInCallScreen; 93 94 95 InCallMenuView(Context context, InCallScreen inCallScreen) { 96 super(context); 97 if (DBG) log("InCallMenuView constructor..."); 98 99 mInCallScreen = inCallScreen; 100 101 // Look up a few styled attrs from IconMenuView and/or MenuView 102 // (to keep our look and feel at least *somewhat* consistent with 103 // menus in other apps.) 104 105 TypedArray a = 106 mContext.obtainStyledAttributes(com.android.internal.R.styleable.IconMenuView); 107 if (DBG) log("- IconMenuView styled attrs: " + a); 108 mRowHeight = a.getDimensionPixelSize( 109 com.android.internal.R.styleable.IconMenuView_rowHeight, 64); 110 if (DBG) log(" - mRowHeight: " + mRowHeight); 111 a.recycle(); 112 113 a = mContext.obtainStyledAttributes(com.android.internal.R.styleable.MenuView); 114 if (DBG) log("- MenuView styled attrs: " + a); 115 mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground); 116 if (DBG) log(" - mItemBackground: " + mItemBackground); 117 mHorizontalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider); 118 if (DBG) log(" - mHorizontalDivider: " + mHorizontalDivider); 119 mHorizontalDividerRects = new ArrayList<Rect>(); 120 mVerticalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider); 121 if (DBG) log(" - mVerticalDivider: " + mVerticalDivider); 122 mVerticalDividerRects = new ArrayList<Rect>(); 123 a.recycle(); 124 125 if (mHorizontalDivider != null) { 126 mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight(); 127 // Make sure to have some height for the divider 128 if (mHorizontalDividerHeight == -1) mHorizontalDividerHeight = 1; 129 } 130 131 if (mVerticalDivider != null) { 132 mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth(); 133 // Make sure to have some width for the divider 134 if (mVerticalDividerWidth == -1) mVerticalDividerWidth = 1; 135 } 136 137 // This view will be drawing the dividers. 138 setWillNotDraw(false); 139 140 // Arrange to get key events even when there's no focused item in 141 // the in-call menu (i.e. when in touch mode). 142 // (We *always* want key events whenever we're visible, so that we 143 // can forward them to the InCallScreen activity; see dispatchKeyEvent().) 144 setFocusableInTouchMode(true); 145 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 146 147 // The default ViewGroup.LayoutParams width and height are 148 // WRAP_CONTENT. (This applies to us right now since we 149 // initially have no LayoutParams at all.) 150 // But in the Menu framework, when returning a view from 151 // onCreatePanelView(), a layout width of WRAP_CONTENT indicates 152 // that you want the smaller-sized "More" menu frame. We want the 153 // full-screen-width menu frame instead, though, so we need to 154 // give ourselves a LayoutParams with width==MATCH_PARENT. 155 ViewGroup.LayoutParams lp = 156 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 157 ViewGroup.LayoutParams.WRAP_CONTENT); 158 setLayoutParams(lp); 159 } 160 161 /** 162 * Null out our reference to the InCallScreen activity. 163 * This indicates that the InCallScreen activity has been destroyed. 164 */ 165 void clearInCallScreenReference() { 166 mInCallScreen = null; 167 } 168 169 /** 170 * Adds an InCallMenuItemView to the specified row. 171 */ 172 /* package */ void addItemView(InCallMenuItemView itemView, int row) { 173 if (DBG) log("addItemView(" + itemView + ", row " + row + ")..."); 174 175 if (row >= NUM_ROWS) { 176 throw new IllegalStateException("Row index " + row + " > NUM_ROWS"); 177 } 178 179 int indexInRow = mNumItemsForRow[row]; 180 if (indexInRow >= MAX_ITEMS_PER_ROW) { 181 throw new IllegalStateException("Too many items (" + indexInRow + ") in row " + row); 182 } 183 mNumItemsForRow[row]++; 184 mItems[row][indexInRow] = itemView; 185 186 // 187 // Finally, add this item as a child. 188 // 189 190 ViewGroup.LayoutParams lp = itemView.getLayoutParams(); 191 192 if (lp == null) { 193 // Default layout parameters 194 lp = new LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.MATCH_PARENT); 195 } 196 197 // Apply the background to the item view 198 itemView.setBackgroundDrawable(mItemBackground.getConstantState().newDrawable()); 199 200 addView(itemView, lp); 201 } 202 203 /** 204 * Precomputes the number of visible items per row, and the total 205 * number of visible rows. (A row with zero visible items isn't 206 * drawn at all.) 207 */ 208 /* package */ void updateVisibility() { 209 if (DBG) log("updateVisibility()..."); 210 211 mNumVisibleRows = 0; 212 213 for (int row = 0; row < NUM_ROWS; row++) { 214 InCallMenuItemView[] thisRow = mItems[row]; 215 int numItemsThisRow = mNumItemsForRow[row]; 216 int numVisibleThisRow = 0; 217 for (int itemIndex = 0; itemIndex < numItemsThisRow; itemIndex++) { 218 // if (DBG) log(" - Checking item: " + mItems[row][itemIndex]); 219 if (mItems[row][itemIndex].isVisible()) numVisibleThisRow++; 220 } 221 if (DBG) log("==> Num visible for row " + row + ": " + numVisibleThisRow); 222 mNumVisibleItemsForRow[row] = numVisibleThisRow; 223 if (numVisibleThisRow > 0) mNumVisibleRows++; 224 } 225 if (DBG) log("==> Num visible rows: " + mNumVisibleRows); 226 } 227 228 /* package */ void dumpState() { 229 if (DBG) log("============ dumpState() ============"); 230 if (DBG) log("- mItems LENGTH: " + mItems.length); 231 for (int row = 0; row < NUM_ROWS; row++) { 232 if (DBG) log("- Row " + row + ": length " + mItems[row].length 233 + ", num items " + mNumItemsForRow[row] 234 + ", num visible " + mNumVisibleItemsForRow[row]); 235 } 236 } 237 238 /** 239 * The positioning algorithm that gets called from onMeasure. It just 240 * computes positions for each child, and then stores them in the 241 * child's layout params. 242 * 243 * At this point the visibility of each item in mItems[][] is correct, 244 * and mNumVisibleRows and mNumVisibleItemsForRow[] have already been 245 * precomputed. 246 * 247 * @param menuWidth The width of this menu to assume for positioning 248 * @param menuHeight The height of this menu to assume for positioning 249 * 250 * TODO: This is a near-exact duplicate of IconMenuView.positionChildren(). 251 * Consider abstracting this out into a more general-purpose "grid layout 252 * with dividers" container that both classes could use... 253 */ 254 private void positionChildren(int menuWidth, int menuHeight) { 255 if (DBG) log("positionChildren(" + menuWidth + " x " + menuHeight + ")..."); 256 257 // Clear the containers for the positions where the dividers should be drawn 258 if (mHorizontalDivider != null) mHorizontalDividerRects.clear(); 259 if (mVerticalDivider != null) mVerticalDividerRects.clear(); 260 261 InCallMenuItemView child; 262 InCallMenuView.LayoutParams childLayoutParams = null; 263 264 // Use float for this to get precise positions (uniform item widths 265 // instead of last one taking any slack), and then convert to ints at last opportunity 266 float itemLeft; 267 float itemTop = 0; 268 // Since each row can have a different number of items, this will be computed per row 269 float itemWidth; 270 // Subtract the space needed for the horizontal dividers 271 final float itemHeight = (menuHeight - mHorizontalDividerHeight * (mNumVisibleRows - 1)) 272 / (float) mNumVisibleRows; 273 274 // We add horizontal dividers between each visible row, so there should 275 // be a total of mNumVisibleRows-1 of them. 276 int numHorizDividersRemainingToDraw = mNumVisibleRows - 1; 277 278 for (int row = 0; row < NUM_ROWS; row++) { 279 int numItemsThisRow = mNumItemsForRow[row]; 280 int numVisibleThisRow = mNumVisibleItemsForRow[row]; 281 if (DBG) log(" - num visible for row " + row + ": " + numVisibleThisRow); 282 if (numVisibleThisRow == 0) { 283 continue; 284 } 285 286 InCallMenuItemView[] thisRow = mItems[row]; 287 288 // Start at the left 289 itemLeft = 0; 290 291 // Subtract the space needed for the vertical dividers, and 292 // divide by the number of items. 293 itemWidth = (menuWidth - mVerticalDividerWidth * (numVisibleThisRow - 1)) 294 / (float) numVisibleThisRow; 295 296 for (int itemIndex = 0; itemIndex < numItemsThisRow; itemIndex++) { 297 child = mItems[row][itemIndex]; 298 299 if (!child.isVisible()) continue; 300 301 if (DBG) log("==> child [" + row + "][" + itemIndex + "]: " + child); 302 303 // Tell the child to be exactly this size 304 child.measure(MeasureSpec.makeMeasureSpec((int) itemWidth, MeasureSpec.EXACTLY), 305 MeasureSpec.makeMeasureSpec((int) itemHeight, MeasureSpec.EXACTLY)); 306 307 // Remember the child's position for layout 308 childLayoutParams = (InCallMenuView.LayoutParams) child.getLayoutParams(); 309 childLayoutParams.left = (int) itemLeft; 310 childLayoutParams.right = (int) (itemLeft + itemWidth); 311 childLayoutParams.top = (int) itemTop; 312 childLayoutParams.bottom = (int) (itemTop + itemHeight); 313 314 // Increment by item width 315 itemLeft += itemWidth; 316 317 // Add a vertical divider to draw 318 if (mVerticalDivider != null) { 319 mVerticalDividerRects.add(new Rect((int) itemLeft, 320 (int) itemTop, (int) (itemLeft + mVerticalDividerWidth), 321 (int) (itemTop + itemHeight))); 322 } 323 324 // Increment by divider width (even if we're not computing 325 // dividers, since we need to leave room for them when 326 // calculating item positions) 327 itemLeft += mVerticalDividerWidth; 328 } 329 330 // Last child on each row should extend to very right edge 331 if (childLayoutParams != null) { 332 childLayoutParams.right = menuWidth; 333 } 334 335 itemTop += itemHeight; 336 337 // Add a horizontal divider (if we need one under this row) 338 if ((mHorizontalDivider != null) && (numHorizDividersRemainingToDraw-- > 0)) { 339 mHorizontalDividerRects.add(new Rect(0, (int) itemTop, menuWidth, 340 (int) (itemTop + mHorizontalDividerHeight))); 341 itemTop += mHorizontalDividerHeight; 342 } 343 } 344 } 345 346 @Override 347 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 348 if (DBG) log("onMeasure(" + widthMeasureSpec + " x " + heightMeasureSpec + ")..."); 349 350 // Get the desired height of the icon menu view (last row of items does 351 // not have a divider below) 352 final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) * mNumVisibleRows 353 - mHorizontalDividerHeight; 354 355 // Maximum possible width and desired height 356 setMeasuredDimension(resolveSize(Integer.MAX_VALUE, widthMeasureSpec), 357 resolveSize(desiredHeight, heightMeasureSpec)); 358 359 // Position the children 360 positionChildren(mMeasuredWidth, mMeasuredHeight); 361 } 362 363 @Override 364 protected void onLayout(boolean changed, int l, int t, int r, int b) { 365 if (DBG) log("onLayout(changed " + changed 366 + ", l " + l + " t " + t + " r " + r + " b " + b + ")..."); 367 368 View child; 369 InCallMenuView.LayoutParams childLayoutParams; 370 371 for (int i = getChildCount() - 1; i >= 0; i--) { 372 child = getChildAt(i); 373 childLayoutParams = (InCallMenuView.LayoutParams) child.getLayoutParams(); 374 375 // Layout children according to positions set during the measure 376 child.layout(childLayoutParams.left, childLayoutParams.top, 377 childLayoutParams.right, childLayoutParams.bottom); 378 } 379 } 380 381 @Override 382 protected void onDraw(Canvas canvas) { 383 if (DBG) log("onDraw()..."); 384 385 if (mHorizontalDivider != null) { 386 // If we have a horizontal divider to draw, draw it at the remembered positions 387 for (int i = mHorizontalDividerRects.size() - 1; i >= 0; i--) { 388 mHorizontalDivider.setBounds(mHorizontalDividerRects.get(i)); 389 mHorizontalDivider.draw(canvas); 390 } 391 } 392 393 if (mVerticalDivider != null) { 394 // If we have a vertical divider to draw, draw it at the remembered positions 395 for (int i = mVerticalDividerRects.size() - 1; i >= 0; i--) { 396 mVerticalDivider.setBounds(mVerticalDividerRects.get(i)); 397 mVerticalDivider.draw(canvas); 398 } 399 } 400 } 401 402 @Override 403 public boolean dispatchKeyEvent(KeyEvent event) { 404 if (DBG) log("dispatchKeyEvent(" + event + ")..."); 405 406 // In most other apps, when a menu is up, the menu itself handles 407 // keypresses. And keys that aren't handled by the menu do NOT 408 // get dispatched to the current Activity. 409 // 410 // But in the in-call UI, we don't have any menu shortcuts, *and* 411 // it's important for buttons like CALL to work normally even 412 // while the menu is up. So we handle ALL key events (with some 413 // exceptions -- see below) by simply forwarding them to the 414 // InCallScreen. 415 416 int keyCode = event.getKeyCode(); 417 if (event.isDown()) { 418 switch (keyCode) { 419 // The BACK key dismisses the menu. 420 case KeyEvent.KEYCODE_BACK: 421 if (DBG) log("==> BACK key! handling it ourselves..."); 422 // We don't need to do anything here (since BACK 423 // is magically handled by the framework); we just 424 // need to *not* forward it to the InCallScreen. 425 break; 426 427 // Don't send KEYCODE_DPAD_CENTER/KEYCODE_ENTER to the 428 // InCallScreen either, since the framework needs those to 429 // activate the focused item when using the trackball. 430 case KeyEvent.KEYCODE_DPAD_CENTER: 431 case KeyEvent.KEYCODE_ENTER: 432 break; 433 434 // Anything else gets forwarded to the InCallScreen. 435 default: 436 if (DBG) log("==> dispatchKeyEvent: forwarding event to the InCallScreen"); 437 if (mInCallScreen != null) { 438 return mInCallScreen.onKeyDown(keyCode, event); 439 } 440 break; 441 } 442 } else if (mInCallScreen != null && 443 (keyCode == KeyEvent.KEYCODE_CALL || 444 mInCallScreen.isKeyEventAcceptableDTMF(event))) { 445 446 // Forward the key-up for the call and dialer buttons to the 447 // InCallScreen. All other key-up events are NOT handled here, 448 // but instead fall through to dispatchKeyEvent from the superclass. 449 if (DBG) log("==> dispatchKeyEvent: forwarding key up event to the InCallScreen"); 450 return mInCallScreen.onKeyUp(keyCode, event); 451 } 452 return super.dispatchKeyEvent(event); 453 } 454 455 456 @Override 457 public LayoutParams generateLayoutParams(AttributeSet attrs) { 458 return new InCallMenuView.LayoutParams(getContext(), attrs); 459 } 460 461 @Override 462 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 463 // Override to allow type-checking of LayoutParams. 464 return p instanceof InCallMenuView.LayoutParams; 465 } 466 467 /** 468 * Layout parameters specific to InCallMenuView (stores the left, top, 469 * right, bottom from the measure pass). 470 */ 471 public static class LayoutParams extends ViewGroup.MarginLayoutParams { 472 int left, top, right, bottom; 473 474 public LayoutParams(Context c, AttributeSet attrs) { 475 super(c, attrs); 476 } 477 478 public LayoutParams(int width, int height) { 479 super(width, height); 480 } 481 } 482 483 private void log(String msg) { 484 Log.d(LOG_TAG, msg); 485 } 486 } 487