| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.widget; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.ViewRootImpl; |
| import android.view.WindowManager; |
| import android.view.View.OnClickListener; |
| import android.view.WindowManager.LayoutParams; |
| |
| /* |
| * Implementation notes: |
| * - The zoom controls are displayed in their own window. |
| * (Easier for the client and better performance) |
| * - This window is never touchable, and by default is not focusable. |
| * Its rect is quite big (fills horizontally) but has empty space between the |
| * edges and center. Touches there should be given to the owner. Instead of |
| * having the window touchable and dispatching these empty touch events to the |
| * owner, we set the window to not touchable and steal events from owner |
| * via onTouchListener. |
| * - To make the buttons clickable, it attaches an OnTouchListener to the owner |
| * view and does the hit detection locally (attaches when visible, detaches when invisible). |
| * - When it is focusable, it forwards uninteresting events to the owner view's |
| * view hierarchy. |
| */ |
| /** |
| * The {@link ZoomButtonsController} handles showing and hiding the zoom |
| * controls and positioning it relative to an owner view. It also gives the |
| * client access to the zoom controls container, allowing for additional |
| * accessory buttons to be shown in the zoom controls window. |
| * <p> |
| * Typically, clients should call {@link #setVisible(boolean) setVisible(true)} |
| * on a touch down or move (no need to call {@link #setVisible(boolean) |
| * setVisible(false)} since it will time out on its own). Also, whenever the |
| * owner cannot be zoomed further, the client should update |
| * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}. |
| * <p> |
| * If you are using this with a custom View, please call |
| * {@link #setVisible(boolean) setVisible(false)} from |
| * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged} |
| * when <code>visibility != View.VISIBLE</code>. |
| * |
| */ |
| public class ZoomButtonsController implements View.OnTouchListener { |
| |
| private static final String TAG = "ZoomButtonsController"; |
| |
| private static final int ZOOM_CONTROLS_TIMEOUT = |
| (int) ViewConfiguration.getZoomControlsTimeout(); |
| |
| private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20; |
| private int mTouchPaddingScaledSq; |
| |
| private final Context mContext; |
| private final WindowManager mWindowManager; |
| private boolean mAutoDismissControls = true; |
| |
| /** |
| * The view that is being zoomed by this zoom controller. |
| */ |
| private final View mOwnerView; |
| |
| /** |
| * The location of the owner view on the screen. This is recalculated |
| * each time the zoom controller is shown. |
| */ |
| private final int[] mOwnerViewRawLocation = new int[2]; |
| |
| /** |
| * The container that is added as a window. |
| */ |
| private final FrameLayout mContainer; |
| private LayoutParams mContainerLayoutParams; |
| private final int[] mContainerRawLocation = new int[2]; |
| |
| private ZoomControls mControls; |
| |
| /** |
| * The view (or null) that should receive touch events. This will get set if |
| * the touch down hits the container. It will be reset on the touch up. |
| */ |
| private View mTouchTargetView; |
| /** |
| * The {@link #mTouchTargetView}'s location in window, set on touch down. |
| */ |
| private final int[] mTouchTargetWindowLocation = new int[2]; |
| |
| /** |
| * If the zoom controller is dismissed but the user is still in a touch |
| * interaction, we set this to true. This will ignore all touch events until |
| * up/cancel, and then set the owner's touch listener to null. |
| * <p> |
| * Otherwise, the owner view would get mismatched events (i.e., touch move |
| * even though it never got the touch down.) |
| */ |
| private boolean mReleaseTouchListenerOnUp; |
| |
| /** Whether the container has been added to the window manager. */ |
| private boolean mIsVisible; |
| |
| private final Rect mTempRect = new Rect(); |
| private final int[] mTempIntArray = new int[2]; |
| |
| private OnZoomListener mCallback; |
| |
| /** |
| * When showing the zoom, we add the view as a new window. However, there is |
| * logic that needs to know the size of the zoom which is determined after |
| * it's laid out. Therefore, we must post this logic onto the UI thread so |
| * it will be exceuted AFTER the layout. This is the logic. |
| */ |
| private Runnable mPostedVisibleInitializer; |
| |
| private final IntentFilter mConfigurationChangedFilter = |
| new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); |
| |
| /** |
| * Needed to reposition the zoom controls after configuration changes. |
| */ |
| private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (!mIsVisible) return; |
| |
| mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); |
| mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); |
| } |
| }; |
| |
| /** When configuration changes, this is called after the UI thread is idle. */ |
| private static final int MSG_POST_CONFIGURATION_CHANGED = 2; |
| /** Used to delay the zoom controller dismissal. */ |
| private static final int MSG_DISMISS_ZOOM_CONTROLS = 3; |
| /** |
| * If setVisible(true) is called and the owner view's window token is null, |
| * we delay the setVisible(true) call until it is not null. |
| */ |
| private static final int MSG_POST_SET_VISIBLE = 4; |
| |
| private final Handler mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_POST_CONFIGURATION_CHANGED: |
| onPostConfigurationChanged(); |
| break; |
| |
| case MSG_DISMISS_ZOOM_CONTROLS: |
| setVisible(false); |
| break; |
| |
| case MSG_POST_SET_VISIBLE: |
| if (mOwnerView.getWindowToken() == null) { |
| // Doh, it is still null, just ignore the set visible call |
| Log.e(TAG, |
| "Cannot make the zoom controller visible if the owner view is " + |
| "not attached to a window."); |
| } else { |
| setVisible(true); |
| } |
| break; |
| } |
| |
| } |
| }; |
| |
| /** |
| * Constructor for the {@link ZoomButtonsController}. |
| * |
| * @param ownerView The view that is being zoomed by the zoom controls. The |
| * zoom controls will be displayed aligned with this view. |
| */ |
| public ZoomButtonsController(View ownerView) { |
| mContext = ownerView.getContext(); |
| mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); |
| mOwnerView = ownerView; |
| |
| mTouchPaddingScaledSq = (int) |
| (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density); |
| mTouchPaddingScaledSq *= mTouchPaddingScaledSq; |
| |
| mContainer = createContainer(); |
| } |
| |
| /** |
| * Whether to enable the zoom in control. |
| * |
| * @param enabled Whether to enable the zoom in control. |
| */ |
| public void setZoomInEnabled(boolean enabled) { |
| mControls.setIsZoomInEnabled(enabled); |
| } |
| |
| /** |
| * Whether to enable the zoom out control. |
| * |
| * @param enabled Whether to enable the zoom out control. |
| */ |
| public void setZoomOutEnabled(boolean enabled) { |
| mControls.setIsZoomOutEnabled(enabled); |
| } |
| |
| /** |
| * Sets the delay between zoom callbacks as the user holds a zoom button. |
| * |
| * @param speed The delay in milliseconds between zoom callbacks. |
| */ |
| public void setZoomSpeed(long speed) { |
| mControls.setZoomSpeed(speed); |
| } |
| |
| private FrameLayout createContainer() { |
| LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| // Controls are positioned BOTTOM | CENTER with respect to the owner view. |
| lp.gravity = Gravity.TOP | Gravity.START; |
| lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE | |
| LayoutParams.FLAG_NOT_FOCUSABLE | |
| LayoutParams.FLAG_LAYOUT_NO_LIMITS | |
| LayoutParams.FLAG_ALT_FOCUSABLE_IM; |
| lp.height = LayoutParams.WRAP_CONTENT; |
| lp.width = LayoutParams.MATCH_PARENT; |
| lp.type = LayoutParams.TYPE_APPLICATION_PANEL; |
| lp.format = PixelFormat.TRANSLUCENT; |
| lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons; |
| mContainerLayoutParams = lp; |
| |
| FrameLayout container = new Container(mContext); |
| container.setLayoutParams(lp); |
| container.setMeasureAllChildren(true); |
| |
| LayoutInflater inflater = (LayoutInflater) mContext |
| .getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| inflater.inflate(com.android.internal.R.layout.zoom_container, container); |
| |
| mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls); |
| mControls.setOnZoomInClickListener(new OnClickListener() { |
| public void onClick(View v) { |
| dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); |
| if (mCallback != null) mCallback.onZoom(true); |
| } |
| }); |
| mControls.setOnZoomOutClickListener(new OnClickListener() { |
| public void onClick(View v) { |
| dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); |
| if (mCallback != null) mCallback.onZoom(false); |
| } |
| }); |
| |
| return container; |
| } |
| |
| /** |
| * Sets the {@link OnZoomListener} listener that receives callbacks to zoom. |
| * |
| * @param listener The listener that will be told to zoom. |
| */ |
| public void setOnZoomListener(OnZoomListener listener) { |
| mCallback = listener; |
| } |
| |
| /** |
| * Sets whether the zoom controls should be focusable. If the controls are |
| * focusable, then trackball and arrow key interactions are possible. |
| * Otherwise, only touch interactions are possible. |
| * |
| * @param focusable Whether the zoom controls should be focusable. |
| */ |
| public void setFocusable(boolean focusable) { |
| int oldFlags = mContainerLayoutParams.flags; |
| if (focusable) { |
| mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; |
| } else { |
| mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE; |
| } |
| |
| if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) { |
| mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); |
| } |
| } |
| |
| /** |
| * Whether the zoom controls will be automatically dismissed after showing. |
| * |
| * @return Whether the zoom controls will be auto dismissed after showing. |
| */ |
| public boolean isAutoDismissed() { |
| return mAutoDismissControls; |
| } |
| |
| /** |
| * Sets whether the zoom controls will be automatically dismissed after |
| * showing. |
| */ |
| public void setAutoDismissed(boolean autoDismiss) { |
| if (mAutoDismissControls == autoDismiss) return; |
| mAutoDismissControls = autoDismiss; |
| } |
| |
| /** |
| * Whether the zoom controls are visible to the user. |
| * |
| * @return Whether the zoom controls are visible to the user. |
| */ |
| public boolean isVisible() { |
| return mIsVisible; |
| } |
| |
| /** |
| * Sets whether the zoom controls should be visible to the user. |
| * |
| * @param visible Whether the zoom controls should be visible to the user. |
| */ |
| public void setVisible(boolean visible) { |
| |
| if (visible) { |
| if (mOwnerView.getWindowToken() == null) { |
| /* |
| * We need a window token to show ourselves, maybe the owner's |
| * window hasn't been created yet but it will have been by the |
| * time the looper is idle, so post the setVisible(true) call. |
| */ |
| if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) { |
| mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE); |
| } |
| return; |
| } |
| |
| dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); |
| } |
| |
| if (mIsVisible == visible) { |
| return; |
| } |
| mIsVisible = visible; |
| |
| if (visible) { |
| if (mContainerLayoutParams.token == null) { |
| mContainerLayoutParams.token = mOwnerView.getWindowToken(); |
| } |
| |
| mWindowManager.addView(mContainer, mContainerLayoutParams); |
| |
| if (mPostedVisibleInitializer == null) { |
| mPostedVisibleInitializer = new Runnable() { |
| public void run() { |
| refreshPositioningVariables(); |
| |
| if (mCallback != null) { |
| mCallback.onVisibilityChanged(true); |
| } |
| } |
| }; |
| } |
| |
| mHandler.post(mPostedVisibleInitializer); |
| |
| // Handle configuration changes when visible |
| mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); |
| |
| // Steal touches events from the owner |
| mOwnerView.setOnTouchListener(this); |
| mReleaseTouchListenerOnUp = false; |
| |
| } else { |
| // Don't want to steal any more touches |
| if (mTouchTargetView != null) { |
| // We are still stealing the touch events for this touch |
| // sequence, so release the touch listener later |
| mReleaseTouchListenerOnUp = true; |
| } else { |
| mOwnerView.setOnTouchListener(null); |
| } |
| |
| // No longer care about configuration changes |
| mContext.unregisterReceiver(mConfigurationChangedReceiver); |
| |
| mWindowManager.removeView(mContainer); |
| mHandler.removeCallbacks(mPostedVisibleInitializer); |
| |
| if (mCallback != null) { |
| mCallback.onVisibilityChanged(false); |
| } |
| } |
| |
| } |
| |
| /** |
| * Gets the container that is the parent of the zoom controls. |
| * <p> |
| * The client can add other views to this container to link them with the |
| * zoom controls. |
| * |
| * @return The container of the zoom controls. It will be a layout that |
| * respects the gravity of a child's layout parameters. |
| */ |
| public ViewGroup getContainer() { |
| return mContainer; |
| } |
| |
| /** |
| * Gets the view for the zoom controls. |
| * |
| * @return The zoom controls view. |
| */ |
| public View getZoomControls() { |
| return mControls; |
| } |
| |
| private void dismissControlsDelayed(int delay) { |
| if (mAutoDismissControls) { |
| mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS); |
| mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay); |
| } |
| } |
| |
| private void refreshPositioningVariables() { |
| // if the mOwnerView is detached from window then skip. |
| if (mOwnerView.getWindowToken() == null) return; |
| |
| // Position the zoom controls on the bottom of the owner view. |
| int ownerHeight = mOwnerView.getHeight(); |
| int ownerWidth = mOwnerView.getWidth(); |
| // The gap between the top of the owner and the top of the container |
| int containerOwnerYOffset = ownerHeight - mContainer.getHeight(); |
| |
| // Calculate the owner view's bounds |
| mOwnerView.getLocationOnScreen(mOwnerViewRawLocation); |
| mContainerRawLocation[0] = mOwnerViewRawLocation[0]; |
| mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset; |
| |
| int[] ownerViewWindowLoc = mTempIntArray; |
| mOwnerView.getLocationInWindow(ownerViewWindowLoc); |
| |
| // lp.x and lp.y should be relative to the owner's window top-left |
| mContainerLayoutParams.x = ownerViewWindowLoc[0]; |
| mContainerLayoutParams.width = ownerWidth; |
| mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset; |
| if (mIsVisible) { |
| mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); |
| } |
| |
| } |
| |
| /* This will only be called when the container has focus. */ |
| private boolean onContainerKey(KeyEvent event) { |
| int keyCode = event.getKeyCode(); |
| if (isInterestingKey(keyCode)) { |
| |
| if (keyCode == KeyEvent.KEYCODE_BACK) { |
| if (event.getAction() == KeyEvent.ACTION_DOWN |
| && event.getRepeatCount() == 0) { |
| if (mOwnerView != null) { |
| KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState(); |
| if (ds != null) { |
| ds.startTracking(event, this); |
| } |
| } |
| return true; |
| } else if (event.getAction() == KeyEvent.ACTION_UP |
| && event.isTracking() && !event.isCanceled()) { |
| setVisible(false); |
| return true; |
| } |
| |
| } else { |
| dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); |
| } |
| |
| // Let the container handle the key |
| return false; |
| |
| } else { |
| |
| ViewRootImpl viewRoot = mOwnerView.getViewRootImpl(); |
| if (viewRoot != null) { |
| viewRoot.dispatchInputEvent(event); |
| } |
| |
| // We gave the key to the owner, don't let the container handle this key |
| return true; |
| } |
| } |
| |
| private boolean isInterestingKey(int keyCode) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_DPAD_CENTER: |
| case KeyEvent.KEYCODE_DPAD_UP: |
| case KeyEvent.KEYCODE_DPAD_DOWN: |
| case KeyEvent.KEYCODE_DPAD_LEFT: |
| case KeyEvent.KEYCODE_DPAD_RIGHT: |
| case KeyEvent.KEYCODE_ENTER: |
| case KeyEvent.KEYCODE_BACK: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * @hide The ZoomButtonsController implements the OnTouchListener, but this |
| * does not need to be shown in its public API. |
| */ |
| public boolean onTouch(View v, MotionEvent event) { |
| int action = event.getAction(); |
| |
| if (event.getPointerCount() > 1) { |
| // ZoomButtonsController doesn't handle mutitouch. Give up control. |
| return false; |
| } |
| |
| if (mReleaseTouchListenerOnUp) { |
| // The controls were dismissed but we need to throw away all events until the up |
| if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { |
| mOwnerView.setOnTouchListener(null); |
| setTouchTargetView(null); |
| mReleaseTouchListenerOnUp = false; |
| } |
| |
| // Eat this event |
| return true; |
| } |
| |
| dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); |
| |
| View targetView = mTouchTargetView; |
| |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY()); |
| setTouchTargetView(targetView); |
| break; |
| |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| setTouchTargetView(null); |
| break; |
| } |
| |
| if (targetView != null) { |
| // The upperleft corner of the target view in raw coordinates |
| int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0]; |
| int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1]; |
| |
| MotionEvent containerEvent = MotionEvent.obtain(event); |
| // Convert the motion event into the target view's coordinates (from |
| // owner view's coordinates) |
| containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX, |
| mOwnerViewRawLocation[1] - targetViewRawY); |
| /* Disallow negative coordinates (which can occur due to |
| * ZOOM_CONTROLS_TOUCH_PADDING) */ |
| // These are floats because we need to potentially offset away this exact amount |
| float containerX = containerEvent.getX(); |
| float containerY = containerEvent.getY(); |
| if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) { |
| containerEvent.offsetLocation(-containerX, 0); |
| } |
| if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) { |
| containerEvent.offsetLocation(0, -containerY); |
| } |
| boolean retValue = targetView.dispatchTouchEvent(containerEvent); |
| containerEvent.recycle(); |
| return retValue; |
| |
| } else { |
| return false; |
| } |
| } |
| |
| private void setTouchTargetView(View view) { |
| mTouchTargetView = view; |
| if (view != null) { |
| view.getLocationInWindow(mTouchTargetWindowLocation); |
| } |
| } |
| |
| /** |
| * Returns the View that should receive a touch at the given coordinates. |
| * |
| * @param rawX The raw X. |
| * @param rawY The raw Y. |
| * @return The view that should receive the touches, or null if there is not one. |
| */ |
| private View findViewForTouch(int rawX, int rawY) { |
| // Reverse order so the child drawn on top gets first dibs. |
| int containerCoordsX = rawX - mContainerRawLocation[0]; |
| int containerCoordsY = rawY - mContainerRawLocation[1]; |
| Rect frame = mTempRect; |
| |
| View closestChild = null; |
| int closestChildDistanceSq = Integer.MAX_VALUE; |
| |
| for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { |
| View child = mContainer.getChildAt(i); |
| if (child.getVisibility() != View.VISIBLE) { |
| continue; |
| } |
| |
| child.getHitRect(frame); |
| if (frame.contains(containerCoordsX, containerCoordsY)) { |
| return child; |
| } |
| |
| int distanceX; |
| if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) { |
| distanceX = 0; |
| } else { |
| distanceX = Math.min(Math.abs(frame.left - containerCoordsX), |
| Math.abs(containerCoordsX - frame.right)); |
| } |
| int distanceY; |
| if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) { |
| distanceY = 0; |
| } else { |
| distanceY = Math.min(Math.abs(frame.top - containerCoordsY), |
| Math.abs(containerCoordsY - frame.bottom)); |
| } |
| int distanceSq = distanceX * distanceX + distanceY * distanceY; |
| |
| if ((distanceSq < mTouchPaddingScaledSq) && |
| (distanceSq < closestChildDistanceSq)) { |
| closestChild = child; |
| closestChildDistanceSq = distanceSq; |
| } |
| } |
| |
| return closestChild; |
| } |
| |
| private void onPostConfigurationChanged() { |
| dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); |
| refreshPositioningVariables(); |
| } |
| |
| /** |
| * Interface that will be called when the user performs an interaction that |
| * triggers some action, for example zooming. |
| */ |
| public interface OnZoomListener { |
| |
| /** |
| * Called when the zoom controls' visibility changes. |
| * |
| * @param visible Whether the zoom controls are visible. |
| */ |
| void onVisibilityChanged(boolean visible); |
| |
| /** |
| * Called when the owner view needs to be zoomed. |
| * |
| * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out. |
| */ |
| void onZoom(boolean zoomIn); |
| } |
| |
| private class Container extends FrameLayout { |
| public Container(Context context) { |
| super(context); |
| } |
| |
| /* |
| * Need to override this to intercept the key events. Otherwise, we |
| * would attach a key listener to the container but its superclass |
| * ViewGroup gives it to the focused View instead of calling the key |
| * listener, and so we wouldn't get the events. |
| */ |
| @Override |
| public boolean dispatchKeyEvent(KeyEvent event) { |
| return onContainerKey(event) ? true : super.dispatchKeyEvent(event); |
| } |
| } |
| |
| } |