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