blob: 2e97fdafebade8e5fabe1472032e8126fa3943b3 [file] [log] [blame]
/*
* 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.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
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.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.Window;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
// TODO: make sure no px values exist, only dip (scale if necessary from Viewconfiguration)
/**
* TODO: Docs
*
* If you are using this with a custom View, please call
* {@link #setVisible(boolean) setVisible(false)} from the
* {@link View#onDetachedFromWindow}.
*
* @hide
*/
public class ZoomRingController implements ZoomRing.OnZoomRingCallback,
View.OnTouchListener, View.OnKeyListener {
private static final int ZOOM_RING_RADIUS_INSET = 24;
private static final int ZOOM_RING_RECENTERING_DURATION = 500;
private static final String TAG = "ZoomRing";
public static final boolean USE_OLD_ZOOM = false;
public static boolean useOldZoom(Context context) {
return Settings.System.getInt(context.getContentResolver(), "zoom", 1) == 0;
}
private static final int ZOOM_CONTROLS_TIMEOUT =
(int) ViewConfiguration.getZoomControlsTimeout();
// TODO: move these to ViewConfiguration or re-use existing ones
// TODO: scale px values based on latest from ViewConfiguration
private static final int SECOND_TAP_TIMEOUT = 500;
private static final int ZOOM_RING_DISMISS_DELAY = SECOND_TAP_TIMEOUT / 2;
// TODO: view config? at least scaled
private static final int MAX_PAN_GAP = 20;
private static final int MAX_INITIATE_PAN_GAP = 10;
// TODO view config
private static final int INITIATE_PAN_DELAY = 300;
private static final String SETTING_NAME_SHOWN_TOAST = "shown_zoom_ring_toast";
private Context mContext;
private WindowManager mWindowManager;
/**
* The view that is being zoomed by this zoom ring.
*/
private View mOwnerView;
/**
* The bounds of the owner view in global coordinates. This is recalculated
* each time the zoom ring is shown.
*/
private Rect mOwnerViewBounds = new Rect();
/**
* The container that is added as a window.
*/
private FrameLayout mContainer;
private LayoutParams mContainerLayoutParams;
/**
* 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 int[] mTouchTargetLocationInWindow = new int[2];
/**
* If the zoom ring 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.
*/
private boolean mReleaseTouchListenerOnUp;
/*
* Tap-drag is an interaction where the user first taps and then (quickly)
* does the clockwise or counter-clockwise drag. In reality, this is: (down,
* up, down, move in circles, up). This differs from the usual events of:
* (down, up, down, up, down, move in circles, up). While the only
* difference is the omission of an (up, down), for power-users this is a
* pretty big improvement as it now only requires them to focus on the
* screen once (for the first tap down) instead of twice (for the first tap
* down and then to grab the thumb).
*/
private int mTapDragStartX;
private int mTapDragStartY;
private static final int TOUCH_MODE_IDLE = 0;
private static final int TOUCH_MODE_WAITING_FOR_SECOND_TAP = 1;
private static final int TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT = 2;
private static final int TOUCH_MODE_FORWARDING_FOR_TAP_DRAG = 3;
private int mTouchMode;
private boolean mIsZoomRingVisible;
private ZoomRing mZoomRing;
private int mZoomRingWidth;
private int mZoomRingHeight;
/** Invokes panning of owner view if the zoom ring is touching an edge. */
private Panner mPanner;
private long mTouchingEdgeStartTime;
private boolean mPanningEnabledForThisInteraction;
private ImageView mPanningArrows;
private Animation mPanningArrowsEnterAnimation;
private Animation mPanningArrowsExitAnimation;
private Rect mTempRect = new Rect();
private OnZoomListener mCallback;
private ViewConfiguration mViewConfig;
/**
* When the zoom ring is centered on screen, this will be the x value used
* for the container's layout params.
*/
private int mCenteredContainerX = Integer.MIN_VALUE;
/**
* When the zoom ring is centered on screen, this will be the y value used
* for the container's layout params.
*/
private int mCenteredContainerY = Integer.MIN_VALUE;
/**
* Scroller used to re-center the zoom ring if the user had dragged it to a
* corner and then double-taps any point on the owner view (the owner view
* will center the double-tapped point, but we should re-center the zoom
* ring).
* <p>
* The (x,y) of the scroller is the (x,y) of the container's layout params.
*/
private Scroller mScroller;
/**
* When showing the zoom ring, we add the view as a new window. However,
* there is logic that needs to know the size of the zoom ring 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;
/**
* Only touch from the main thread.
*/
private static Dialog sTutorialDialog;
private static long sTutorialShowTime;
private static final int TUTORIAL_MIN_DISPLAY_TIME = 2000;
private IntentFilter mConfigurationChangedFilter =
new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!mIsZoomRingVisible) return;
mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED);
mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED);
}
};
/** Keeps the scroller going (or starts it). */
private static final int MSG_SCROLLER_TICK = 1;
/** 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 ring dismissal. */
private static final int MSG_DISMISS_ZOOM_RING = 3;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SCROLLER_TICK:
onScrollerTick();
break;
case MSG_POST_CONFIGURATION_CHANGED:
onPostConfigurationChanged();
break;
case MSG_DISMISS_ZOOM_RING:
setVisible(false);
break;
}
}
};
public ZoomRingController(Context context, View ownerView) {
mContext = context;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mPanner = new Panner();
mOwnerView = ownerView;
mZoomRing = new ZoomRing(context);
mZoomRing.setId(com.android.internal.R.id.zoomControls);
mZoomRing.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER));
mZoomRing.setCallback(this);
createPanningArrows();
mContainerLayoutParams = new LayoutParams();
mContainerLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mContainerLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCHABLE |
LayoutParams.FLAG_NOT_FOCUSABLE |
LayoutParams.FLAG_LAYOUT_NO_LIMITS;
mContainerLayoutParams.height = LayoutParams.WRAP_CONTENT;
mContainerLayoutParams.width = LayoutParams.WRAP_CONTENT;
mContainerLayoutParams.type = LayoutParams.TYPE_APPLICATION_PANEL;
mContainerLayoutParams.format = PixelFormat.TRANSPARENT;
// TODO: make a new animation for this
mContainerLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Dialog;
mContainer = new FrameLayout(context);
mContainer.setLayoutParams(mContainerLayoutParams);
mContainer.setMeasureAllChildren(true);
mContainer.addView(mZoomRing);
mContainer.addView(mPanningArrows);
mScroller = new Scroller(context, new DecelerateInterpolator());
mViewConfig = ViewConfiguration.get(context);
}
private void createPanningArrows() {
// TODO: style
mPanningArrows = new ImageView(mContext);
mPanningArrows.setImageResource(com.android.internal.R.drawable.zoom_ring_arrows);
mPanningArrows.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER));
mPanningArrows.setVisibility(View.INVISIBLE);
mPanningArrowsEnterAnimation = AnimationUtils.loadAnimation(mContext,
com.android.internal.R.anim.fade_in);
mPanningArrowsExitAnimation = AnimationUtils.loadAnimation(mContext,
com.android.internal.R.anim.fade_out);
}
/**
* Sets the angle (in radians) a user must travel in order for the client to
* get a callback. Once there is a callback, the accumulator resets. For
* example, if you set this to PI/6, it will give a callback every time the
* user moves PI/6 amount on the ring.
*
* @param callbackThreshold The angle for the callback threshold, in radians
*/
public void setZoomCallbackThreshold(float callbackThreshold) {
mZoomRing.setCallbackThreshold((int) (callbackThreshold * ZoomRing.RADIAN_INT_MULTIPLIER));
}
/**
* Sets a drawable for the zoom ring track.
*
* @param drawable The drawable to use for the track.
* @hide Need a better way of doing this, but this one-off for browser so it
* can have its final look for the usability study
*/
public void setZoomRingTrack(int drawable) {
mZoomRing.setBackgroundResource(drawable);
}
public void setCallback(OnZoomListener callback) {
mCallback = callback;
}
public void setThumbAngle(float angle) {
mZoomRing.setThumbAngle((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER));
}
public void setThumbAngleAnimated(float angle) {
mZoomRing.setThumbAngleAnimated((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER), 0);
}
public void setResetThumbAutomatically(boolean resetThumbAutomatically) {
mZoomRing.setResetThumbAutomatically(resetThumbAutomatically);
}
public void setThumbClockwiseBound(float angle) {
mZoomRing.setThumbClockwiseBound(angle >= 0 ?
(int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) :
Integer.MIN_VALUE);
}
public void setThumbCounterclockwiseBound(float angle) {
mZoomRing.setThumbCounterclockwiseBound(angle >= 0 ?
(int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) :
Integer.MIN_VALUE);
}
public boolean isVisible() {
return mIsZoomRingVisible;
}
public void setVisible(boolean visible) {
if (useOldZoom(mContext)) return;
if (visible) {
dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT);
} else {
mPanner.stop();
}
if (mIsZoomRingVisible == visible) {
return;
}
mIsZoomRingVisible = 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();
resetZoomRing();
// TODO: remove this 'update' and just center zoom ring before the
// 'add', but need to make sure we have the width and height (which
// probably can only be retrieved after it's measured, which happens
// after it's added).
mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
if (mCallback != null) {
mCallback.onVisibilityChanged(true);
}
}
};
}
mPanningArrows.setAnimation(null);
mHandler.post(mPostedVisibleInitializer);
// Handle configuration changes when visible
mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
// Steal key/touches events from the owner
mOwnerView.setOnKeyListener(this);
mOwnerView.setOnTouchListener(this);
mReleaseTouchListenerOnUp = false;
} else {
// Don't want to steal any more keys/touches
mOwnerView.setOnKeyListener(null);
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);
}
}
}
/**
* TODO: docs
*
* Notes:
* - Touch dispatching is different. Only direct children who are clickable are eligble for touch events.
* - Please ensure you set your View to INVISIBLE not GONE when hiding it.
*
* @return
*/
public FrameLayout getContainer() {
return mContainer;
}
public int getZoomRingId() {
return mZoomRing.getId();
}
private void dismissZoomRingDelayed(int delay) {
mHandler.removeMessages(MSG_DISMISS_ZOOM_RING);
mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_RING, delay);
}
private void resetZoomRing() {
mScroller.abortAnimation();
mContainerLayoutParams.x = mCenteredContainerX;
mContainerLayoutParams.y = mCenteredContainerY;
// Reset the thumb
mZoomRing.resetThumbAngle();
}
/**
* Should be called by the client for each event belonging to the second tap
* (the down, move, up, and cancel events).
*
* @param event The event belonging to the second tap.
* @return Whether the event was consumed.
*/
public boolean handleDoubleTapEvent(MotionEvent event) {
int action = event.getAction();
// TODO: make sure this works well with the
// ownerView.setOnTouchListener(this) instead of window receiving
// touches
if (action == MotionEvent.ACTION_DOWN) {
mTouchMode = TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT;
int x = (int) event.getX();
int y = (int) event.getY();
refreshPositioningVariables();
setVisible(true);
centerPoint(x, y);
ensureZoomRingIsCentered();
// Tap drag mode stuff
mTapDragStartX = x;
mTapDragStartY = y;
} else if (action == MotionEvent.ACTION_CANCEL) {
mTouchMode = TOUCH_MODE_IDLE;
} else { // action is move or up
switch (mTouchMode) {
case TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT: {
switch (action) {
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int y = (int) event.getY();
if (Math.abs(x - mTapDragStartX) > mViewConfig.getScaledTouchSlop() ||
Math.abs(y - mTapDragStartY) >
mViewConfig.getScaledTouchSlop()) {
mZoomRing.setTapDragMode(true, x, y);
mTouchMode = TOUCH_MODE_FORWARDING_FOR_TAP_DRAG;
setTouchTargetView(mZoomRing);
}
return true;
case MotionEvent.ACTION_UP:
mTouchMode = TOUCH_MODE_IDLE;
break;
}
break;
}
case TOUCH_MODE_FORWARDING_FOR_TAP_DRAG: {
switch (action) {
case MotionEvent.ACTION_MOVE:
giveTouchToZoomRing(event);
return true;
case MotionEvent.ACTION_UP:
mTouchMode = TOUCH_MODE_IDLE;
/*
* This is a power-user feature that only shows the
* zoom while the user is performing the tap-drag.
* That means once it is released, the zoom ring
* should disappear.
*/
mZoomRing.setTapDragMode(false, (int) event.getX(), (int) event.getY());
dismissZoomRingDelayed(0);
break;
}
break;
}
}
}
return true;
}
private void ensureZoomRingIsCentered() {
LayoutParams lp = mContainerLayoutParams;
if (lp.x != mCenteredContainerX || lp.y != mCenteredContainerY) {
int width = mContainer.getWidth();
int height = mContainer.getHeight();
mScroller.startScroll(lp.x, lp.y, mCenteredContainerX - lp.x,
mCenteredContainerY - lp.y, ZOOM_RING_RECENTERING_DURATION);
mHandler.sendEmptyMessage(MSG_SCROLLER_TICK);
}
}
private void refreshPositioningVariables() {
mZoomRingWidth = mZoomRing.getWidth();
mZoomRingHeight = mZoomRing.getHeight();
// Calculate the owner view's bounds
mOwnerView.getGlobalVisibleRect(mOwnerViewBounds);
// Get the center
Gravity.apply(Gravity.CENTER, mContainer.getWidth(), mContainer.getHeight(),
mOwnerViewBounds, mTempRect);
mCenteredContainerX = mTempRect.left;
mCenteredContainerY = mTempRect.top;
}
/**
* Centers the point (in owner view's coordinates).
*/
private void centerPoint(int x, int y) {
if (mCallback != null) {
mCallback.onCenter(x, y);
}
}
private void giveTouchToZoomRing(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
int x = rawX - mContainerLayoutParams.x - mZoomRing.getLeft();
int y = rawY - mContainerLayoutParams.y - mZoomRing.getTop();
mZoomRing.handleTouch(event.getAction(), event.getEventTime(), x, y, rawX, rawY);
}
public void onZoomRingSetMovableHintVisible(boolean visible) {
setPanningArrowsVisible(visible);
}
public void onUserInteractionStarted() {
mHandler.removeMessages(MSG_DISMISS_ZOOM_RING);
}
public void onUserInteractionStopped() {
dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT);
}
public void onZoomRingMovingStarted() {
mScroller.abortAnimation();
mTouchingEdgeStartTime = 0;
if (mCallback != null) {
mCallback.onBeginPan();
}
}
private void setPanningArrowsVisible(boolean visible) {
mPanningArrows.startAnimation(visible ? mPanningArrowsEnterAnimation
: mPanningArrowsExitAnimation);
mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
public boolean onZoomRingMoved(int deltaX, int deltaY) {
WindowManager.LayoutParams lp = mContainerLayoutParams;
Rect ownerBounds = mOwnerViewBounds;
int zoomRingLeft = mZoomRing.getLeft();
int zoomRingTop = mZoomRing.getTop();
int newX = lp.x + deltaX;
int newZoomRingX = newX + zoomRingLeft;
newZoomRingX = (newZoomRingX <= ownerBounds.left) ? ownerBounds.left :
(newZoomRingX + mZoomRingWidth > ownerBounds.right) ?
ownerBounds.right - mZoomRingWidth : newZoomRingX;
lp.x = newZoomRingX - zoomRingLeft;
int newY = lp.y + deltaY;
int newZoomRingY = newY + zoomRingTop;
newZoomRingY = (newZoomRingY <= ownerBounds.top) ? ownerBounds.top :
(newZoomRingY + mZoomRingHeight > ownerBounds.bottom) ?
ownerBounds.bottom - mZoomRingHeight : newZoomRingY;
lp.y = newZoomRingY - zoomRingTop;
mWindowManager.updateViewLayout(mContainer, lp);
// Check for pan
boolean horizontalPanning = true;
int leftGap = newZoomRingX - ownerBounds.left;
if (leftGap < MAX_PAN_GAP) {
if (shouldPan(leftGap)) {
mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap));
}
} else {
int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft);
if (rightGap < MAX_PAN_GAP) {
if (shouldPan(rightGap)) {
mPanner.setHorizontalStrength(getStrengthFromGap(rightGap));
}
} else {
mPanner.setHorizontalStrength(0);
horizontalPanning = false;
}
}
int topGap = newZoomRingY - ownerBounds.top;
if (topGap < MAX_PAN_GAP) {
if (shouldPan(topGap)) {
mPanner.setVerticalStrength(-getStrengthFromGap(topGap));
}
} else {
int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop);
if (bottomGap < MAX_PAN_GAP) {
if (shouldPan(bottomGap)) {
mPanner.setVerticalStrength(getStrengthFromGap(bottomGap));
}
} else {
mPanner.setVerticalStrength(0);
if (!horizontalPanning) {
// Neither are panning, reset any timer to start pan mode
mTouchingEdgeStartTime = 0;
mPanningEnabledForThisInteraction = false;
mPanner.stop();
}
}
}
return true;
}
private boolean shouldPan(int gap) {
if (mPanningEnabledForThisInteraction) return true;
if (gap < MAX_INITIATE_PAN_GAP) {
long time = SystemClock.elapsedRealtime();
if (mTouchingEdgeStartTime != 0 &&
mTouchingEdgeStartTime + INITIATE_PAN_DELAY < time) {
mPanningEnabledForThisInteraction = true;
return true;
} else if (mTouchingEdgeStartTime == 0) {
mTouchingEdgeStartTime = time;
} else {
}
} else {
// Moved away from the initiate pan gap, so reset the timer
mTouchingEdgeStartTime = 0;
}
return false;
}
public void onZoomRingMovingStopped() {
mPanner.stop();
setPanningArrowsVisible(false);
if (mCallback != null) {
mCallback.onEndPan();
}
}
private int getStrengthFromGap(int gap) {
return gap > MAX_PAN_GAP ? 0 :
(MAX_PAN_GAP - gap) * 100 / MAX_PAN_GAP;
}
public void onZoomRingThumbDraggingStarted() {
if (mCallback != null) {
mCallback.onBeginDrag();
}
}
public boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle) {
if (mCallback != null) {
int deltaZoomLevel = -numLevels;
int globalZoomCenterX = mContainerLayoutParams.x + mZoomRing.getLeft() +
mZoomRingWidth / 2;
int globalZoomCenterY = mContainerLayoutParams.y + mZoomRing.getTop() +
mZoomRingHeight / 2;
return mCallback.onDragZoom(deltaZoomLevel,
globalZoomCenterX - mOwnerViewBounds.left,
globalZoomCenterY - mOwnerViewBounds.top,
(float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER,
(float) curAngle / ZoomRing.RADIAN_INT_MULTIPLIER);
}
return false;
}
public void onZoomRingThumbDraggingStopped() {
if (mCallback != null) {
mCallback.onEndDrag();
}
}
public void onZoomRingDismissed(boolean dismissImmediately) {
if (dismissImmediately) {
mHandler.removeMessages(MSG_DISMISS_ZOOM_RING);
setVisible(false);
} else {
dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY);
}
}
public void onRingDown(int tickAngle, int touchAngle) {
}
public boolean onTouch(View v, MotionEvent event) {
if (sTutorialDialog != null && sTutorialDialog.isShowing() &&
SystemClock.elapsedRealtime() - sTutorialShowTime >= TUTORIAL_MIN_DISPLAY_TIME) {
finishZoomTutorial();
}
int action = event.getAction();
if (mReleaseTouchListenerOnUp) {
// The ring was 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;
}
View targetView = mTouchTargetView;
switch (action) {
case MotionEvent.ACTION_DOWN:
targetView = getViewForTouch((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 = mContainerLayoutParams.x + mTouchTargetLocationInWindow[0];
int targetViewRawY = mContainerLayoutParams.y + mTouchTargetLocationInWindow[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);
boolean retValue = targetView.dispatchTouchEvent(containerEvent);
containerEvent.recycle();
return retValue;
} else {
if (action == MotionEvent.ACTION_DOWN) {
dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY);
}
return false;
}
}
private void setTouchTargetView(View view) {
mTouchTargetView = view;
if (view != null) {
view.getLocationInWindow(mTouchTargetLocationInWindow);
}
}
/**
* 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 getViewForTouch(int rawX, int rawY) {
// Check to see if it is touching the ring
int containerCenterX = mContainerLayoutParams.x + mContainer.getWidth() / 2;
int containerCenterY = mContainerLayoutParams.y + mContainer.getHeight() / 2;
int distanceFromCenterX = rawX - containerCenterX;
int distanceFromCenterY = rawY - containerCenterY;
int zoomRingRadius = mZoomRingWidth / 2 - ZOOM_RING_RADIUS_INSET;
if (distanceFromCenterX * distanceFromCenterX +
distanceFromCenterY * distanceFromCenterY <=
zoomRingRadius * zoomRingRadius) {
return mZoomRing;
}
// Check to see if it is touching any other clickable View.
// Reverse order so the child drawn on top gets first dibs.
int containerCoordsX = rawX - mContainerLayoutParams.x;
int containerCoordsY = rawY - mContainerLayoutParams.y;
Rect frame = mTempRect;
for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
View child = mContainer.getChildAt(i);
if (child == mZoomRing || child.getVisibility() != View.VISIBLE ||
!child.isClickable()) {
continue;
}
child.getHitRect(frame);
if (frame.contains(containerCoordsX, containerCoordsY)) {
return child;
}
}
return null;
}
/** Steals key events from the owner view. */
public boolean onKey(View v, int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
// Eat these
return true;
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
// Keep the zoom alive a little longer
dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT);
// They started zooming, hide the thumb arrows
mZoomRing.setThumbArrowsVisible(false);
if (mCallback != null && event.getAction() == KeyEvent.ACTION_DOWN) {
mCallback.onSimpleZoom(keyCode == KeyEvent.KEYCODE_DPAD_UP);
}
return true;
}
return false;
}
private void onScrollerTick() {
if (!mScroller.computeScrollOffset() || !mIsZoomRingVisible) return;
mContainerLayoutParams.x = mScroller.getCurrX();
mContainerLayoutParams.y = mScroller.getCurrY();
mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
mHandler.sendEmptyMessage(MSG_SCROLLER_TICK);
}
private void onPostConfigurationChanged() {
dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT);
refreshPositioningVariables();
ensureZoomRingIsCentered();
}
/*
* This is static so Activities can call this instead of the Views
* (Activities usually do not have a reference to the ZoomRingController
* instance.)
*/
/**
* Shows a "tutorial" (some text) to the user teaching her the new zoom
* invocation method. Must call from the main thread.
* <p>
* It checks the global system setting to ensure this has not been seen
* before. Furthermore, if the application does not have privilege to write
* to the system settings, it will store this bit locally in a shared
* preference.
*
* @hide This should only be used by our main apps--browser, maps, and
* gallery
*/
public static void showZoomTutorialOnce(Context context) {
ContentResolver cr = context.getContentResolver();
if (Settings.System.getInt(cr, SETTING_NAME_SHOWN_TOAST, 0) == 1) {
return;
}
SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE);
if (sp.getInt(SETTING_NAME_SHOWN_TOAST, 0) == 1) {
return;
}
if (sTutorialDialog != null && sTutorialDialog.isShowing()) {
sTutorialDialog.dismiss();
}
sTutorialDialog = new AlertDialog.Builder(context)
.setMessage(
com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short)
.setIcon(0)
.create();
Window window = sTutorialDialog.getWindow();
window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND |
WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
sTutorialDialog.show();
sTutorialShowTime = SystemClock.elapsedRealtime();
}
public void finishZoomTutorial() {
if (sTutorialDialog == null) return;
sTutorialDialog.dismiss();
sTutorialDialog = null;
// Record that they have seen the tutorial
try {
Settings.System.putInt(mContext.getContentResolver(), SETTING_NAME_SHOWN_TOAST, 1);
} catch (SecurityException e) {
/*
* The app does not have permission to clear this global flag, make
* sure the user does not see the message when he comes back to this
* same app at least.
*/
SharedPreferences sp = mContext.getSharedPreferences("_zoom", Context.MODE_PRIVATE);
sp.edit().putInt(SETTING_NAME_SHOWN_TOAST, 1).commit();
}
}
public void setPannerStartVelocity(float startVelocity) {
mPanner.mStartVelocity = startVelocity;
}
public void setPannerAcceleration(float acceleration) {
mPanner.mAcceleration = acceleration;
}
public void setPannerMaxVelocity(float maxVelocity) {
mPanner.mMaxVelocity = maxVelocity;
}
public void setPannerStartAcceleratingDuration(int duration) {
mPanner.mStartAcceleratingDuration = duration;
}
private class Panner implements Runnable {
private static final int RUN_DELAY = 15;
private static final float STOP_SLOWDOWN = 0.8f;
private final Handler mUiHandler = new Handler();
private int mVerticalStrength;
private int mHorizontalStrength;
private boolean mStopping;
/** The time this current pan started. */
private long mStartTime;
/** The time of the last callback to pan the map/browser/etc. */
private long mPreviousCallbackTime;
// TODO Adjust to be DPI safe
private float mStartVelocity = 135;
private float mAcceleration = 160;
private float mMaxVelocity = 1000;
private int mStartAcceleratingDuration = 700;
private float mVelocity;
/** -100 (full left) to 0 (none) to 100 (full right) */
public void setHorizontalStrength(int horizontalStrength) {
if (mHorizontalStrength == 0 && mVerticalStrength == 0 && horizontalStrength != 0) {
start();
} else if (mVerticalStrength == 0 && horizontalStrength == 0) {
stop();
}
mHorizontalStrength = horizontalStrength;
mStopping = false;
}
/** -100 (full up) to 0 (none) to 100 (full down) */
public void setVerticalStrength(int verticalStrength) {
if (mHorizontalStrength == 0 && mVerticalStrength == 0 && verticalStrength != 0) {
start();
} else if (mHorizontalStrength == 0 && verticalStrength == 0) {
stop();
}
mVerticalStrength = verticalStrength;
mStopping = false;
}
private void start() {
mUiHandler.post(this);
mPreviousCallbackTime = 0;
mStartTime = 0;
}
public void stop() {
mStopping = true;
}
public void run() {
if (mStopping) {
mHorizontalStrength *= STOP_SLOWDOWN;
mVerticalStrength *= STOP_SLOWDOWN;
}
if (mHorizontalStrength == 0 && mVerticalStrength == 0) {
return;
}
boolean firstRun = mPreviousCallbackTime == 0;
long curTime = SystemClock.elapsedRealtime();
int panAmount = getPanAmount(mPreviousCallbackTime, curTime);
mPreviousCallbackTime = curTime;
if (firstRun) {
mStartTime = curTime;
mVelocity = mStartVelocity;
} else {
int panX = panAmount * mHorizontalStrength / 100;
int panY = panAmount * mVerticalStrength / 100;
if (mCallback != null) {
mCallback.onPan(panX, panY);
}
}
mUiHandler.postDelayed(this, RUN_DELAY);
}
private int getPanAmount(long previousTime, long currentTime) {
if (mVelocity > mMaxVelocity) {
mVelocity = mMaxVelocity;
} else if (mVelocity < mMaxVelocity) {
// See if it's time to add in some acceleration
if (currentTime - mStartTime > mStartAcceleratingDuration) {
mVelocity += (currentTime - previousTime) * mAcceleration / 1000;
}
}
return (int) ((currentTime - previousTime) * mVelocity) / 1000;
}
}
public interface OnZoomListener {
void onBeginDrag();
boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle,
float curAngle);
void onEndDrag();
void onSimpleZoom(boolean deltaZoomLevel);
void onBeginPan();
boolean onPan(int deltaX, int deltaY);
void onEndPan();
void onCenter(int x, int y);
void onVisibilityChanged(boolean visible);
}
}