/*
 * 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.View.OnClickListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
import android.view.WindowManager;
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 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.

Typically, clients should call setVisible(true) on a touch down or move (no need to call setVisible(false) since it will time out on its own). Also, whenever the owner cannot be zoomed further, the client should update setZoomInEnabled(boolean) and setZoomOutEnabled(boolean).

If you are using this with a custom View, please call setVisible(false) from View.onDetachedFromWindow and from View.onVisibilityChanged when visibility != View.VISIBLE.

Deprecated:This functionality and UI is better handled with custom views and layouts rather than a dedicated zoom-control widget
/** * 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>. * * @deprecated This functionality and UI is better handled with custom views and layouts * rather than a dedicated zoom-control widget */
@Deprecated 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.
/** * 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.
/** * 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.
/** * 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.
/** * 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 mTouchTargetView's location in window, set on touch down.
/** * 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.

Otherwise, the owner view would get mismatched events (i.e., touch move even though it never got the touch down.)

/** * 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.
/** 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.
/** * 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.
/** * 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.
/** 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.
/** 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.
/** * 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 ZoomButtonsController.
Params:
  • ownerView – The view that is being zoomed by the zoom controls. The zoom controls will be displayed aligned with this view.
/** * 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.
Params:
  • enabled – Whether to enable the zoom in control.
/** * 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.
Params:
  • enabled – Whether to enable the zoom out control.
/** * 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.
Params:
  • speed – The delay in milliseconds between zoom callbacks.
/** * 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 = 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 OnZoomListener listener that receives callbacks to zoom.
Params:
  • listener – The listener that will be told to zoom.
/** * 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.
Params:
  • focusable – Whether the zoom controls should be focusable.
/** * 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.
Returns:Whether the zoom controls will be auto dismissed after showing.
/** * 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.
/** * 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.
Returns:Whether the zoom controls are visible to the user.
/** * 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.
Params:
  • visible – Whether the zoom controls should be visible to the user.
/** * 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.removeViewImmediate(mContainer); mHandler.removeCallbacks(mPostedVisibleInitializer); if (mCallback != null) { mCallback.onVisibilityChanged(false); } } }
Gets the container that is the parent of the zoom controls.

The client can add other views to this container to link them with the zoom controls.

Returns:The container of the zoom controls. It will be a layout that respects the gravity of a child's layout parameters.
/** * 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.
Returns:The zoom controls view.
/** * 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; } }
@hideThe ZoomButtonsController implements the OnTouchListener, but this does not need to be shown in its public API.
/** * @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.
Params:
  • rawX – The raw X.
  • rawY – The raw Y.
Returns:The view that should receive the touches, or null if there is not one.
/** * 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.
/** * 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.
Params:
  • visible – Whether the zoom controls are visible.
/** * 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.
Params:
  • zoomIn – The direction of the zoom: true to zoom in, false to zoom out.
/** * 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); } } }