2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.widget;
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.ViewRoot;
37 import android.view.WindowManager;
38 import android.view.View.OnClickListener;
39 import android.view.WindowManager.LayoutParams;
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
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.
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)}.
68 * If you are using this with a custom View, please call
69 * {@link #setVisible(boolean) setVisible(false)} from the
70 * {@link View#onDetachedFromWindow}.
73 public class ZoomButtonsController implements View.OnTouchListener {
75 private static final String TAG = "ZoomButtonsController";
77 private static final int ZOOM_CONTROLS_TIMEOUT =
78 (int) ViewConfiguration.getZoomControlsTimeout();
80 private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20;
81 private int mTouchPaddingScaledSq;
83 private final Context mContext;
84 private final WindowManager mWindowManager;
85 private boolean mAutoDismissControls = true;
88 * The view that is being zoomed by this zoom controller.
90 private final View mOwnerView;
93 * The location of the owner view on the screen. This is recalculated
94 * each time the zoom controller is shown.
96 private final int[] mOwnerViewRawLocation = new int[2];
99 * The container that is added as a window.
101 private final FrameLayout mContainer;
102 private LayoutParams mContainerLayoutParams;
103 private final int[] mContainerRawLocation = new int[2];
105 private ZoomControls mControls;
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.
111 private View mTouchTargetView;
113 * The {@link #mTouchTargetView}'s location in window, set on touch down.
115 private final int[] mTouchTargetWindowLocation = new int[2];
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.
122 * Otherwise, the owner view would get mismatched events (i.e., touch move
123 * even though it never got the touch down.)
125 private boolean mReleaseTouchListenerOnUp;
127 /** Whether the container has been added to the window manager. */
128 private boolean mIsVisible;
130 private final Rect mTempRect = new Rect();
131 private final int[] mTempIntArray = new int[2];
133 private OnZoomListener mCallback;
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.
141 private Runnable mPostedVisibleInitializer;
143 private final IntentFilter mConfigurationChangedFilter =
144 new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
147 * Needed to reposition the zoom controls after configuration changes.
149 private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() {
151 public void onReceive(Context context, Intent intent) {
152 if (!mIsVisible) return;
154 mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED);
155 mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED);
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;
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.
167 private static final int MSG_POST_SET_VISIBLE = 4;
169 private final Handler mHandler = new Handler() {
171 public void handleMessage(Message msg) {
173 case MSG_POST_CONFIGURATION_CHANGED:
174 onPostConfigurationChanged();
177 case MSG_DISMISS_ZOOM_CONTROLS:
181 case MSG_POST_SET_VISIBLE:
182 if (mOwnerView.getWindowToken() == null) {
183 // Doh, it is still null, just ignore the set visible call
185 "Cannot make the zoom controller visible if the owner view is " +
186 "not attached to a window.");
197 * Constructor for the {@link ZoomButtonsController}.
199 * @param ownerView The view that is being zoomed by the zoom controls. The
200 * zoom controls will be displayed aligned with this view.
202 public ZoomButtonsController(View ownerView) {
203 mContext = ownerView.getContext();
204 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
205 mOwnerView = ownerView;
207 mTouchPaddingScaledSq = (int)
208 (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
209 mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
211 mContainer = createContainer();
215 * Whether to enable the zoom in control.
217 * @param enabled Whether to enable the zoom in control.
219 public void setZoomInEnabled(boolean enabled) {
220 mControls.setIsZoomInEnabled(enabled);
224 * Whether to enable the zoom out control.
226 * @param enabled Whether to enable the zoom out control.
228 public void setZoomOutEnabled(boolean enabled) {
229 mControls.setIsZoomOutEnabled(enabled);
233 * Sets the delay between zoom callbacks as the user holds a zoom button.
235 * @param speed The delay in milliseconds between zoom callbacks.
237 public void setZoomSpeed(long speed) {
238 mControls.setZoomSpeed(speed);
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.LEFT;
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;
256 FrameLayout container = new Container(mContext);
257 container.setLayoutParams(lp);
258 container.setMeasureAllChildren(true);
260 LayoutInflater inflater = (LayoutInflater) mContext
261 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
262 inflater.inflate(com.android.internal.R.layout.zoom_container, container);
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);
271 mControls.setOnZoomOutClickListener(new OnClickListener() {
272 public void onClick(View v) {
273 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
274 if (mCallback != null) mCallback.onZoom(false);
282 * Sets the {@link OnZoomListener} listener that receives callbacks to zoom.
284 * @param listener The listener that will be told to zoom.
286 public void setOnZoomListener(OnZoomListener listener) {
287 mCallback = listener;
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.
295 * @param focusable Whether the zoom controls should be focusable.
297 public void setFocusable(boolean focusable) {
298 int oldFlags = mContainerLayoutParams.flags;
300 mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
302 mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
305 if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
306 mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
311 * Whether the zoom controls will be automatically dismissed after showing.
313 * @return Whether the zoom controls will be auto dismissed after showing.
315 public boolean isAutoDismissed() {
316 return mAutoDismissControls;
320 * Sets whether the zoom controls will be automatically dismissed after
323 public void setAutoDismissed(boolean autoDismiss) {
324 if (mAutoDismissControls == autoDismiss) return;
325 mAutoDismissControls = autoDismiss;
329 * Whether the zoom controls are visible to the user.
331 * @return Whether the zoom controls are visible to the user.
333 public boolean isVisible() {
338 * Sets whether the zoom controls should be visible to the user.
340 * @param visible Whether the zoom controls should be visible to the user.
342 public void setVisible(boolean visible) {
345 if (mOwnerView.getWindowToken() == null) {
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.
351 if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
352 mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
357 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
360 if (mIsVisible == visible) {
363 mIsVisible = visible;
366 if (mContainerLayoutParams.token == null) {
367 mContainerLayoutParams.token = mOwnerView.getWindowToken();
370 mWindowManager.addView(mContainer, mContainerLayoutParams);
372 if (mPostedVisibleInitializer == null) {
373 mPostedVisibleInitializer = new Runnable() {
375 refreshPositioningVariables();
377 if (mCallback != null) {
378 mCallback.onVisibilityChanged(true);
384 mHandler.post(mPostedVisibleInitializer);
386 // Handle configuration changes when visible
387 mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
389 // Steal touches events from the owner
390 mOwnerView.setOnTouchListener(this);
391 mReleaseTouchListenerOnUp = false;
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;
400 mOwnerView.setOnTouchListener(null);
403 // No longer care about configuration changes
404 mContext.unregisterReceiver(mConfigurationChangedReceiver);
406 mWindowManager.removeView(mContainer);
407 mHandler.removeCallbacks(mPostedVisibleInitializer);
409 if (mCallback != null) {
410 mCallback.onVisibilityChanged(false);
417 * Gets the container that is the parent of the zoom controls.
419 * The client can add other views to this container to link them with the
422 * @return The container of the zoom controls. It will be a layout that
423 * respects the gravity of a child's layout parameters.
425 public ViewGroup getContainer() {
430 * Gets the view for the zoom controls.
432 * @return The zoom controls view.
434 public View getZoomControls() {
438 private void dismissControlsDelayed(int delay) {
439 if (mAutoDismissControls) {
440 mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
441 mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
445 private void refreshPositioningVariables() {
446 // if the mOwnerView is detached from window then skip.
447 if (mOwnerView.getWindowToken() == null) return;
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();
455 // Calculate the owner view's bounds
456 mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
457 mContainerRawLocation[0] = mOwnerViewRawLocation[0];
458 mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
460 int[] ownerViewWindowLoc = mTempIntArray;
461 mOwnerView.getLocationInWindow(ownerViewWindowLoc);
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;
468 mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
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)) {
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();
484 ds.startTracking(event, this);
488 } else if (event.getAction() == KeyEvent.ACTION_UP
489 && event.isTracking() && !event.isCanceled()) {
495 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
498 // Let the container handle the key
503 ViewRoot viewRoot = getOwnerViewRoot();
504 if (viewRoot != null) {
505 viewRoot.dispatchKey(event);
508 // We gave the key to the owner, don't let the container handle this key
513 private boolean isInterestingKey(int 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:
528 private ViewRoot getOwnerViewRoot() {
529 View rootViewOfOwner = mOwnerView.getRootView();
530 if (rootViewOfOwner == null) {
534 ViewParent parentOfRootView = rootViewOfOwner.getParent();
535 if (parentOfRootView instanceof ViewRoot) {
536 return (ViewRoot) parentOfRootView;
543 * @hide The ZoomButtonsController implements the OnTouchListener, but this
544 * does not need to be shown in its public API.
546 public boolean onTouch(View v, MotionEvent event) {
547 int action = event.getAction();
549 if (event.getPointerCount() > 1) {
550 // ZoomButtonsController doesn't handle mutitouch. Give up control.
554 if (mReleaseTouchListenerOnUp) {
555 // The controls were dismissed but we need to throw away all events until the up
556 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
557 mOwnerView.setOnTouchListener(null);
558 setTouchTargetView(null);
559 mReleaseTouchListenerOnUp = false;
566 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
568 View targetView = mTouchTargetView;
571 case MotionEvent.ACTION_DOWN:
572 targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
573 setTouchTargetView(targetView);
576 case MotionEvent.ACTION_UP:
577 case MotionEvent.ACTION_CANCEL:
578 setTouchTargetView(null);
582 if (targetView != null) {
583 // The upperleft corner of the target view in raw coordinates
584 int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
585 int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
587 MotionEvent containerEvent = MotionEvent.obtain(event);
588 // Convert the motion event into the target view's coordinates (from
589 // owner view's coordinates)
590 containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
591 mOwnerViewRawLocation[1] - targetViewRawY);
592 /* Disallow negative coordinates (which can occur due to
593 * ZOOM_CONTROLS_TOUCH_PADDING) */
594 // These are floats because we need to potentially offset away this exact amount
595 float containerX = containerEvent.getX();
596 float containerY = containerEvent.getY();
597 if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) {
598 containerEvent.offsetLocation(-containerX, 0);
600 if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) {
601 containerEvent.offsetLocation(0, -containerY);
603 boolean retValue = targetView.dispatchTouchEvent(containerEvent);
604 containerEvent.recycle();
612 private void setTouchTargetView(View view) {
613 mTouchTargetView = view;
615 view.getLocationInWindow(mTouchTargetWindowLocation);
620 * Returns the View that should receive a touch at the given coordinates.
622 * @param rawX The raw X.
623 * @param rawY The raw Y.
624 * @return The view that should receive the touches, or null if there is not one.
626 private View findViewForTouch(int rawX, int rawY) {
627 // Reverse order so the child drawn on top gets first dibs.
628 int containerCoordsX = rawX - mContainerRawLocation[0];
629 int containerCoordsY = rawY - mContainerRawLocation[1];
630 Rect frame = mTempRect;
632 View closestChild = null;
633 int closestChildDistanceSq = Integer.MAX_VALUE;
635 for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
636 View child = mContainer.getChildAt(i);
637 if (child.getVisibility() != View.VISIBLE) {
641 child.getHitRect(frame);
642 if (frame.contains(containerCoordsX, containerCoordsY)) {
647 if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) {
650 distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
651 Math.abs(containerCoordsX - frame.right));
654 if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) {
657 distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
658 Math.abs(containerCoordsY - frame.bottom));
660 int distanceSq = distanceX * distanceX + distanceY * distanceY;
662 if ((distanceSq < mTouchPaddingScaledSq) &&
663 (distanceSq < closestChildDistanceSq)) {
664 closestChild = child;
665 closestChildDistanceSq = distanceSq;
672 private void onPostConfigurationChanged() {
673 dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
674 refreshPositioningVariables();
678 * Interface that will be called when the user performs an interaction that
679 * triggers some action, for example zooming.
681 public interface OnZoomListener {
684 * Called when the zoom controls' visibility changes.
686 * @param visible Whether the zoom controls are visible.
688 void onVisibilityChanged(boolean visible);
691 * Called when the owner view needs to be zoomed.
693 * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
695 void onZoom(boolean zoomIn);
698 private class Container extends FrameLayout {
699 public Container(Context context) {
704 * Need to override this to intercept the key events. Otherwise, we
705 * would attach a key listener to the container but its superclass
706 * ViewGroup gives it to the focused View instead of calling the key
707 * listener, and so we wouldn't get the events.
710 public boolean dispatchKeyEvent(KeyEvent event) {
711 return onContainerKey(event) ? true : super.dispatchKeyEvent(event);