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 android.widget; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.graphics.PixelFormat; 24 import android.graphics.Rect; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.util.Log; 28 import android.view.Gravity; 29 import android.view.KeyEvent; 30 import android.view.LayoutInflater; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.ViewConfiguration; 34 import android.view.ViewGroup; 35 import android.view.ViewRootImpl; 36 import android.view.WindowManager; 37 import android.view.View.OnClickListener; 38 import android.view.WindowManager.LayoutParams; 39 40 /* 41 * Implementation notes: 42 * - The zoom controls are displayed in their own window. 43 * (Easier for the client and better performance) 44 * - This window is never touchable, and by default is not focusable. 45 * Its rect is quite big (fills horizontally) but has empty space between the 46 * edges and center. Touches there should be given to the owner. Instead of 47 * having the window touchable and dispatching these empty touch events to the 48 * owner, we set the window to not touchable and steal events from owner 49 * via onTouchListener. 50 * - To make the buttons clickable, it attaches an OnTouchListener to the owner 51 * view and does the hit detection locally (attaches when visible, detaches when invisible). 52 * - When it is focusable, it forwards uninteresting events to the owner view's 53 * view hierarchy. 54 */ 55 /** 56 * The {@link ZoomButtonsController} handles showing and hiding the zoom 57 * controls and positioning it relative to an owner view. It also gives the 58 * client access to the zoom controls container, allowing for additional 59 * accessory buttons to be shown in the zoom controls window. 60 * <p> 61 * Typically, clients should call {@link #setVisible(boolean) setVisible(true)} 62 * on a touch down or move (no need to call {@link #setVisible(boolean) 63 * setVisible(false)} since it will time out on its own). Also, whenever the 64 * owner cannot be zoomed further, the client should update 65 * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}. 66 * <p> 67 * If you are using this with a custom View, please call 68 * {@link #setVisible(boolean) setVisible(false)} from 69 * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged} 70 * when <code>visibility != View.VISIBLE</code>. 71 * 72 */ 73 public class ZoomButtonsController implements View.OnTouchListener { 74 75 private static final String TAG = "ZoomButtonsController"; 76 77 private static final int ZOOM_CONTROLS_TIMEOUT = 78 (int) ViewConfiguration.getZoomControlsTimeout(); 79 80 private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20; 81 private int mTouchPaddingScaledSq; 82 83 private final Context mContext; 84 private final WindowManager mWindowManager; 85 private boolean mAutoDismissControls = true; 86 87 /** 88 * The view that is being zoomed by this zoom controller. 89 */ 90 private final View mOwnerView; 91 92 /** 93 * The location of the owner view on the screen. This is recalculated 94 * each time the zoom controller is shown. 95 */ 96 private final int[] mOwnerViewRawLocation = new int[2]; 97 98 /** 99 * The container that is added as a window. 100 */ 101 private final FrameLayout mContainer; 102 private LayoutParams mContainerLayoutParams; 103 private final int[] mContainerRawLocation = new int[2]; 104 105 private ZoomControls mControls; 106 107 /** 108 * The view (or null) that should receive touch events. This will get set if 109 * the touch down hits the container. It will be reset on the touch up. 110 */ 111 private View mTouchTargetView; 112 /** 113 * The {@link #mTouchTargetView}'s location in window, set on touch down. 114 */ 115 private final int[] mTouchTargetWindowLocation = new int[2]; 116 117 /** 118 * If the zoom controller is dismissed but the user is still in a touch 119 * interaction, we set this to true. This will ignore all touch events until 120 * up/cancel, and then set the owner's touch listener to null. 121 * <p> 122 * Otherwise, the owner view would get mismatched events (i.e., touch move 123 * even though it never got the touch down.) 124 */ 125 private boolean mReleaseTouchListenerOnUp; 126 127 /** Whether the container has been added to the window manager. */ 128 private boolean mIsVisible; 129 130 private final Rect mTempRect = new Rect(); 131 private final int[] mTempIntArray = new int[2]; 132 133 private OnZoomListener mCallback; 134 135 /** 136 * When showing the zoom, we add the view as a new window. However, there is 137 * logic that needs to know the size of the zoom which is determined after 138 * it's laid out. Therefore, we must post this logic onto the UI thread so 139 * it will be exceuted AFTER the layout. This is the logic. 140 */ 141 private Runnable mPostedVisibleInitializer; 142 143 private final IntentFilter mConfigurationChangedFilter = 144 new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); 145 146 /** 147 * Needed to reposition the zoom controls after configuration changes. 148 */ 149 private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { 150 @Override 151 public void onReceive(Context context, Intent intent) { 152 if (!mIsVisible) return; 153 154 mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); 155 mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); 156 } 157 }; 158 159 /** When configuration changes, this is called after the UI thread is idle. */ 160 private static final int MSG_POST_CONFIGURATION_CHANGED = 2; 161 /** Used to delay the zoom controller dismissal. */ 162 private static final int MSG_DISMISS_ZOOM_CONTROLS = 3; 163 /** 164 * If setVisible(true) is called and the owner view's window token is null, 165 * we delay the setVisible(true) call until it is not null. 166 */ 167 private static final int MSG_POST_SET_VISIBLE = 4; 168 169 private final Handler mHandler = new Handler() { 170 @Override 171 public void handleMessage(Message msg) { 172 switch (msg.what) { 173 case MSG_POST_CONFIGURATION_CHANGED: 174 onPostConfigurationChanged(); 175 break; 176 177 case MSG_DISMISS_ZOOM_CONTROLS: 178 setVisible(false); 179 break; 180 181 case MSG_POST_SET_VISIBLE: 182 if (mOwnerView.getWindowToken() == null) { 183 // Doh, it is still null, just ignore the set visible call 184 Log.e(TAG, 185 "Cannot make the zoom controller visible if the owner view is " + 186 "not attached to a window."); 187 } else { 188 setVisible(true); 189 } 190 break; 191 } 192 193 } 194 }; 195 196 /** 197 * Constructor for the {@link ZoomButtonsController}. 198 * 199 * @param ownerView The view that is being zoomed by the zoom controls. The 200 * zoom controls will be displayed aligned with this view. 201 */ 202 public ZoomButtonsController(View ownerView) { 203 mContext = ownerView.getContext(); 204 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 205 mOwnerView = ownerView; 206 207 mTouchPaddingScaledSq = (int) 208 (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density); 209 mTouchPaddingScaledSq *= mTouchPaddingScaledSq; 210 211 mContainer = createContainer(); 212 } 213 214 /** 215 * Whether to enable the zoom in control. 216 * 217 * @param enabled Whether to enable the zoom in control. 218 */ 219 public void setZoomInEnabled(boolean enabled) { 220 mControls.setIsZoomInEnabled(enabled); 221 } 222 223 /** 224 * Whether to enable the zoom out control. 225 * 226 * @param enabled Whether to enable the zoom out control. 227 */ 228 public void setZoomOutEnabled(boolean enabled) { 229 mControls.setIsZoomOutEnabled(enabled); 230 } 231 232 /** 233 * Sets the delay between zoom callbacks as the user holds a zoom button. 234 * 235 * @param speed The delay in milliseconds between zoom callbacks. 236 */ 237 public void setZoomSpeed(long speed) { 238 mControls.setZoomSpeed(speed); 239 } 240 241 private FrameLayout createContainer() { 242 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 243 // Controls are positioned BOTTOM | CENTER with respect to the owner view. 244 lp.gravity = Gravity.TOP | Gravity.START; 245 lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE | 246 LayoutParams.FLAG_NOT_FOCUSABLE | 247 LayoutParams.FLAG_LAYOUT_NO_LIMITS | 248 LayoutParams.FLAG_ALT_FOCUSABLE_IM; 249 lp.height = LayoutParams.WRAP_CONTENT; 250 lp.width = LayoutParams.MATCH_PARENT; 251 lp.type = LayoutParams.TYPE_APPLICATION_PANEL; 252 lp.format = PixelFormat.TRANSLUCENT; 253 lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons; 254 mContainerLayoutParams = lp; 255 256 FrameLayout container = new Container(mContext); 257 container.setLayoutParams(lp); 258 container.setMeasureAllChildren(true); 259 260 LayoutInflater inflater = (LayoutInflater) mContext 261 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 262 inflater.inflate(com.android.internal.R.layout.zoom_container, container); 263 264 mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls); 265 mControls.setOnZoomInClickListener(new OnClickListener() { 266 public void onClick(View v) { 267 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 268 if (mCallback != null) mCallback.onZoom(true); 269 } 270 }); 271 mControls.setOnZoomOutClickListener(new OnClickListener() { 272 public void onClick(View v) { 273 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 274 if (mCallback != null) mCallback.onZoom(false); 275 } 276 }); 277 278 return container; 279 } 280 281 /** 282 * Sets the {@link OnZoomListener} listener that receives callbacks to zoom. 283 * 284 * @param listener The listener that will be told to zoom. 285 */ 286 public void setOnZoomListener(OnZoomListener listener) { 287 mCallback = listener; 288 } 289 290 /** 291 * Sets whether the zoom controls should be focusable. If the controls are 292 * focusable, then trackball and arrow key interactions are possible. 293 * Otherwise, only touch interactions are possible. 294 * 295 * @param focusable Whether the zoom controls should be focusable. 296 */ 297 public void setFocusable(boolean focusable) { 298 int oldFlags = mContainerLayoutParams.flags; 299 if (focusable) { 300 mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; 301 } else { 302 mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE; 303 } 304 305 if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) { 306 mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); 307 } 308 } 309 310 /** 311 * Whether the zoom controls will be automatically dismissed after showing. 312 * 313 * @return Whether the zoom controls will be auto dismissed after showing. 314 */ 315 public boolean isAutoDismissed() { 316 return mAutoDismissControls; 317 } 318 319 /** 320 * Sets whether the zoom controls will be automatically dismissed after 321 * showing. 322 */ 323 public void setAutoDismissed(boolean autoDismiss) { 324 if (mAutoDismissControls == autoDismiss) return; 325 mAutoDismissControls = autoDismiss; 326 } 327 328 /** 329 * Whether the zoom controls are visible to the user. 330 * 331 * @return Whether the zoom controls are visible to the user. 332 */ 333 public boolean isVisible() { 334 return mIsVisible; 335 } 336 337 /** 338 * Sets whether the zoom controls should be visible to the user. 339 * 340 * @param visible Whether the zoom controls should be visible to the user. 341 */ 342 public void setVisible(boolean visible) { 343 344 if (visible) { 345 if (mOwnerView.getWindowToken() == null) { 346 /* 347 * We need a window token to show ourselves, maybe the owner's 348 * window hasn't been created yet but it will have been by the 349 * time the looper is idle, so post the setVisible(true) call. 350 */ 351 if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) { 352 mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE); 353 } 354 return; 355 } 356 357 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 358 } 359 360 if (mIsVisible == visible) { 361 return; 362 } 363 mIsVisible = visible; 364 365 if (visible) { 366 if (mContainerLayoutParams.token == null) { 367 mContainerLayoutParams.token = mOwnerView.getWindowToken(); 368 } 369 370 mWindowManager.addView(mContainer, mContainerLayoutParams); 371 372 if (mPostedVisibleInitializer == null) { 373 mPostedVisibleInitializer = new Runnable() { 374 public void run() { 375 refreshPositioningVariables(); 376 377 if (mCallback != null) { 378 mCallback.onVisibilityChanged(true); 379 } 380 } 381 }; 382 } 383 384 mHandler.post(mPostedVisibleInitializer); 385 386 // Handle configuration changes when visible 387 mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); 388 389 // Steal touches events from the owner 390 mOwnerView.setOnTouchListener(this); 391 mReleaseTouchListenerOnUp = false; 392 393 } else { 394 // Don't want to steal any more touches 395 if (mTouchTargetView != null) { 396 // We are still stealing the touch events for this touch 397 // sequence, so release the touch listener later 398 mReleaseTouchListenerOnUp = true; 399 } else { 400 mOwnerView.setOnTouchListener(null); 401 } 402 403 // No longer care about configuration changes 404 mContext.unregisterReceiver(mConfigurationChangedReceiver); 405 406 mWindowManager.removeViewImmediate(mContainer); 407 mHandler.removeCallbacks(mPostedVisibleInitializer); 408 409 if (mCallback != null) { 410 mCallback.onVisibilityChanged(false); 411 } 412 } 413 414 } 415 416 /** 417 * Gets the container that is the parent of the zoom controls. 418 * <p> 419 * The client can add other views to this container to link them with the 420 * zoom controls. 421 * 422 * @return The container of the zoom controls. It will be a layout that 423 * respects the gravity of a child's layout parameters. 424 */ 425 public ViewGroup getContainer() { 426 return mContainer; 427 } 428 429 /** 430 * Gets the view for the zoom controls. 431 * 432 * @return The zoom controls view. 433 */ 434 public View getZoomControls() { 435 return mControls; 436 } 437 438 private void dismissControlsDelayed(int delay) { 439 if (mAutoDismissControls) { 440 mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS); 441 mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay); 442 } 443 } 444 445 private void refreshPositioningVariables() { 446 // if the mOwnerView is detached from window then skip. 447 if (mOwnerView.getWindowToken() == null) return; 448 449 // Position the zoom controls on the bottom of the owner view. 450 int ownerHeight = mOwnerView.getHeight(); 451 int ownerWidth = mOwnerView.getWidth(); 452 // The gap between the top of the owner and the top of the container 453 int containerOwnerYOffset = ownerHeight - mContainer.getHeight(); 454 455 // Calculate the owner view's bounds 456 mOwnerView.getLocationOnScreen(mOwnerViewRawLocation); 457 mContainerRawLocation[0] = mOwnerViewRawLocation[0]; 458 mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset; 459 460 int[] ownerViewWindowLoc = mTempIntArray; 461 mOwnerView.getLocationInWindow(ownerViewWindowLoc); 462 463 // lp.x and lp.y should be relative to the owner's window top-left 464 mContainerLayoutParams.x = ownerViewWindowLoc[0]; 465 mContainerLayoutParams.width = ownerWidth; 466 mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset; 467 if (mIsVisible) { 468 mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); 469 } 470 471 } 472 473 /* This will only be called when the container has focus. */ 474 private boolean onContainerKey(KeyEvent event) { 475 int keyCode = event.getKeyCode(); 476 if (isInterestingKey(keyCode)) { 477 478 if (keyCode == KeyEvent.KEYCODE_BACK) { 479 if (event.getAction() == KeyEvent.ACTION_DOWN 480 && event.getRepeatCount() == 0) { 481 if (mOwnerView != null) { 482 KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState(); 483 if (ds != null) { 484 ds.startTracking(event, this); 485 } 486 } 487 return true; 488 } else if (event.getAction() == KeyEvent.ACTION_UP 489 && event.isTracking() && !event.isCanceled()) { 490 setVisible(false); 491 return true; 492 } 493 494 } else { 495 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 496 } 497 498 // Let the container handle the key 499 return false; 500 501 } else { 502 503 ViewRootImpl viewRoot = mOwnerView.getViewRootImpl(); 504 if (viewRoot != null) { 505 viewRoot.dispatchInputEvent(event); 506 } 507 508 // We gave the key to the owner, don't let the container handle this key 509 return true; 510 } 511 } 512 513 private boolean isInterestingKey(int keyCode) { 514 switch (keyCode) { 515 case KeyEvent.KEYCODE_DPAD_CENTER: 516 case KeyEvent.KEYCODE_DPAD_UP: 517 case KeyEvent.KEYCODE_DPAD_DOWN: 518 case KeyEvent.KEYCODE_DPAD_LEFT: 519 case KeyEvent.KEYCODE_DPAD_RIGHT: 520 case KeyEvent.KEYCODE_ENTER: 521 case KeyEvent.KEYCODE_BACK: 522 return true; 523 default: 524 return false; 525 } 526 } 527 528 /** 529 * @hide The ZoomButtonsController implements the OnTouchListener, but this 530 * does not need to be shown in its public API. 531 */ 532 public boolean onTouch(View v, MotionEvent event) { 533 int action = event.getAction(); 534 535 if (event.getPointerCount() > 1) { 536 // ZoomButtonsController doesn't handle mutitouch. Give up control. 537 return false; 538 } 539 540 if (mReleaseTouchListenerOnUp) { 541 // The controls were dismissed but we need to throw away all events until the up 542 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 543 mOwnerView.setOnTouchListener(null); 544 setTouchTargetView(null); 545 mReleaseTouchListenerOnUp = false; 546 } 547 548 // Eat this event 549 return true; 550 } 551 552 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 553 554 View targetView = mTouchTargetView; 555 556 switch (action) { 557 case MotionEvent.ACTION_DOWN: 558 targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY()); 559 setTouchTargetView(targetView); 560 break; 561 562 case MotionEvent.ACTION_UP: 563 case MotionEvent.ACTION_CANCEL: 564 setTouchTargetView(null); 565 break; 566 } 567 568 if (targetView != null) { 569 // The upperleft corner of the target view in raw coordinates 570 int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0]; 571 int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1]; 572 573 MotionEvent containerEvent = MotionEvent.obtain(event); 574 // Convert the motion event into the target view's coordinates (from 575 // owner view's coordinates) 576 containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX, 577 mOwnerViewRawLocation[1] - targetViewRawY); 578 /* Disallow negative coordinates (which can occur due to 579 * ZOOM_CONTROLS_TOUCH_PADDING) */ 580 // These are floats because we need to potentially offset away this exact amount 581 float containerX = containerEvent.getX(); 582 float containerY = containerEvent.getY(); 583 if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) { 584 containerEvent.offsetLocation(-containerX, 0); 585 } 586 if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) { 587 containerEvent.offsetLocation(0, -containerY); 588 } 589 boolean retValue = targetView.dispatchTouchEvent(containerEvent); 590 containerEvent.recycle(); 591 return retValue; 592 593 } else { 594 return false; 595 } 596 } 597 598 private void setTouchTargetView(View view) { 599 mTouchTargetView = view; 600 if (view != null) { 601 view.getLocationInWindow(mTouchTargetWindowLocation); 602 } 603 } 604 605 /** 606 * Returns the View that should receive a touch at the given coordinates. 607 * 608 * @param rawX The raw X. 609 * @param rawY The raw Y. 610 * @return The view that should receive the touches, or null if there is not one. 611 */ 612 private View findViewForTouch(int rawX, int rawY) { 613 // Reverse order so the child drawn on top gets first dibs. 614 int containerCoordsX = rawX - mContainerRawLocation[0]; 615 int containerCoordsY = rawY - mContainerRawLocation[1]; 616 Rect frame = mTempRect; 617 618 View closestChild = null; 619 int closestChildDistanceSq = Integer.MAX_VALUE; 620 621 for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { 622 View child = mContainer.getChildAt(i); 623 if (child.getVisibility() != View.VISIBLE) { 624 continue; 625 } 626 627 child.getHitRect(frame); 628 if (frame.contains(containerCoordsX, containerCoordsY)) { 629 return child; 630 } 631 632 int distanceX; 633 if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) { 634 distanceX = 0; 635 } else { 636 distanceX = Math.min(Math.abs(frame.left - containerCoordsX), 637 Math.abs(containerCoordsX - frame.right)); 638 } 639 int distanceY; 640 if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) { 641 distanceY = 0; 642 } else { 643 distanceY = Math.min(Math.abs(frame.top - containerCoordsY), 644 Math.abs(containerCoordsY - frame.bottom)); 645 } 646 int distanceSq = distanceX * distanceX + distanceY * distanceY; 647 648 if ((distanceSq < mTouchPaddingScaledSq) && 649 (distanceSq < closestChildDistanceSq)) { 650 closestChild = child; 651 closestChildDistanceSq = distanceSq; 652 } 653 } 654 655 return closestChild; 656 } 657 658 private void onPostConfigurationChanged() { 659 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 660 refreshPositioningVariables(); 661 } 662 663 /** 664 * Interface that will be called when the user performs an interaction that 665 * triggers some action, for example zooming. 666 */ 667 public interface OnZoomListener { 668 669 /** 670 * Called when the zoom controls' visibility changes. 671 * 672 * @param visible Whether the zoom controls are visible. 673 */ 674 void onVisibilityChanged(boolean visible); 675 676 /** 677 * Called when the owner view needs to be zoomed. 678 * 679 * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out. 680 */ 681 void onZoom(boolean zoomIn); 682 } 683 684 private class Container extends FrameLayout { 685 public Container(Context context) { 686 super(context); 687 } 688 689 /* 690 * Need to override this to intercept the key events. Otherwise, we 691 * would attach a key listener to the container but its superclass 692 * ViewGroup gives it to the focused View instead of calling the key 693 * listener, and so we wouldn't get the events. 694 */ 695 @Override 696 public boolean dispatchKeyEvent(KeyEvent event) { 697 return onContainerKey(event) ? true : super.dispatchKeyEvent(event); 698 } 699 } 700 701 } 702