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.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      */
    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      */
    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      */
    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      */
    238     public void setZoomSpeed(long speed) {
    239         mControls.setZoomSpeed(speed);
    240     }
    241 
    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      */
    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      */
    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      */
    316     public boolean isAutoDismissed() {
    317         return mAutoDismissControls;
    318     }
    319 
    320     /**
    321      * Sets whether the zoom controls will be automatically dismissed after
    322      * showing.
    323      */
    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      */
    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      */
    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      */
    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      */
    435     public View getZoomControls() {
    436         return mControls;
    437     }
    438 
    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 
    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. */
    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 
    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      */
    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 
    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      */
    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 
    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          */
    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          */
    682         void onZoom(boolean zoomIn);
    683     }
    684 
    685     private class Container extends FrameLayout {
    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
    697         public boolean dispatchKeyEvent(KeyEvent event) {
    698             return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
    699         }
    700     }
    701 
    702 }
    703