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