ZoomButtonsController.java revision 16bd9372ee724b78d96fbddd551aa3e42a8451a6
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
17package android.widget;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.graphics.PixelFormat;
24import android.graphics.Rect;
25import android.os.Handler;
26import android.os.Message;
27import android.util.Log;
28import android.view.Gravity;
29import android.view.KeyEvent;
30import android.view.LayoutInflater;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.ViewGroup;
35import android.view.ViewParent;
36import android.view.ViewRoot;
37import android.view.WindowManager;
38import android.view.View.OnClickListener;
39import 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 */
74public 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.LEFT;
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            ViewRoot viewRoot = getOwnerViewRoot();
505            if (viewRoot != null) {
506                viewRoot.dispatchKey(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    private ViewRoot getOwnerViewRoot() {
530        View rootViewOfOwner = mOwnerView.getRootView();
531        if (rootViewOfOwner == null) {
532            return null;
533        }
534
535        ViewParent parentOfRootView = rootViewOfOwner.getParent();
536        if (parentOfRootView instanceof ViewRoot) {
537            return (ViewRoot) parentOfRootView;
538        } else {
539            return null;
540        }
541    }
542
543    /**
544     * @hide The ZoomButtonsController implements the OnTouchListener, but this
545     *       does not need to be shown in its public API.
546     */
547    public boolean onTouch(View v, MotionEvent event) {
548        int action = event.getAction();
549
550        if (event.getPointerCount() > 1) {
551            // ZoomButtonsController doesn't handle mutitouch. Give up control.
552            return false;
553        }
554
555        if (mReleaseTouchListenerOnUp) {
556            // The controls were dismissed but we need to throw away all events until the up
557            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
558                mOwnerView.setOnTouchListener(null);
559                setTouchTargetView(null);
560                mReleaseTouchListenerOnUp = false;
561            }
562
563            // Eat this event
564            return true;
565        }
566
567        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
568
569        View targetView = mTouchTargetView;
570
571        switch (action) {
572            case MotionEvent.ACTION_DOWN:
573                targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
574                setTouchTargetView(targetView);
575                break;
576
577            case MotionEvent.ACTION_UP:
578            case MotionEvent.ACTION_CANCEL:
579                setTouchTargetView(null);
580                break;
581        }
582
583        if (targetView != null) {
584            // The upperleft corner of the target view in raw coordinates
585            int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
586            int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
587
588            MotionEvent containerEvent = MotionEvent.obtain(event);
589            // Convert the motion event into the target view's coordinates (from
590            // owner view's coordinates)
591            containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
592                    mOwnerViewRawLocation[1] - targetViewRawY);
593            /* Disallow negative coordinates (which can occur due to
594             * ZOOM_CONTROLS_TOUCH_PADDING) */
595            // These are floats because we need to potentially offset away this exact amount
596            float containerX = containerEvent.getX();
597            float containerY = containerEvent.getY();
598            if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) {
599                containerEvent.offsetLocation(-containerX, 0);
600            }
601            if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) {
602                containerEvent.offsetLocation(0, -containerY);
603            }
604            boolean retValue = targetView.dispatchTouchEvent(containerEvent);
605            containerEvent.recycle();
606            return retValue;
607
608        } else {
609            return false;
610        }
611    }
612
613    private void setTouchTargetView(View view) {
614        mTouchTargetView = view;
615        if (view != null) {
616            view.getLocationInWindow(mTouchTargetWindowLocation);
617        }
618    }
619
620    /**
621     * Returns the View that should receive a touch at the given coordinates.
622     *
623     * @param rawX The raw X.
624     * @param rawY The raw Y.
625     * @return The view that should receive the touches, or null if there is not one.
626     */
627    private View findViewForTouch(int rawX, int rawY) {
628        // Reverse order so the child drawn on top gets first dibs.
629        int containerCoordsX = rawX - mContainerRawLocation[0];
630        int containerCoordsY = rawY - mContainerRawLocation[1];
631        Rect frame = mTempRect;
632
633        View closestChild = null;
634        int closestChildDistanceSq = Integer.MAX_VALUE;
635
636        for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
637            View child = mContainer.getChildAt(i);
638            if (child.getVisibility() != View.VISIBLE) {
639                continue;
640            }
641
642            child.getHitRect(frame);
643            if (frame.contains(containerCoordsX, containerCoordsY)) {
644                return child;
645            }
646
647            int distanceX;
648            if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) {
649                distanceX = 0;
650            } else {
651                distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
652                    Math.abs(containerCoordsX - frame.right));
653            }
654            int distanceY;
655            if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) {
656                distanceY = 0;
657            } else {
658                distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
659                        Math.abs(containerCoordsY - frame.bottom));
660            }
661            int distanceSq = distanceX * distanceX + distanceY * distanceY;
662
663            if ((distanceSq < mTouchPaddingScaledSq) &&
664                    (distanceSq < closestChildDistanceSq)) {
665                closestChild = child;
666                closestChildDistanceSq = distanceSq;
667            }
668        }
669
670        return closestChild;
671    }
672
673    private void onPostConfigurationChanged() {
674        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
675        refreshPositioningVariables();
676    }
677
678    /**
679     * Interface that will be called when the user performs an interaction that
680     * triggers some action, for example zooming.
681     */
682    public interface OnZoomListener {
683
684        /**
685         * Called when the zoom controls' visibility changes.
686         *
687         * @param visible Whether the zoom controls are visible.
688         */
689        void onVisibilityChanged(boolean visible);
690
691        /**
692         * Called when the owner view needs to be zoomed.
693         *
694         * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
695         */
696        void onZoom(boolean zoomIn);
697    }
698
699    private class Container extends FrameLayout {
700        public Container(Context context) {
701            super(context);
702        }
703
704        /*
705         * Need to override this to intercept the key events. Otherwise, we
706         * would attach a key listener to the container but its superclass
707         * ViewGroup gives it to the focused View instead of calling the key
708         * listener, and so we wouldn't get the events.
709         */
710        @Override
711        public boolean dispatchKeyEvent(KeyEvent event) {
712            return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
713        }
714    }
715
716}
717