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