OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / frameworks / base / core / java / android / widget / ZoomButtonsController.java
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      */
202     public ZoomButtonsController(View ownerView) {
203         mContext = ownerView.getContext();
204         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
205         mOwnerView = ownerView;
206
207         mTouchPaddingScaledSq = (int)
208                 (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
209         mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
210
211         mContainer = createContainer();
212     }
213
214     /**
215      * Whether to enable the zoom in control.
216      *
217      * @param enabled Whether to enable the zoom in control.
218      */
219     public void setZoomInEnabled(boolean enabled) {
220         mControls.setIsZoomInEnabled(enabled);
221     }
222
223     /**
224      * Whether to enable the zoom out control.
225      *
226      * @param enabled Whether to enable the zoom out control.
227      */
228     public void setZoomOutEnabled(boolean enabled) {
229         mControls.setIsZoomOutEnabled(enabled);
230     }
231
232     /**
233      * Sets the delay between zoom callbacks as the user holds a zoom button.
234      *
235      * @param speed The delay in milliseconds between zoom callbacks.
236      */
237     public void setZoomSpeed(long speed) {
238         mControls.setZoomSpeed(speed);
239     }
240
241     private FrameLayout createContainer() {
242         LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
243         // Controls are positioned BOTTOM | CENTER with respect to the owner view.
244         lp.gravity = Gravity.TOP | Gravity.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.MATCH_PARENT;
251         lp.type = LayoutParams.TYPE_APPLICATION_PANEL;
252         lp.format = PixelFormat.TRANSLUCENT;
253         lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons;
254         mContainerLayoutParams = lp;
255
256         FrameLayout container = new Container(mContext);
257         container.setLayoutParams(lp);
258         container.setMeasureAllChildren(true);
259
260         LayoutInflater inflater = (LayoutInflater) mContext
261                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
262         inflater.inflate(com.android.internal.R.layout.zoom_container, container);
263
264         mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls);
265         mControls.setOnZoomInClickListener(new OnClickListener() {
266             public void onClick(View v) {
267                 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
268                 if (mCallback != null) mCallback.onZoom(true);
269             }
270         });
271         mControls.setOnZoomOutClickListener(new OnClickListener() {
272             public void onClick(View v) {
273                 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
274                 if (mCallback != null) mCallback.onZoom(false);
275             }
276         });
277
278         return container;
279     }
280
281     /**
282      * Sets the {@link OnZoomListener} listener that receives callbacks to zoom.
283      *
284      * @param listener The listener that will be told to zoom.
285      */
286     public void setOnZoomListener(OnZoomListener listener) {
287         mCallback = listener;
288     }
289
290     /**
291      * Sets whether the zoom controls should be focusable. If the controls are
292      * focusable, then trackball and arrow key interactions are possible.
293      * Otherwise, only touch interactions are possible.
294      *
295      * @param focusable Whether the zoom controls should be focusable.
296      */
297     public void setFocusable(boolean focusable) {
298         int oldFlags = mContainerLayoutParams.flags;
299         if (focusable) {
300             mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
301         } else {
302             mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
303         }
304
305         if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
306             mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
307         }
308     }
309
310     /**
311      * Whether the zoom controls will be automatically dismissed after showing.
312      *
313      * @return Whether the zoom controls will be auto dismissed after showing.
314      */
315     public boolean isAutoDismissed() {
316         return mAutoDismissControls;
317     }
318
319     /**
320      * Sets whether the zoom controls will be automatically dismissed after
321      * showing.
322      */
323     public void setAutoDismissed(boolean autoDismiss) {
324         if (mAutoDismissControls == autoDismiss) return;
325         mAutoDismissControls = autoDismiss;
326     }
327
328     /**
329      * Whether the zoom controls are visible to the user.
330      *
331      * @return Whether the zoom controls are visible to the user.
332      */
333     public boolean isVisible() {
334         return mIsVisible;
335     }
336
337     /**
338      * Sets whether the zoom controls should be visible to the user.
339      *
340      * @param visible Whether the zoom controls should be visible to the user.
341      */
342     public void setVisible(boolean visible) {
343
344         if (visible) {
345             if (mOwnerView.getWindowToken() == null) {
346                 /*
347                  * We need a window token to show ourselves, maybe the owner's
348                  * window hasn't been created yet but it will have been by the
349                  * time the looper is idle, so post the setVisible(true) call.
350                  */
351                 if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
352                     mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
353                 }
354                 return;
355             }
356
357             dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
358         }
359
360         if (mIsVisible == visible) {
361             return;
362         }
363         mIsVisible = visible;
364
365         if (visible) {
366             if (mContainerLayoutParams.token == null) {
367                 mContainerLayoutParams.token = mOwnerView.getWindowToken();
368             }
369
370             mWindowManager.addView(mContainer, mContainerLayoutParams);
371
372             if (mPostedVisibleInitializer == null) {
373                 mPostedVisibleInitializer = new Runnable() {
374                     public void run() {
375                         refreshPositioningVariables();
376
377                         if (mCallback != null) {
378                             mCallback.onVisibilityChanged(true);
379                         }
380                     }
381                 };
382             }
383
384             mHandler.post(mPostedVisibleInitializer);
385
386             // Handle configuration changes when visible
387             mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
388
389             // Steal touches events from the owner
390             mOwnerView.setOnTouchListener(this);
391             mReleaseTouchListenerOnUp = false;
392
393         } else {
394             // Don't want to steal any more touches
395             if (mTouchTargetView != null) {
396                 // We are still stealing the touch events for this touch
397                 // sequence, so release the touch listener later
398                 mReleaseTouchListenerOnUp = true;
399             } else {
400                 mOwnerView.setOnTouchListener(null);
401             }
402
403             // No longer care about configuration changes
404             mContext.unregisterReceiver(mConfigurationChangedReceiver);
405
406             mWindowManager.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      */
425     public ViewGroup getContainer() {
426         return mContainer;
427     }
428
429     /**
430      * Gets the view for the zoom controls.
431      *
432      * @return The zoom controls view.
433      */
434     public View getZoomControls() {
435         return mControls;
436     }
437
438     private void dismissControlsDelayed(int delay) {
439         if (mAutoDismissControls) {
440             mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
441             mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
442         }
443     }
444
445     private void refreshPositioningVariables() {
446         // if the mOwnerView is detached from window then skip.
447         if (mOwnerView.getWindowToken() == null) return;
448
449         // Position the zoom controls on the bottom of the owner view.
450         int ownerHeight = mOwnerView.getHeight();
451         int ownerWidth = mOwnerView.getWidth();
452         // The gap between the top of the owner and the top of the container
453         int containerOwnerYOffset = ownerHeight - mContainer.getHeight();
454
455         // Calculate the owner view's bounds
456         mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
457         mContainerRawLocation[0] = mOwnerViewRawLocation[0];
458         mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
459
460         int[] ownerViewWindowLoc = mTempIntArray;
461         mOwnerView.getLocationInWindow(ownerViewWindowLoc);
462
463         // lp.x and lp.y should be relative to the owner's window top-left
464         mContainerLayoutParams.x = ownerViewWindowLoc[0];
465         mContainerLayoutParams.width = ownerWidth;
466         mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset;
467         if (mIsVisible) {
468             mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
469         }
470
471     }
472
473     /* This will only be called when the container has focus. */
474     private boolean onContainerKey(KeyEvent event) {
475         int keyCode = event.getKeyCode();
476         if (isInterestingKey(keyCode)) {
477
478             if (keyCode == KeyEvent.KEYCODE_BACK) {
479                 if (event.getAction() == KeyEvent.ACTION_DOWN
480                         && event.getRepeatCount() == 0) {
481                     if (mOwnerView != null) {
482                         KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState();
483                         if (ds != null) {
484                             ds.startTracking(event, this);
485                         }
486                     }
487                     return true;
488                 } else if (event.getAction() == KeyEvent.ACTION_UP
489                         && event.isTracking() && !event.isCanceled()) {
490                     setVisible(false);
491                     return true;
492                 }
493                 
494             } else {
495                 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
496             }
497
498             // Let the container handle the key
499             return false;
500
501         } else {
502
503             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
513     private boolean isInterestingKey(int keyCode) {
514         switch (keyCode) {
515             case KeyEvent.KEYCODE_DPAD_CENTER:
516             case KeyEvent.KEYCODE_DPAD_UP:
517             case KeyEvent.KEYCODE_DPAD_DOWN:
518             case KeyEvent.KEYCODE_DPAD_LEFT:
519             case KeyEvent.KEYCODE_DPAD_RIGHT:
520             case KeyEvent.KEYCODE_ENTER:
521             case KeyEvent.KEYCODE_BACK:
522                 return true;
523             default:
524                 return false;
525         }
526     }
527
528     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      */
546     public boolean onTouch(View v, MotionEvent event) {
547         int action = event.getAction();
548
549         if (event.getPointerCount() > 1) {
550             // ZoomButtonsController doesn't handle mutitouch. Give up control.
551             return false;
552         }
553
554         if (mReleaseTouchListenerOnUp) {
555             // The controls were dismissed but we need to throw away all events until the up
556             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
557                 mOwnerView.setOnTouchListener(null);
558                 setTouchTargetView(null);
559                 mReleaseTouchListenerOnUp = false;
560             }
561
562             // Eat this event
563             return true;
564         }
565
566         dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
567
568         View targetView = mTouchTargetView;
569
570         switch (action) {
571             case MotionEvent.ACTION_DOWN:
572                 targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
573                 setTouchTargetView(targetView);
574                 break;
575
576             case MotionEvent.ACTION_UP:
577             case MotionEvent.ACTION_CANCEL:
578                 setTouchTargetView(null);
579                 break;
580         }
581
582         if (targetView != null) {
583             // The upperleft corner of the target view in raw coordinates
584             int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
585             int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
586
587             MotionEvent containerEvent = MotionEvent.obtain(event);
588             // Convert the motion event into the target view's coordinates (from
589             // owner view's coordinates)
590             containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
591                     mOwnerViewRawLocation[1] - targetViewRawY);
592             /* Disallow negative coordinates (which can occur due to
593              * ZOOM_CONTROLS_TOUCH_PADDING) */
594             // These are floats because we need to potentially offset away this exact amount
595             float containerX = containerEvent.getX();
596             float containerY = containerEvent.getY();
597             if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) {
598                 containerEvent.offsetLocation(-containerX, 0);
599             }
600             if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) {
601                 containerEvent.offsetLocation(0, -containerY);
602             }
603             boolean retValue = targetView.dispatchTouchEvent(containerEvent);
604             containerEvent.recycle();
605             return retValue;
606
607         } else {
608             return false;
609         }
610     }
611
612     private void setTouchTargetView(View view) {
613         mTouchTargetView = view;
614         if (view != null) {
615             view.getLocationInWindow(mTouchTargetWindowLocation);
616         }
617     }
618
619     /**
620      * Returns the View that should receive a touch at the given coordinates.
621      *
622      * @param rawX The raw X.
623      * @param rawY The raw Y.
624      * @return The view that should receive the touches, or null if there is not one.
625      */
626     private View findViewForTouch(int rawX, int rawY) {
627         // Reverse order so the child drawn on top gets first dibs.
628         int containerCoordsX = rawX - mContainerRawLocation[0];
629         int containerCoordsY = rawY - mContainerRawLocation[1];
630         Rect frame = mTempRect;
631
632         View closestChild = null;
633         int closestChildDistanceSq = Integer.MAX_VALUE;
634
635         for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
636             View child = mContainer.getChildAt(i);
637             if (child.getVisibility() != View.VISIBLE) {
638                 continue;
639             }
640
641             child.getHitRect(frame);
642             if (frame.contains(containerCoordsX, containerCoordsY)) {
643                 return child;
644             }
645
646             int distanceX;
647             if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) {
648                 distanceX = 0;
649             } else {
650                 distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
651                     Math.abs(containerCoordsX - frame.right));
652             }
653             int distanceY;
654             if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) {
655                 distanceY = 0;
656             } else {
657                 distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
658                         Math.abs(containerCoordsY - frame.bottom));
659             }
660             int distanceSq = distanceX * distanceX + distanceY * distanceY;
661
662             if ((distanceSq < mTouchPaddingScaledSq) &&
663                     (distanceSq < closestChildDistanceSq)) {
664                 closestChild = child;
665                 closestChildDistanceSq = distanceSq;
666             }
667         }
668
669         return closestChild;
670     }
671
672     private void onPostConfigurationChanged() {
673         dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
674         refreshPositioningVariables();
675     }
676
677     /**
678      * Interface that will be called when the user performs an interaction that
679      * triggers some action, for example zooming.
680      */
681     public interface OnZoomListener {
682
683         /**
684          * Called when the zoom controls' visibility changes.
685          *
686          * @param visible Whether the zoom controls are visible.
687          */
688         void onVisibilityChanged(boolean visible);
689
690         /**
691          * Called when the owner view needs to be zoomed.
692          *
693          * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
694          */
695         void onZoom(boolean zoomIn);
696     }
697
698     private class Container extends FrameLayout {
699         public Container(Context context) {
700             super(context);
701         }
702
703         /*
704          * Need to override this to intercept the key events. Otherwise, we
705          * would attach a key listener to the container but its superclass
706          * ViewGroup gives it to the focused View instead of calling the key
707          * listener, and so we wouldn't get the events.
708          */
709         @Override
710         public boolean dispatchKeyEvent(KeyEvent event) {
711             return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
712         }
713     }
714
715 }