diff options
| author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-13 13:04:22 -0700 |
|---|---|---|
| committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-13 13:04:22 -0700 |
| commit | ba87e3e6c985e7175152993b5efcc7dd2f0e1c93 (patch) | |
| tree | ee35f76532767dc29411a8738a434d1d88d330f2 /core/java/android/widget/ZoomButtonsController.java | |
| parent | c39a6e0c51e182338deb8b63d07933b585134929 (diff) | |
auto import from //branches/cupcake_rel/...@138607
Diffstat (limited to 'core/java/android/widget/ZoomButtonsController.java')
| -rw-r--r-- | core/java/android/widget/ZoomButtonsController.java | 374 |
1 files changed, 309 insertions, 65 deletions
diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 6729fd17a5ef..4daa41949330 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -24,12 +24,15 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.graphics.Canvas; import android.graphics.PixelFormat; import android.graphics.Rect; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.provider.Settings; +import android.util.Log; +import android.view.GestureDetector; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -37,31 +40,51 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewRoot; import android.view.Window; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.WindowManager.LayoutParams; -// TODO: make sure no px values exist, only dip (scale if necessary from Viewconfiguration) - +/* + * Implementation notes: + * - The zoom controls are displayed in their own window. + * (Easier for the client and better performance) + * - This window is not touchable, and by default is not focusable. + * - To make the buttons clickable, it attaches a OnTouchListener to the owner + * view and does the hit detection locally. + * - When it is focusable, it forwards uninteresting events to the owner view's + * view hierarchy. + */ /** - * TODO: Docs - * + * The {@link ZoomButtonsController} handles showing and hiding the zoom + * controls 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> + * Typical usage involves the client using the {@link GestureDetector} to + * forward events from + * {@link GestureDetector.OnDoubleTapListener#onDoubleTapEvent(MotionEvent)} to + * {@link #handleDoubleTapEvent(MotionEvent)}. 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 the * {@link View#onDetachedFromWindow}. - * + * * @hide */ -public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyListener { +public class ZoomButtonsController implements View.OnTouchListener { private static final String TAG = "ZoomButtonsController"; private static final int ZOOM_CONTROLS_TIMEOUT = (int) ViewConfiguration.getZoomControlsTimeout(); - // TODO: scaled to density private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20; + private int mTouchPaddingScaledSq; private Context mContext; private WindowManager mWindowManager; @@ -72,17 +95,17 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi private View mOwnerView; /** - * The bounds of the owner view in global coordinates. This is recalculated + * The location of the owner view on the screen. This is recalculated * each time the zoom controller is shown. */ - private Rect mOwnerViewBounds = new Rect(); + private int[] mOwnerViewRawLocation = new int[2]; /** * The container that is added as a window. */ private FrameLayout mContainer; private LayoutParams mContainerLayoutParams; - private int[] mContainerLocation = new int[2]; + private int[] mContainerRawLocation = new int[2]; private ZoomControls mControls; @@ -94,7 +117,7 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi /** * The {@link #mTouchTargetView}'s location in window, set on touch down. */ - private int[] mTouchTargetLocationInWindow = new int[2]; + private 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 @@ -102,15 +125,28 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi */ private boolean mReleaseTouchListenerOnUp; + /** + * Whether we are currently in the double-tap gesture, with the second tap + * still being performed (i.e., we're waiting for the second tap's touch up). + */ private boolean mIsSecondTapDown; + /** Whether the container has been added to the window manager. */ private boolean mIsVisible; private Rect mTempRect = new Rect(); - + private int[] mTempIntArray = new int[2]; + private OnZoomListener mCallback; /** + * In 1.0, the ZoomControls were to be added to the UI by the client of + * WebView, MapView, etc. We didn't want apps to break, so we return a dummy + * view in place now. + */ + private InvisibleView mDummyZoomControls; + + /** * 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 @@ -121,6 +157,9 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi private IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + /** + * Needed to reposition the zoom controls after configuration changes. + */ private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -161,41 +200,68 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi case MSG_POST_SET_VISIBLE: if (mOwnerView.getWindowToken() == null) { - // Doh, it is still null, throw an exception - throw new IllegalArgumentException( + // 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); } - setVisible(true); break; } } }; - public ZoomButtonsController(Context context, View ownerView) { - mContext = context; - mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + /** + * 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); - lp.gravity = Gravity.BOTTOM | Gravity.CENTER; + // Controls are positioned BOTTOM | CENTER with respect to the owner view. + lp.gravity = Gravity.TOP | Gravity.LEFT; lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_NO_LIMITS; @@ -206,10 +272,9 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons; mContainerLayoutParams = lp; - FrameLayout container = new FrameLayout(mContext); + FrameLayout container = new Container(mContext); container.setLayoutParams(lp); container.setMeasureAllChildren(true); - container.setOnKeyListener(this); LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -232,30 +297,51 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi return container; } - public void setCallback(OnZoomListener callback) { - mCallback = callback; + /** + * 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 (mIsVisible) { + if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) { mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); } } + /** + * 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 (!useThisZoom(mContext)) return; - if (visible) { if (mOwnerView.getWindowToken() == null) { /* @@ -329,12 +415,13 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi } /** - * TODO: docs - * - * Notes: - * - Please ensure you set your View to INVISIBLE not GONE when hiding it. - * - * @return TODO + * 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; @@ -347,14 +434,12 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi /** * Should be called by the client for each event belonging to the second tap - * (the down, move, up, and cancel events). + * (the down, move, up, and/or cancel events). * * @param event The event belonging to the second tap. * @return Whether the event was consumed. */ public boolean handleDoubleTapEvent(MotionEvent event) { - if (!useThisZoom(mContext)) return false; - int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { @@ -382,9 +467,28 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi } private void refreshPositioningVariables() { + // 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.getGlobalVisibleRect(mOwnerViewBounds); - mContainer.getLocationOnScreen(mContainerLocation); + 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); + } + } /** @@ -396,11 +500,65 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi } } - public boolean onKey(View v, int keyCode, KeyEvent event) { - dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); - return false; + /* 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) { + setVisible(false); + } else { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + } + + // Let the container handle the key + return false; + + } else { + + ViewRoot viewRoot = getOwnerViewRoot(); + if (viewRoot != null) { + viewRoot.dispatchKey(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; + } + } + + private ViewRoot getOwnerViewRoot() { + View rootViewOfOwner = mOwnerView.getRootView(); + if (rootViewOfOwner == null) { + return null; + } + + ViewParent parentOfRootView = rootViewOfOwner.getParent(); + if (parentOfRootView instanceof ViewRoot) { + return (ViewRoot) parentOfRootView; + } else { + return null; + } + } + + /** + * @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(); @@ -423,14 +581,13 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi return true; } - // TODO: optimize this (it ends up removing message and queuing another) dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); View targetView = mTouchTargetView; switch (action) { case MotionEvent.ACTION_DOWN: - targetView = getViewForTouch((int) event.getRawX(), (int) event.getRawY()); + targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY()); setTouchTargetView(targetView); break; @@ -442,14 +599,22 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi if (targetView != null) { // The upperleft corner of the target view in raw coordinates - int targetViewRawX = mContainerLocation[0] + mTouchTargetLocationInWindow[0]; - int targetViewRawY = mContainerLocation[1] + mTouchTargetLocationInWindow[1]; + 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(mOwnerViewBounds.left - targetViewRawX, - mOwnerViewBounds.top - targetViewRawY); + containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX, + mOwnerViewRawLocation[1] - targetViewRawY); + /* Disallow negative coordinates (which can occur due to + * ZOOM_CONTROLS_TOUCH_PADDING) */ + if (containerEvent.getX() < 0) { + containerEvent.offsetLocation(-containerEvent.getX(), 0); + } + if (containerEvent.getY() < 0) { + containerEvent.offsetLocation(0, -containerEvent.getY()); + } boolean retValue = targetView.dispatchTouchEvent(containerEvent); containerEvent.recycle(); return retValue || consumeEvent; @@ -462,7 +627,7 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi private void setTouchTargetView(View view) { mTouchTargetView = view; if (view != null) { - view.getLocationInWindow(mTouchTargetLocationInWindow); + view.getLocationInWindow(mTouchTargetWindowLocation); } } @@ -473,11 +638,15 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi * @param rawY The raw Y. * @return The view that should receive the touches, or null if there is not one. */ - private View getViewForTouch(int rawX, int rawY) { + private View findViewForTouch(int rawX, int rawY) { // Reverse order so the child drawn on top gets first dibs. - int containerCoordsX = rawX - mContainerLocation[0]; - int containerCoordsY = rawY - mContainerLocation[1]; + 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) { @@ -485,14 +654,24 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi } child.getHitRect(frame); - // Expand the touch region - frame.top -= ZOOM_CONTROLS_TOUCH_PADDING; if (frame.contains(containerCoordsX, containerCoordsY)) { return child; } + + int distanceX = Math.min(Math.abs(frame.left - containerCoordsX), + Math.abs(containerCoordsX - frame.right)); + int 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 null; + return closestChild; } private void onPostConfigurationChanged() { @@ -518,6 +697,10 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi * gallery */ public static void showZoomTutorialOnce(Context context) { + + // TODO: remove this code, but to hit the weekend build, just never show + if (true) return; + ContentResolver cr = context.getContentResolver(); if (Settings.System.getInt(cr, SETTING_NAME_SHOWN_TUTORIAL, 0) == 1) { return; @@ -583,22 +766,83 @@ public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyLi finishZoomTutorial(mContext, true); } - // Temporary methods for different zoom types - static int getZoomType(Context context) { - return Settings.System.getInt(context.getContentResolver(), "zoom", 2); - } - - public static boolean useOldZoom(Context context) { - return getZoomType(context) == 0; - } - - public static boolean useThisZoom(Context context) { - return getZoomType(context) == 2; + /** @hide Should only be used only be WebView and MapView */ + public View getDummyZoomControls() { + if (mDummyZoomControls == null) { + mDummyZoomControls = new InvisibleView(mContext); + } + return mDummyZoomControls; } - + + /** + * Interface that will be called when the user performs an interaction that + * triggers some action, for example zooming. + */ public interface OnZoomListener { + /** + * Called when the given point should be centered. The point will be in + * owner view coordinates. + * + * @param x The x of the point. + * @param y The y of the point. + */ void onCenter(int x, int y); + + /** + * 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); + } + } + + /** + * An InvisibleView is an invisible, zero-sized View for backwards + * compatibility + */ + private final class InvisibleView extends View { + + private InvisibleView(Context context) { + super(context); + setVisibility(GONE); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(0, 0); + } + + @Override + public void draw(Canvas canvas) { + } + + @Override + protected void dispatchDraw(Canvas canvas) { + } + } + } |
