Home | History | Annotate | Download | only in widget
      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      */
    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      */
    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      */
    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      */
    240     public void setZoomSpeed(long speed) {
    241         mControls.setZoomSpeed(speed);
    242     }
    243 
    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      */
    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      */
    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      */
    318     public boolean isAutoDismissed() {
    319         return mAutoDismissControls;
    320     }
    321 
    322     /**
    323      * Sets whether the zoom controls will be automatically dismissed after
    324      * showing.
    325      */
    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      */
    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      */
    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      */
    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      */
    437     public View getZoomControls() {
    438         return mControls;
    439     }
    440 
    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 
    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. */
    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 
    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      */
    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 
    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      */
    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 
    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          */
    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          */
    684         void onZoom(boolean zoomIn);
    685     }
    686 
    687     private class Container extends FrameLayout {
    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
    699         public boolean dispatchKeyEvent(KeyEvent event) {
    700             return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
    701         }
    702     }
    703 
    704 }
    705