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.ViewParent; 36 import android.view.ViewRoot; 37 import android.view.WindowManager; 38 import android.view.View.OnClickListener; 39 import android.view.WindowManager.LayoutParams; 40 41 /* 42 * Implementation notes: 43 * - The zoom controls are displayed in their own window. 44 * (Easier for the client and better performance) 45 * - This window is never touchable, and by default is not focusable. 46 * Its rect is quite big (fills horizontally) but has empty space between the 47 * edges and center. Touches there should be given to the owner. Instead of 48 * having the window touchable and dispatching these empty touch events to the 49 * owner, we set the window to not touchable and steal events from owner 50 * via onTouchListener. 51 * - To make the buttons clickable, it attaches an OnTouchListener to the owner 52 * view and does the hit detection locally (attaches when visible, detaches when invisible). 53 * - When it is focusable, it forwards uninteresting events to the owner view's 54 * view hierarchy. 55 */ 56 /** 57 * The {@link ZoomButtonsController} handles showing and hiding the zoom 58 * controls and positioning it relative to an owner view. It also gives the 59 * client access to the zoom controls container, allowing for additional 60 * accessory buttons to be shown in the zoom controls window. 61 * <p> 62 * Typically, clients should call {@link #setVisible(boolean) setVisible(true)} 63 * on a touch down or move (no need to call {@link #setVisible(boolean) 64 * setVisible(false)} since it will time out on its own). Also, whenever the 65 * owner cannot be zoomed further, the client should update 66 * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}. 67 * <p> 68 * If you are using this with a custom View, please call 69 * {@link #setVisible(boolean) setVisible(false)} from the 70 * {@link View#onDetachedFromWindow}. 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 */ ZoomButtonsController(View ownerView)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 */ setZoomInEnabled(boolean enabled)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 */ setZoomOutEnabled(boolean enabled)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 */ setZoomSpeed(long speed)237 public void setZoomSpeed(long speed) { 238 mControls.setZoomSpeed(speed); 239 } 240 createContainer()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.LEFT; 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.FILL_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 */ setOnZoomListener(OnZoomListener listener)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 */ setFocusable(boolean focusable)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 */ isAutoDismissed()315 public boolean isAutoDismissed() { 316 return mAutoDismissControls; 317 } 318 319 /** 320 * Sets whether the zoom controls will be automatically dismissed after 321 * showing. 322 */ setAutoDismissed(boolean autoDismiss)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 */ isVisible()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 */ setVisible(boolean visible)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.removeView(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 */ getContainer()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 */ getZoomControls()434 public View getZoomControls() { 435 return mControls; 436 } 437 dismissControlsDelayed(int delay)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 refreshPositioningVariables()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. */ onContainerKey(KeyEvent event)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 ViewRoot viewRoot = getOwnerViewRoot(); 504 if (viewRoot != null) { 505 viewRoot.dispatchKey(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 isInterestingKey(int keyCode)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 getOwnerViewRoot()528 private ViewRoot getOwnerViewRoot() { 529 View rootViewOfOwner = mOwnerView.getRootView(); 530 if (rootViewOfOwner == null) { 531 return null; 532 } 533 534 ViewParent parentOfRootView = rootViewOfOwner.getParent(); 535 if (parentOfRootView instanceof ViewRoot) { 536 return (ViewRoot) parentOfRootView; 537 } else { 538 return null; 539 } 540 } 541 542 /** 543 * @hide The ZoomButtonsController implements the OnTouchListener, but this 544 * does not need to be shown in its public API. 545 */ onTouch(View v, MotionEvent event)546 public boolean onTouch(View v, MotionEvent event) { 547 int action = event.getAction(); 548 549 if (mReleaseTouchListenerOnUp) { 550 // The controls were dismissed but we need to throw away all events until the up 551 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 552 mOwnerView.setOnTouchListener(null); 553 setTouchTargetView(null); 554 mReleaseTouchListenerOnUp = false; 555 } 556 557 // Eat this event 558 return true; 559 } 560 561 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 562 563 View targetView = mTouchTargetView; 564 565 switch (action) { 566 case MotionEvent.ACTION_DOWN: 567 targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY()); 568 setTouchTargetView(targetView); 569 break; 570 571 case MotionEvent.ACTION_UP: 572 case MotionEvent.ACTION_CANCEL: 573 setTouchTargetView(null); 574 break; 575 } 576 577 if (targetView != null) { 578 // The upperleft corner of the target view in raw coordinates 579 int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0]; 580 int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1]; 581 582 MotionEvent containerEvent = MotionEvent.obtain(event); 583 // Convert the motion event into the target view's coordinates (from 584 // owner view's coordinates) 585 containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX, 586 mOwnerViewRawLocation[1] - targetViewRawY); 587 /* Disallow negative coordinates (which can occur due to 588 * ZOOM_CONTROLS_TOUCH_PADDING) */ 589 // These are floats because we need to potentially offset away this exact amount 590 float containerX = containerEvent.getX(); 591 float containerY = containerEvent.getY(); 592 if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) { 593 containerEvent.offsetLocation(-containerX, 0); 594 } 595 if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) { 596 containerEvent.offsetLocation(0, -containerY); 597 } 598 boolean retValue = targetView.dispatchTouchEvent(containerEvent); 599 containerEvent.recycle(); 600 return retValue; 601 602 } else { 603 return false; 604 } 605 } 606 setTouchTargetView(View view)607 private void setTouchTargetView(View view) { 608 mTouchTargetView = view; 609 if (view != null) { 610 view.getLocationInWindow(mTouchTargetWindowLocation); 611 } 612 } 613 614 /** 615 * Returns the View that should receive a touch at the given coordinates. 616 * 617 * @param rawX The raw X. 618 * @param rawY The raw Y. 619 * @return The view that should receive the touches, or null if there is not one. 620 */ findViewForTouch(int rawX, int rawY)621 private View findViewForTouch(int rawX, int rawY) { 622 // Reverse order so the child drawn on top gets first dibs. 623 int containerCoordsX = rawX - mContainerRawLocation[0]; 624 int containerCoordsY = rawY - mContainerRawLocation[1]; 625 Rect frame = mTempRect; 626 627 View closestChild = null; 628 int closestChildDistanceSq = Integer.MAX_VALUE; 629 630 for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { 631 View child = mContainer.getChildAt(i); 632 if (child.getVisibility() != View.VISIBLE) { 633 continue; 634 } 635 636 child.getHitRect(frame); 637 if (frame.contains(containerCoordsX, containerCoordsY)) { 638 return child; 639 } 640 641 int distanceX; 642 if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) { 643 distanceX = 0; 644 } else { 645 distanceX = Math.min(Math.abs(frame.left - containerCoordsX), 646 Math.abs(containerCoordsX - frame.right)); 647 } 648 int distanceY; 649 if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) { 650 distanceY = 0; 651 } else { 652 distanceY = Math.min(Math.abs(frame.top - containerCoordsY), 653 Math.abs(containerCoordsY - frame.bottom)); 654 } 655 int distanceSq = distanceX * distanceX + distanceY * distanceY; 656 657 if ((distanceSq < mTouchPaddingScaledSq) && 658 (distanceSq < closestChildDistanceSq)) { 659 closestChild = child; 660 closestChildDistanceSq = distanceSq; 661 } 662 } 663 664 return closestChild; 665 } 666 onPostConfigurationChanged()667 private void onPostConfigurationChanged() { 668 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 669 refreshPositioningVariables(); 670 } 671 672 /** 673 * Interface that will be called when the user performs an interaction that 674 * triggers some action, for example zooming. 675 */ 676 public interface OnZoomListener { 677 678 /** 679 * Called when the zoom controls' visibility changes. 680 * 681 * @param visible Whether the zoom controls are visible. 682 */ onVisibilityChanged(boolean visible)683 void onVisibilityChanged(boolean visible); 684 685 /** 686 * Called when the owner view needs to be zoomed. 687 * 688 * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out. 689 */ onZoom(boolean zoomIn)690 void onZoom(boolean zoomIn); 691 } 692 693 private class Container extends FrameLayout { Container(Context context)694 public Container(Context context) { 695 super(context); 696 } 697 698 /* 699 * Need to override this to intercept the key events. Otherwise, we 700 * would attach a key listener to the container but its superclass 701 * ViewGroup gives it to the focused View instead of calling the key 702 * listener, and so we wouldn't get the events. 703 */ 704 @Override dispatchKeyEvent(KeyEvent event)705 public boolean dispatchKeyEvent(KeyEvent event) { 706 return onContainerKey(event) ? true : super.dispatchKeyEvent(event); 707 } 708 } 709 710 } 711