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