ZoomButtonsController.java revision ba87e3e6c985e7175152993b5efcc7dd2f0e1c93
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 17package android.widget; 18 19import android.app.AlertDialog; 20import android.app.Dialog; 21import android.content.BroadcastReceiver; 22import android.content.ContentResolver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentFilter; 26import android.content.SharedPreferences; 27import android.graphics.Canvas; 28import android.graphics.PixelFormat; 29import android.graphics.Rect; 30import android.os.Handler; 31import android.os.Message; 32import android.os.SystemClock; 33import android.provider.Settings; 34import android.util.Log; 35import android.view.GestureDetector; 36import android.view.Gravity; 37import android.view.KeyEvent; 38import android.view.LayoutInflater; 39import android.view.MotionEvent; 40import android.view.View; 41import android.view.ViewConfiguration; 42import android.view.ViewGroup; 43import android.view.ViewParent; 44import android.view.ViewRoot; 45import android.view.Window; 46import android.view.WindowManager; 47import android.view.View.OnClickListener; 48import android.view.WindowManager.LayoutParams; 49 50/* 51 * Implementation notes: 52 * - The zoom controls are displayed in their own window. 53 * (Easier for the client and better performance) 54 * - This window is not touchable, and by default is not focusable. 55 * - To make the buttons clickable, it attaches a OnTouchListener to the owner 56 * view and does the hit detection locally. 57 * - When it is focusable, it forwards uninteresting events to the owner view's 58 * view hierarchy. 59 */ 60/** 61 * The {@link ZoomButtonsController} handles showing and hiding the zoom 62 * controls relative to an owner view. It also gives the client access to the 63 * zoom controls container, allowing for additional accessory buttons to be 64 * shown in the zoom controls window. 65 * <p> 66 * Typical usage involves the client using the {@link GestureDetector} to 67 * forward events from 68 * {@link GestureDetector.OnDoubleTapListener#onDoubleTapEvent(MotionEvent)} to 69 * {@link #handleDoubleTapEvent(MotionEvent)}. Also, whenever the owner cannot 70 * be zoomed further, the client should update 71 * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}. 72 * <p> 73 * If you are using this with a custom View, please call 74 * {@link #setVisible(boolean) setVisible(false)} from the 75 * {@link View#onDetachedFromWindow}. 76 * 77 * @hide 78 */ 79public class ZoomButtonsController implements View.OnTouchListener { 80 81 private static final String TAG = "ZoomButtonsController"; 82 83 private static final int ZOOM_CONTROLS_TIMEOUT = 84 (int) ViewConfiguration.getZoomControlsTimeout(); 85 86 private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20; 87 private int mTouchPaddingScaledSq; 88 89 private Context mContext; 90 private WindowManager mWindowManager; 91 92 /** 93 * The view that is being zoomed by this zoom controller. 94 */ 95 private View mOwnerView; 96 97 /** 98 * The location of the owner view on the screen. This is recalculated 99 * each time the zoom controller is shown. 100 */ 101 private int[] mOwnerViewRawLocation = new int[2]; 102 103 /** 104 * The container that is added as a window. 105 */ 106 private FrameLayout mContainer; 107 private LayoutParams mContainerLayoutParams; 108 private int[] mContainerRawLocation = new int[2]; 109 110 private ZoomControls mControls; 111 112 /** 113 * The view (or null) that should receive touch events. This will get set if 114 * the touch down hits the container. It will be reset on the touch up. 115 */ 116 private View mTouchTargetView; 117 /** 118 * The {@link #mTouchTargetView}'s location in window, set on touch down. 119 */ 120 private int[] mTouchTargetWindowLocation = new int[2]; 121 /** 122 * If the zoom controller is dismissed but the user is still in a touch 123 * interaction, we set this to true. This will ignore all touch events until 124 * up/cancel, and then set the owner's touch listener to null. 125 */ 126 private boolean mReleaseTouchListenerOnUp; 127 128 /** 129 * Whether we are currently in the double-tap gesture, with the second tap 130 * still being performed (i.e., we're waiting for the second tap's touch up). 131 */ 132 private boolean mIsSecondTapDown; 133 134 /** Whether the container has been added to the window manager. */ 135 private boolean mIsVisible; 136 137 private Rect mTempRect = new Rect(); 138 private int[] mTempIntArray = new int[2]; 139 140 private OnZoomListener mCallback; 141 142 /** 143 * In 1.0, the ZoomControls were to be added to the UI by the client of 144 * WebView, MapView, etc. We didn't want apps to break, so we return a dummy 145 * view in place now. 146 */ 147 private InvisibleView mDummyZoomControls; 148 149 /** 150 * When showing the zoom, we add the view as a new window. However, there is 151 * logic that needs to know the size of the zoom which is determined after 152 * it's laid out. Therefore, we must post this logic onto the UI thread so 153 * it will be exceuted AFTER the layout. This is the logic. 154 */ 155 private Runnable mPostedVisibleInitializer; 156 157 private IntentFilter mConfigurationChangedFilter = 158 new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); 159 160 /** 161 * Needed to reposition the zoom controls after configuration changes. 162 */ 163 private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { 164 @Override 165 public void onReceive(Context context, Intent intent) { 166 if (!mIsVisible) return; 167 168 mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); 169 mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); 170 } 171 }; 172 173 /** 174 * The setting name that tracks whether we've shown the zoom tutorial. 175 */ 176 private static final String SETTING_NAME_SHOWN_TUTORIAL = "shown_zoom_tutorial"; 177 private static Dialog sTutorialDialog; 178 179 /** When configuration changes, this is called after the UI thread is idle. */ 180 private static final int MSG_POST_CONFIGURATION_CHANGED = 2; 181 /** Used to delay the zoom controller dismissal. */ 182 private static final int MSG_DISMISS_ZOOM_CONTROLS = 3; 183 /** 184 * If setVisible(true) is called and the owner view's window token is null, 185 * we delay the setVisible(true) call until it is not null. 186 */ 187 private static final int MSG_POST_SET_VISIBLE = 4; 188 189 private Handler mHandler = new Handler() { 190 @Override 191 public void handleMessage(Message msg) { 192 switch (msg.what) { 193 case MSG_POST_CONFIGURATION_CHANGED: 194 onPostConfigurationChanged(); 195 break; 196 197 case MSG_DISMISS_ZOOM_CONTROLS: 198 setVisible(false); 199 break; 200 201 case MSG_POST_SET_VISIBLE: 202 if (mOwnerView.getWindowToken() == null) { 203 // Doh, it is still null, just ignore the set visible call 204 Log.e(TAG, 205 "Cannot make the zoom controller visible if the owner view is " + 206 "not attached to a window."); 207 } else { 208 setVisible(true); 209 } 210 break; 211 } 212 213 } 214 }; 215 216 /** 217 * Constructor for the {@link ZoomButtonsController}. 218 * 219 * @param ownerView The view that is being zoomed by the zoom controls. The 220 * zoom controls will be displayed aligned with this view. 221 */ 222 public ZoomButtonsController(View ownerView) { 223 mContext = ownerView.getContext(); 224 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 225 mOwnerView = ownerView; 226 227 mTouchPaddingScaledSq = (int) 228 (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density); 229 mTouchPaddingScaledSq *= mTouchPaddingScaledSq; 230 231 mContainer = createContainer(); 232 } 233 234 /** 235 * Whether to enable the zoom in control. 236 * 237 * @param enabled Whether to enable the zoom in control. 238 */ 239 public void setZoomInEnabled(boolean enabled) { 240 mControls.setIsZoomInEnabled(enabled); 241 } 242 243 /** 244 * Whether to enable the zoom out control. 245 * 246 * @param enabled Whether to enable the zoom out control. 247 */ 248 public void setZoomOutEnabled(boolean enabled) { 249 mControls.setIsZoomOutEnabled(enabled); 250 } 251 252 /** 253 * Sets the delay between zoom callbacks as the user holds a zoom button. 254 * 255 * @param speed The delay in milliseconds between zoom callbacks. 256 */ 257 public void setZoomSpeed(long speed) { 258 mControls.setZoomSpeed(speed); 259 } 260 261 private FrameLayout createContainer() { 262 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 263 // Controls are positioned BOTTOM | CENTER with respect to the owner view. 264 lp.gravity = Gravity.TOP | Gravity.LEFT; 265 lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE | 266 LayoutParams.FLAG_NOT_FOCUSABLE | 267 LayoutParams.FLAG_LAYOUT_NO_LIMITS; 268 lp.height = LayoutParams.WRAP_CONTENT; 269 lp.width = LayoutParams.FILL_PARENT; 270 lp.type = LayoutParams.TYPE_APPLICATION_PANEL; 271 lp.format = PixelFormat.TRANSPARENT; 272 lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons; 273 mContainerLayoutParams = lp; 274 275 FrameLayout container = new Container(mContext); 276 container.setLayoutParams(lp); 277 container.setMeasureAllChildren(true); 278 279 LayoutInflater inflater = (LayoutInflater) mContext 280 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 281 inflater.inflate(com.android.internal.R.layout.zoom_container, container); 282 283 mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls); 284 mControls.setOnZoomInClickListener(new OnClickListener() { 285 public void onClick(View v) { 286 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 287 if (mCallback != null) mCallback.onZoom(true); 288 } 289 }); 290 mControls.setOnZoomOutClickListener(new OnClickListener() { 291 public void onClick(View v) { 292 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 293 if (mCallback != null) mCallback.onZoom(false); 294 } 295 }); 296 297 return container; 298 } 299 300 /** 301 * Sets the {@link OnZoomListener} listener that receives callbacks to zoom. 302 * 303 * @param listener The listener that will be told to zoom. 304 */ 305 public void setOnZoomListener(OnZoomListener listener) { 306 mCallback = listener; 307 } 308 309 /** 310 * Sets whether the zoom controls should be focusable. If the controls are 311 * focusable, then trackball and arrow key interactions are possible. 312 * Otherwise, only touch interactions are possible. 313 * 314 * @param focusable Whether the zoom controls should be focusable. 315 */ 316 public void setFocusable(boolean focusable) { 317 int oldFlags = mContainerLayoutParams.flags; 318 if (focusable) { 319 mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; 320 } else { 321 mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE; 322 } 323 324 if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) { 325 mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); 326 } 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 */ 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 */ 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 */ 426 public ViewGroup getContainer() { 427 return mContainer; 428 } 429 430 private void dismissControlsDelayed(int delay) { 431 mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS); 432 mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay); 433 } 434 435 /** 436 * Should be called by the client for each event belonging to the second tap 437 * (the down, move, up, and/or cancel events). 438 * 439 * @param event The event belonging to the second tap. 440 * @return Whether the event was consumed. 441 */ 442 public boolean handleDoubleTapEvent(MotionEvent event) { 443 int action = event.getAction(); 444 445 if (action == MotionEvent.ACTION_DOWN) { 446 int x = (int) event.getX(); 447 int y = (int) event.getY(); 448 449 /* 450 * This class will consume all events in the second tap (down, 451 * move(s), up). But, the owner already got the second tap's down, 452 * so cancel that. Do this before setVisible, since that call 453 * will set us as a touch listener. 454 */ 455 MotionEvent cancelEvent = MotionEvent.obtain(event.getDownTime(), 456 SystemClock.elapsedRealtime(), 457 MotionEvent.ACTION_CANCEL, 0, 0, 0); 458 mOwnerView.dispatchTouchEvent(cancelEvent); 459 cancelEvent.recycle(); 460 461 setVisible(true); 462 centerPoint(x, y); 463 mIsSecondTapDown = true; 464 } 465 466 return true; 467 } 468 469 private void refreshPositioningVariables() { 470 // Position the zoom controls on the bottom of the owner view. 471 int ownerHeight = mOwnerView.getHeight(); 472 int ownerWidth = mOwnerView.getWidth(); 473 // The gap between the top of the owner and the top of the container 474 int containerOwnerYOffset = ownerHeight - mContainer.getHeight(); 475 476 // Calculate the owner view's bounds 477 mOwnerView.getLocationOnScreen(mOwnerViewRawLocation); 478 mContainerRawLocation[0] = mOwnerViewRawLocation[0]; 479 mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset; 480 481 int[] ownerViewWindowLoc = mTempIntArray; 482 mOwnerView.getLocationInWindow(ownerViewWindowLoc); 483 484 // lp.x and lp.y should be relative to the owner's window top-left 485 mContainerLayoutParams.x = ownerViewWindowLoc[0]; 486 mContainerLayoutParams.width = ownerWidth; 487 mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset; 488 if (mIsVisible) { 489 mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); 490 } 491 492 } 493 494 /** 495 * Centers the point (in owner view's coordinates). 496 */ 497 private void centerPoint(int x, int y) { 498 if (mCallback != null) { 499 mCallback.onCenter(x, y); 500 } 501 } 502 503 /* This will only be called when the container has focus. */ 504 private boolean onContainerKey(KeyEvent event) { 505 int keyCode = event.getKeyCode(); 506 if (isInterestingKey(keyCode)) { 507 508 if (keyCode == KeyEvent.KEYCODE_BACK) { 509 setVisible(false); 510 } else { 511 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 512 } 513 514 // Let the container handle the key 515 return false; 516 517 } else { 518 519 ViewRoot viewRoot = getOwnerViewRoot(); 520 if (viewRoot != null) { 521 viewRoot.dispatchKey(event); 522 } 523 524 // We gave the key to the owner, don't let the container handle this key 525 return true; 526 } 527 } 528 529 private boolean isInterestingKey(int keyCode) { 530 switch (keyCode) { 531 case KeyEvent.KEYCODE_DPAD_CENTER: 532 case KeyEvent.KEYCODE_DPAD_UP: 533 case KeyEvent.KEYCODE_DPAD_DOWN: 534 case KeyEvent.KEYCODE_DPAD_LEFT: 535 case KeyEvent.KEYCODE_DPAD_RIGHT: 536 case KeyEvent.KEYCODE_ENTER: 537 case KeyEvent.KEYCODE_BACK: 538 return true; 539 default: 540 return false; 541 } 542 } 543 544 private ViewRoot getOwnerViewRoot() { 545 View rootViewOfOwner = mOwnerView.getRootView(); 546 if (rootViewOfOwner == null) { 547 return null; 548 } 549 550 ViewParent parentOfRootView = rootViewOfOwner.getParent(); 551 if (parentOfRootView instanceof ViewRoot) { 552 return (ViewRoot) parentOfRootView; 553 } else { 554 return null; 555 } 556 } 557 558 /** 559 * @hide The ZoomButtonsController implements the OnTouchListener, but this 560 * does not need to be shown in its public API. 561 */ 562 public boolean onTouch(View v, MotionEvent event) { 563 int action = event.getAction(); 564 565 // Consume all events during the second-tap interaction (down, move, up/cancel) 566 boolean consumeEvent = mIsSecondTapDown; 567 if ((action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL)) { 568 // The second tap can no longer be down 569 mIsSecondTapDown = false; 570 } 571 572 if (mReleaseTouchListenerOnUp) { 573 // The controls were dismissed but we need to throw away all events until the up 574 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 575 mOwnerView.setOnTouchListener(null); 576 setTouchTargetView(null); 577 mReleaseTouchListenerOnUp = false; 578 } 579 580 // Eat this event 581 return true; 582 } 583 584 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 585 586 View targetView = mTouchTargetView; 587 588 switch (action) { 589 case MotionEvent.ACTION_DOWN: 590 targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY()); 591 setTouchTargetView(targetView); 592 break; 593 594 case MotionEvent.ACTION_UP: 595 case MotionEvent.ACTION_CANCEL: 596 setTouchTargetView(null); 597 break; 598 } 599 600 if (targetView != null) { 601 // The upperleft corner of the target view in raw coordinates 602 int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0]; 603 int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1]; 604 605 MotionEvent containerEvent = MotionEvent.obtain(event); 606 // Convert the motion event into the target view's coordinates (from 607 // owner view's coordinates) 608 containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX, 609 mOwnerViewRawLocation[1] - targetViewRawY); 610 /* Disallow negative coordinates (which can occur due to 611 * ZOOM_CONTROLS_TOUCH_PADDING) */ 612 if (containerEvent.getX() < 0) { 613 containerEvent.offsetLocation(-containerEvent.getX(), 0); 614 } 615 if (containerEvent.getY() < 0) { 616 containerEvent.offsetLocation(0, -containerEvent.getY()); 617 } 618 boolean retValue = targetView.dispatchTouchEvent(containerEvent); 619 containerEvent.recycle(); 620 return retValue || consumeEvent; 621 622 } else { 623 return consumeEvent; 624 } 625 } 626 627 private void setTouchTargetView(View view) { 628 mTouchTargetView = view; 629 if (view != null) { 630 view.getLocationInWindow(mTouchTargetWindowLocation); 631 } 632 } 633 634 /** 635 * Returns the View that should receive a touch at the given coordinates. 636 * 637 * @param rawX The raw X. 638 * @param rawY The raw Y. 639 * @return The view that should receive the touches, or null if there is not one. 640 */ 641 private View findViewForTouch(int rawX, int rawY) { 642 // Reverse order so the child drawn on top gets first dibs. 643 int containerCoordsX = rawX - mContainerRawLocation[0]; 644 int containerCoordsY = rawY - mContainerRawLocation[1]; 645 Rect frame = mTempRect; 646 647 View closestChild = null; 648 int closestChildDistanceSq = Integer.MAX_VALUE; 649 650 for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { 651 View child = mContainer.getChildAt(i); 652 if (child.getVisibility() != View.VISIBLE) { 653 continue; 654 } 655 656 child.getHitRect(frame); 657 if (frame.contains(containerCoordsX, containerCoordsY)) { 658 return child; 659 } 660 661 int distanceX = Math.min(Math.abs(frame.left - containerCoordsX), 662 Math.abs(containerCoordsX - frame.right)); 663 int distanceY = Math.min(Math.abs(frame.top - containerCoordsY), 664 Math.abs(containerCoordsY - frame.bottom)); 665 int distanceSq = distanceX * distanceX + distanceY * distanceY; 666 667 if ((distanceSq < mTouchPaddingScaledSq) && 668 (distanceSq < closestChildDistanceSq)) { 669 closestChild = child; 670 closestChildDistanceSq = distanceSq; 671 } 672 } 673 674 return closestChild; 675 } 676 677 private void onPostConfigurationChanged() { 678 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); 679 refreshPositioningVariables(); 680 } 681 682 /* 683 * This is static so Activities can call this instead of the Views 684 * (Activities usually do not have a reference to the ZoomButtonsController 685 * instance.) 686 */ 687 /** 688 * Shows a "tutorial" (some text) to the user teaching her the new zoom 689 * invocation method. Must call from the main thread. 690 * <p> 691 * It checks the global system setting to ensure this has not been seen 692 * before. Furthermore, if the application does not have privilege to write 693 * to the system settings, it will store this bit locally in a shared 694 * preference. 695 * 696 * @hide This should only be used by our main apps--browser, maps, and 697 * gallery 698 */ 699 public static void showZoomTutorialOnce(Context context) { 700 701 // TODO: remove this code, but to hit the weekend build, just never show 702 if (true) return; 703 704 ContentResolver cr = context.getContentResolver(); 705 if (Settings.System.getInt(cr, SETTING_NAME_SHOWN_TUTORIAL, 0) == 1) { 706 return; 707 } 708 709 SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); 710 if (sp.getInt(SETTING_NAME_SHOWN_TUTORIAL, 0) == 1) { 711 return; 712 } 713 714 if (sTutorialDialog != null && sTutorialDialog.isShowing()) { 715 sTutorialDialog.dismiss(); 716 } 717 718 LayoutInflater layoutInflater = 719 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 720 TextView textView = (TextView) layoutInflater.inflate( 721 com.android.internal.R.layout.alert_dialog_simple_text, null) 722 .findViewById(android.R.id.text1); 723 textView.setText(com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short); 724 725 sTutorialDialog = new AlertDialog.Builder(context) 726 .setView(textView) 727 .setIcon(0) 728 .create(); 729 730 Window window = sTutorialDialog.getWindow(); 731 window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 732 window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND | 733 WindowManager.LayoutParams.FLAG_BLUR_BEHIND); 734 window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 735 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); 736 737 sTutorialDialog.show(); 738 } 739 740 /** @hide Should only be used by Android platform apps */ 741 public static void finishZoomTutorial(Context context, boolean userNotified) { 742 if (sTutorialDialog == null) return; 743 744 sTutorialDialog.dismiss(); 745 sTutorialDialog = null; 746 747 // Record that they have seen the tutorial 748 if (userNotified) { 749 try { 750 Settings.System.putInt(context.getContentResolver(), SETTING_NAME_SHOWN_TUTORIAL, 751 1); 752 } catch (SecurityException e) { 753 /* 754 * The app does not have permission to clear this global flag, make 755 * sure the user does not see the message when he comes back to this 756 * same app at least. 757 */ 758 SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); 759 sp.edit().putInt(SETTING_NAME_SHOWN_TUTORIAL, 1).commit(); 760 } 761 } 762 } 763 764 /** @hide Should only be used by Android platform apps */ 765 public void finishZoomTutorial() { 766 finishZoomTutorial(mContext, true); 767 } 768 769 /** @hide Should only be used only be WebView and MapView */ 770 public View getDummyZoomControls() { 771 if (mDummyZoomControls == null) { 772 mDummyZoomControls = new InvisibleView(mContext); 773 } 774 return mDummyZoomControls; 775 } 776 777 /** 778 * Interface that will be called when the user performs an interaction that 779 * triggers some action, for example zooming. 780 */ 781 public interface OnZoomListener { 782 /** 783 * Called when the given point should be centered. The point will be in 784 * owner view coordinates. 785 * 786 * @param x The x of the point. 787 * @param y The y of the point. 788 */ 789 void onCenter(int x, int y); 790 791 /** 792 * Called when the zoom controls' visibility changes. 793 * 794 * @param visible Whether the zoom controls are visible. 795 */ 796 void onVisibilityChanged(boolean visible); 797 798 /** 799 * Called when the owner view needs to be zoomed. 800 * 801 * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out. 802 */ 803 void onZoom(boolean zoomIn); 804 } 805 806 private class Container extends FrameLayout { 807 public Container(Context context) { 808 super(context); 809 } 810 811 /* 812 * Need to override this to intercept the key events. Otherwise, we 813 * would attach a key listener to the container but its superclass 814 * ViewGroup gives it to the focused View instead of calling the key 815 * listener, and so we wouldn't get the events. 816 */ 817 @Override 818 public boolean dispatchKeyEvent(KeyEvent event) { 819 return onContainerKey(event) ? true : super.dispatchKeyEvent(event); 820 } 821 } 822 823 /** 824 * An InvisibleView is an invisible, zero-sized View for backwards 825 * compatibility 826 */ 827 private final class InvisibleView extends View { 828 829 private InvisibleView(Context context) { 830 super(context); 831 setVisibility(GONE); 832 } 833 834 @Override 835 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 836 setMeasuredDimension(0, 0); 837 } 838 839 @Override 840 public void draw(Canvas canvas) { 841 } 842 843 @Override 844 protected void dispatchDraw(Canvas canvas) { 845 } 846 } 847 848} 849