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