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.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      */
    202     public ZoomButtonsController(View ownerView) {
    203         mContext = ownerView.getContext();
    204         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    205         mOwnerView = ownerView;
    206 
    207         mTouchPaddingScaledSq = (int)
    208                 (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
    209         mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
    210 
    211         mContainer = createContainer();
    212     }
    213 
    214     /**
    215      * Whether to enable the zoom in control.
    216      *
    217      * @param enabled Whether to enable the zoom in control.
    218      */
    219     public void setZoomInEnabled(boolean enabled) {
    220         mControls.setIsZoomInEnabled(enabled);
    221     }
    222 
    223     /**
    224      * Whether to enable the zoom out control.
    225      *
    226      * @param enabled Whether to enable the zoom out control.
    227      */
    228     public void setZoomOutEnabled(boolean enabled) {
    229         mControls.setIsZoomOutEnabled(enabled);
    230     }
    231 
    232     /**
    233      * Sets the delay between zoom callbacks as the user holds a zoom button.
    234      *
    235      * @param speed The delay in milliseconds between zoom callbacks.
    236      */
    237     public void setZoomSpeed(long speed) {
    238         mControls.setZoomSpeed(speed);
    239     }
    240 
    241     private FrameLayout createContainer() {
    242         LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    243         // Controls are positioned BOTTOM | CENTER with respect to the owner view.
    244         lp.gravity = Gravity.TOP | Gravity.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      */
    286     public void setOnZoomListener(OnZoomListener listener) {
    287         mCallback = listener;
    288     }
    289 
    290     /**
    291      * Sets whether the zoom controls should be focusable. If the controls are
    292      * focusable, then trackball and arrow key interactions are possible.
    293      * Otherwise, only touch interactions are possible.
    294      *
    295      * @param focusable Whether the zoom controls should be focusable.
    296      */
    297     public void setFocusable(boolean focusable) {
    298         int oldFlags = mContainerLayoutParams.flags;
    299         if (focusable) {
    300             mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
    301         } else {
    302             mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
    303         }
    304 
    305         if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
    306             mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
    307         }
    308     }
    309 
    310     /**
    311      * Whether the zoom controls will be automatically dismissed after showing.
    312      *
    313      * @return Whether the zoom controls will be auto dismissed after showing.
    314      */
    315     public boolean isAutoDismissed() {
    316         return mAutoDismissControls;
    317     }
    318 
    319     /**
    320      * Sets whether the zoom controls will be automatically dismissed after
    321      * showing.
    322      */
    323     public void setAutoDismissed(boolean autoDismiss) {
    324         if (mAutoDismissControls == autoDismiss) return;
    325         mAutoDismissControls = autoDismiss;
    326     }
    327 
    328     /**
    329      * Whether the zoom controls are visible to the user.
    330      *
    331      * @return Whether the zoom controls are visible to the user.
    332      */
    333     public boolean isVisible() {
    334         return mIsVisible;
    335     }
    336 
    337     /**
    338      * Sets whether the zoom controls should be visible to the user.
    339      *
    340      * @param visible Whether the zoom controls should be visible to the user.
    341      */
    342     public void setVisible(boolean visible) {
    343 
    344         if (visible) {
    345             if (mOwnerView.getWindowToken() == null) {
    346                 /*
    347                  * We need a window token to show ourselves, maybe the owner's
    348                  * window hasn't been created yet but it will have been by the
    349                  * time the looper is idle, so post the setVisible(true) call.
    350                  */
    351                 if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
    352                     mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
    353                 }
    354                 return;
    355             }
    356 
    357             dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
    358         }
    359 
    360         if (mIsVisible == visible) {
    361             return;
    362         }
    363         mIsVisible = visible;
    364 
    365         if (visible) {
    366             if (mContainerLayoutParams.token == null) {
    367                 mContainerLayoutParams.token = mOwnerView.getWindowToken();
    368             }
    369 
    370             mWindowManager.addView(mContainer, mContainerLayoutParams);
    371 
    372             if (mPostedVisibleInitializer == null) {
    373                 mPostedVisibleInitializer = new Runnable() {
    374                     public void run() {
    375                         refreshPositioningVariables();
    376 
    377                         if (mCallback != null) {
    378                             mCallback.onVisibilityChanged(true);
    379                         }
    380                     }
    381                 };
    382             }
    383 
    384             mHandler.post(mPostedVisibleInitializer);
    385 
    386             // Handle configuration changes when visible
    387             mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
    388 
    389             // Steal touches events from the owner
    390             mOwnerView.setOnTouchListener(this);
    391             mReleaseTouchListenerOnUp = false;
    392 
    393         } else {
    394             // Don't want to steal any more touches
    395             if (mTouchTargetView != null) {
    396                 // We are still stealing the touch events for this touch
    397                 // sequence, so release the touch listener later
    398                 mReleaseTouchListenerOnUp = true;
    399             } else {
    400                 mOwnerView.setOnTouchListener(null);
    401             }
    402 
    403             // No longer care about configuration changes
    404             mContext.unregisterReceiver(mConfigurationChangedReceiver);
    405 
    406             mWindowManager.removeView(mContainer);
    407             mHandler.removeCallbacks(mPostedVisibleInitializer);
    408 
    409             if (mCallback != null) {
    410                 mCallback.onVisibilityChanged(false);
    411             }
    412         }
    413 
    414     }
    415 
    416     /**
    417      * Gets the container that is the parent of the zoom controls.
    418      * <p>
    419      * The client can add other views to this container to link them with the
    420      * zoom controls.
    421      *
    422      * @return The container of the zoom controls. It will be a layout that
    423      *         respects the gravity of a child's layout parameters.
    424      */
    425     public ViewGroup getContainer() {
    426         return mContainer;
    427     }
    428 
    429     /**
    430      * Gets the view for the zoom controls.
    431      *
    432      * @return The zoom controls view.
    433      */
    434     public View getZoomControls() {
    435         return mControls;
    436     }
    437 
    438     private void dismissControlsDelayed(int delay) {
    439         if (mAutoDismissControls) {
    440             mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
    441             mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
    442         }
    443     }
    444 
    445     private void refreshPositioningVariables() {
    446         // if the mOwnerView is detached from window then skip.
    447         if (mOwnerView.getWindowToken() == null) return;
    448 
    449         // Position the zoom controls on the bottom of the owner view.
    450         int ownerHeight = mOwnerView.getHeight();
    451         int ownerWidth = mOwnerView.getWidth();
    452         // The gap between the top of the owner and the top of the container
    453         int containerOwnerYOffset = ownerHeight - mContainer.getHeight();
    454 
    455         // Calculate the owner view's bounds
    456         mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
    457         mContainerRawLocation[0] = mOwnerViewRawLocation[0];
    458         mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
    459 
    460         int[] ownerViewWindowLoc = mTempIntArray;
    461         mOwnerView.getLocationInWindow(ownerViewWindowLoc);
    462 
    463         // lp.x and lp.y should be relative to the owner's window top-left
    464         mContainerLayoutParams.x = ownerViewWindowLoc[0];
    465         mContainerLayoutParams.width = ownerWidth;
    466         mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset;
    467         if (mIsVisible) {
    468             mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
    469         }
    470 
    471     }
    472 
    473     /* This will only be called when the container has focus. */
    474     private boolean onContainerKey(KeyEvent event) {
    475         int keyCode = event.getKeyCode();
    476         if (isInterestingKey(keyCode)) {
    477 
    478             if (keyCode == KeyEvent.KEYCODE_BACK) {
    479                 if (event.getAction() == KeyEvent.ACTION_DOWN
    480                         && event.getRepeatCount() == 0) {
    481                     if (mOwnerView != null) {
    482                         KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState();
    483                         if (ds != null) {
    484                             ds.startTracking(event, this);
    485                         }
    486                     }
    487                     return true;
    488                 } else if (event.getAction() == KeyEvent.ACTION_UP
    489                         && event.isTracking() && !event.isCanceled()) {
    490                     setVisible(false);
    491                     return true;
    492                 }
    493 
    494             } else {
    495                 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
    496             }
    497 
    498             // Let the container handle the key
    499             return false;
    500 
    501         } else {
    502 
    503             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 
    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      */
    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 
    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      */
    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 
    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          */
    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          */
    681         void onZoom(boolean zoomIn);
    682     }
    683 
    684     private class Container extends FrameLayout {
    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
    696         public boolean dispatchKeyEvent(KeyEvent event) {
    697             return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
    698         }
    699     }
    700 
    701 }
    702