blob: a29e1a0ec38347d26d641a29e8ae0e2b7c6f4f8c [file] [log] [blame]
package android.widget;
import com.android.internal.R;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RotateDrawable;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
/**
* @hide
*/
public class ZoomRing extends View {
// TODO: move to ViewConfiguration?
static final int DOUBLE_TAP_DISMISS_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
// TODO: get from theme
private static final String TAG = "ZoomRing";
// TODO: Temporary until the trail is done
private static final boolean DRAW_TRAIL = false;
// TODO: xml
private static final int THUMB_DISTANCE = 63;
/** To avoid floating point calculations, we multiply radians by this value. */
public static final int RADIAN_INT_MULTIPLIER = 10000;
public static final int RADIAN_INT_ERROR = 100;
/** PI using our multiplier. */
public static final int PI_INT_MULTIPLIED = (int) (Math.PI * RADIAN_INT_MULTIPLIER);
public static final int TWO_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED * 2;
/** PI/2 using our multiplier. */
private static final int HALF_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED / 2;
private int mZeroAngle = HALF_PI_INT_MULTIPLIED * 3;
private static final int THUMB_GRAB_SLOP = PI_INT_MULTIPLIED / 8;
private static final int THUMB_DRAG_SLOP = PI_INT_MULTIPLIED / 12;
/**
* Includes error because we compare this to the result of
* getDelta(getClosestTickeAngle(..), oldAngle) which ends up having some
* rounding error.
*/
private static final int MAX_ABS_JUMP_DELTA_ANGLE = (2 * PI_INT_MULTIPLIED / 3) +
RADIAN_INT_ERROR;
/** The cached X of our center. */
private int mCenterX;
/** The cached Y of our center. */
private int mCenterY;
/** The angle of the thumb (in int radians) */
private int mThumbAngle;
private int mThumbHalfWidth;
private int mThumbHalfHeight;
private int mThumbCwBound = Integer.MIN_VALUE;
private int mThumbCcwBound = Integer.MIN_VALUE;
private boolean mEnforceMaxAbsJump = true;
/** The inner radius of the track. */
private int mBoundInnerRadiusSquared = 0;
/** The outer radius of the track. */
private int mBoundOuterRadiusSquared = Integer.MAX_VALUE;
private int mPreviousWidgetDragX;
private int mPreviousWidgetDragY;
private boolean mDrawThumb = true;
private Drawable mThumbDrawable;
/** Shown beneath the thumb if we can still zoom in. */
private Drawable mThumbPlusArrowDrawable;
/** Shown beneath the thumb if we can still zoom out. */
private Drawable mThumbMinusArrowDrawable;
private static final int THUMB_ARROW_PLUS = 1 << 0;
private static final int THUMB_ARROW_MINUS = 1 << 1;
/** Bitwise-OR of {@link #THUMB_ARROW_MINUS} and {@link #THUMB_ARROW_PLUS} */
private int mThumbArrowsToDraw;
private static final int THUMB_ARROWS_FADE_DURATION = 300;
private long mThumbArrowsFadeStartTime;
private int mThumbArrowsAlpha = 255;
private static final int MODE_IDLE = 0;
/**
* User has his finger down somewhere on the ring (besides the thumb) and we
* are waiting for him to move the slop amount before considering him in the
* drag thumb state.
*/
private static final int MODE_WAITING_FOR_DRAG_THUMB = 5;
private static final int MODE_DRAG_THUMB = 1;
/**
* User has his finger down, but we are waiting for him to pass the touch
* slop before going into the #MODE_MOVE_ZOOM_RING. This is a good time to
* show the movable hint.
*/
private static final int MODE_WAITING_FOR_MOVE_ZOOM_RING = 4;
private static final int MODE_MOVE_ZOOM_RING = 2;
private static final int MODE_TAP_DRAG = 3;
/** Ignore the touch interaction. Reset to MODE_IDLE after up/cancel. */
private static final int MODE_IGNORE_UNTIL_UP = 6;
private int mMode;
private long mPreviousUpTime;
private int mPreviousDownX;
private int mPreviousDownY;
private int mWaitingForDragThumbDownAngle;
private OnZoomRingCallback mCallback;
private int mPreviousCallbackAngle;
private int mCallbackThreshold = Integer.MAX_VALUE;
private boolean mResetThumbAutomatically = true;
private int mThumbDragStartAngle;
private final int mTouchSlop;
private Drawable mTrail;
private double mAcculumalatedTrailAngle;
private Scroller mThumbScroller;
private static final int MSG_THUMB_SCROLLER_TICK = 1;
private static final int MSG_THUMB_ARROWS_FADE_TICK = 2;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_THUMB_SCROLLER_TICK:
onThumbScrollerTick();
break;
case MSG_THUMB_ARROWS_FADE_TICK:
onThumbArrowsFadeTick();
break;
}
}
};
public ZoomRing(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
mTouchSlop = viewConfiguration.getScaledTouchSlop();
// TODO get drawables from style instead
Resources res = context.getResources();
mThumbDrawable = res.getDrawable(R.drawable.zoom_ring_thumb);
mThumbPlusArrowDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_plus_arrow_rotatable).
mutate();
mThumbMinusArrowDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_minus_arrow_rotatable).
mutate();
if (DRAW_TRAIL) {
mTrail = res.getDrawable(R.drawable.zoom_ring_trail).mutate();
}
// TODO: add padding to drawable
setBackgroundResource(R.drawable.zoom_ring_track);
// TODO get from style
setRingBounds(43, Integer.MAX_VALUE);
mThumbHalfHeight = mThumbDrawable.getIntrinsicHeight() / 2;
mThumbHalfWidth = mThumbDrawable.getIntrinsicWidth() / 2;
mCallbackThreshold = PI_INT_MULTIPLIED / 6;
}
public ZoomRing(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZoomRing(Context context) {
this(context, null);
}
public void setCallback(OnZoomRingCallback callback) {
mCallback = callback;
}
// TODO: rename
public void setCallbackThreshold(int callbackThreshold) {
mCallbackThreshold = callbackThreshold;
}
// TODO: from XML too
public void setRingBounds(int innerRadius, int outerRadius) {
mBoundInnerRadiusSquared = innerRadius * innerRadius;
if (mBoundInnerRadiusSquared < innerRadius) {
// Prevent overflow
mBoundInnerRadiusSquared = Integer.MAX_VALUE;
}
mBoundOuterRadiusSquared = outerRadius * outerRadius;
if (mBoundOuterRadiusSquared < outerRadius) {
// Prevent overflow
mBoundOuterRadiusSquared = Integer.MAX_VALUE;
}
}
public void setThumbClockwiseBound(int angle) {
if (angle < 0) {
mThumbCwBound = Integer.MIN_VALUE;
} else {
mThumbCwBound = getClosestTickAngle(angle);
}
setEnforceMaxAbsJump();
}
public void setThumbCounterclockwiseBound(int angle) {
if (angle < 0) {
mThumbCcwBound = Integer.MIN_VALUE;
} else {
mThumbCcwBound = getClosestTickAngle(angle);
}
setEnforceMaxAbsJump();
}
private void setEnforceMaxAbsJump() {
// If there are bounds in both direction, there is no reason to restrict
// the amount that a user can absolute jump to
mEnforceMaxAbsJump =
mThumbCcwBound == Integer.MIN_VALUE || mThumbCwBound == Integer.MIN_VALUE;
}
public int getThumbAngle() {
return mThumbAngle;
}
public void setThumbAngle(int angle) {
angle = getValidAngle(angle);
mPreviousCallbackAngle = getClosestTickAngle(angle);
setThumbAngleAuto(angle, false, false);
}
/**
* Sets the thumb angle. If already animating, will continue the animation,
* otherwise it will do a direct jump.
*
* @param angle
* @param useDirection Whether to use the ccw parameter
* @param ccw Whether going counterclockwise (only used if useDirection is true)
*/
private void setThumbAngleAuto(int angle, boolean useDirection, boolean ccw) {
if (mThumbScroller == null
|| mThumbScroller.isFinished()
|| Math.abs(getDelta(angle, getThumbScrollerAngle())) < THUMB_GRAB_SLOP) {
setThumbAngleInt(angle);
} else {
if (useDirection) {
setThumbAngleAnimated(angle, 0, ccw);
} else {
setThumbAngleAnimated(angle, 0);
}
}
}
private void setThumbAngleInt(int angle) {
mThumbAngle = angle;
int unoffsetAngle = angle + mZeroAngle;
int thumbCenterX = (int) (Math.cos(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) *
THUMB_DISTANCE) + mCenterX;
int thumbCenterY = (int) (Math.sin(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) *
THUMB_DISTANCE) * -1 + mCenterY;
mThumbDrawable.setBounds(thumbCenterX - mThumbHalfWidth,
thumbCenterY - mThumbHalfHeight,
thumbCenterX + mThumbHalfWidth,
thumbCenterY + mThumbHalfHeight);
if (mThumbArrowsToDraw > 0) {
setThumbArrowsAngle(angle);
}
if (DRAW_TRAIL) {
double degrees;
degrees = Math.min(359.0, Math.abs(mAcculumalatedTrailAngle));
int level = (int) (10000.0 * degrees / 360.0);
mTrail.setLevel((int) (10000.0 *
(-Math.toDegrees(angle / (double) RADIAN_INT_MULTIPLIER) -
degrees + 90) / 360.0));
((RotateDrawable) mTrail).getDrawable().setLevel(level);
}
invalidate();
}
/**
*
* @param angle
* @param duration The animation duration, or 0 for the default duration.
*/
public void setThumbAngleAnimated(int angle, int duration) {
// The angle when going from the current angle to the new angle
int deltaAngle = getDelta(mThumbAngle, angle);
// Counter clockwise if the new angle is more the current angle
boolean counterClockwise = deltaAngle > 0;
if (deltaAngle > PI_INT_MULTIPLIED || deltaAngle < -PI_INT_MULTIPLIED) {
// It's quicker to go the other direction
counterClockwise = !counterClockwise;
}
setThumbAngleAnimated(angle, duration, counterClockwise);
}
public void setThumbAngleAnimated(int angle, int duration, boolean counterClockwise) {
if (mThumbScroller == null) {
mThumbScroller = new Scroller(mContext);
}
int startAngle = mThumbAngle;
int endAngle = getValidAngle(angle);
int deltaAngle = getDelta(startAngle, endAngle, counterClockwise);
if (startAngle + deltaAngle < 0) {
// Keep our angles positive
startAngle += TWO_PI_INT_MULTIPLIED;
}
if (!mThumbScroller.isFinished()) {
duration = mThumbScroller.getDuration() - mThumbScroller.timePassed();
} else if (duration == 0) {
duration = getAnimationDuration(deltaAngle);
}
mThumbScroller.startScroll(startAngle, 0, deltaAngle, 0, duration);
onThumbScrollerTick();
}
private int getAnimationDuration(int deltaAngle) {
if (deltaAngle < 0) deltaAngle *= -1;
return 300 + deltaAngle * 300 / RADIAN_INT_MULTIPLIER;
}
private void onThumbScrollerTick() {
if (!mThumbScroller.computeScrollOffset()) return;
setThumbAngleInt(getThumbScrollerAngle());
mHandler.sendEmptyMessage(MSG_THUMB_SCROLLER_TICK);
}
private int getThumbScrollerAngle() {
return mThumbScroller.getCurrX() % TWO_PI_INT_MULTIPLIED;
}
public void resetThumbAngle(int angle) {
mPreviousCallbackAngle = angle;
setThumbAngleInt(angle);
}
public void resetThumbAngle() {
if (mResetThumbAutomatically) {
resetThumbAngle(0);
}
}
public void setResetThumbAutomatically(boolean resetThumbAutomatically) {
mResetThumbAutomatically = resetThumbAutomatically;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
// Cache the center point
mCenterX = (right - left) / 2;
mCenterY = (bottom - top) / 2;
// Done here since we now have center, which is needed to calculate some
// aux info for thumb angle
if (mThumbAngle == Integer.MIN_VALUE) {
resetThumbAngle();
}
if (DRAW_TRAIL) {
mTrail.setBounds(0, 0, right - left, bottom - top);
}
mThumbPlusArrowDrawable.setBounds(0, 0, right - left, bottom - top);
mThumbMinusArrowDrawable.setBounds(0, 0, right - left, bottom - top);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return handleTouch(event.getAction(), event.getEventTime(),
(int) event.getX(), (int) event.getY(), (int) event.getRawX(),
(int) event.getRawY());
}
private void resetState() {
mMode = MODE_IDLE;
mPreviousWidgetDragX = mPreviousWidgetDragY = Integer.MIN_VALUE;
mAcculumalatedTrailAngle = 0.0;
}
public void setTapDragMode(boolean tapDragMode, int x, int y) {
resetState();
mMode = tapDragMode ? MODE_TAP_DRAG : MODE_IDLE;
if (tapDragMode) {
onThumbDragStarted(getAngle(x - mCenterX, y - mCenterY));
}
}
public boolean handleTouch(int action, long time, int x, int y, int rawX, int rawY) {
switch (action) {
case MotionEvent.ACTION_DOWN:
if (time - mPreviousUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT) {
mCallback.onZoomRingDismissed(true);
onTouchUp(time);
// Dismissing, so halt here
return true;
}
mCallback.onUserInteractionStarted();
mPreviousDownX = x;
mPreviousDownY = y;
resetState();
// Fall through to code below switch (since the down is used for
// jumping to the touched tick)
break;
case MotionEvent.ACTION_MOVE:
if (mMode == MODE_IGNORE_UNTIL_UP) return true;
// Fall through to code below switch
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
onTouchUp(time);
return true;
default:
return false;
}
// local{X,Y} will be where the center of the widget is (0,0)
int localX = x - mCenterX;
int localY = y - mCenterY;
boolean isTouchingThumb = true;
boolean isInRingBounds = true;
int touchAngle = getAngle(localX, localY);
int radiusSquared = localX * localX + localY * localY;
if (radiusSquared < mBoundInnerRadiusSquared ||
radiusSquared > mBoundOuterRadiusSquared) {
// Out-of-bounds
isTouchingThumb = false;
isInRingBounds = false;
}
int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle);
int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ?
deltaThumbAndTouch : -deltaThumbAndTouch;
if (isTouchingThumb &&
absoluteDeltaThumbAndTouch > THUMB_GRAB_SLOP) {
// Didn't grab close enough to the thumb
isTouchingThumb = false;
}
if (mMode == MODE_IDLE) {
if (isTouchingThumb) {
// They grabbed the thumb
mMode = MODE_DRAG_THUMB;
onThumbDragStarted(touchAngle);
} else if (isInRingBounds) {
// They tapped somewhere else on the ring
int tickAngle = getClosestTickAngle(touchAngle);
int deltaThumbAndTick = getDelta(mThumbAngle, tickAngle);
int boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick);
if (mEnforceMaxAbsJump) {
// Enforcing the max jump
if (deltaThumbAndTick > MAX_ABS_JUMP_DELTA_ANGLE ||
deltaThumbAndTick < -MAX_ABS_JUMP_DELTA_ANGLE) {
// Trying to jump too far, ignore this touch interaction
mMode = MODE_IGNORE_UNTIL_UP;
return true;
}
// Make sure we only let them jump within bounds
if (boundAngle != Integer.MIN_VALUE) {
tickAngle = boundAngle;
}
} else {
// Not enforcing the max jump, but we have to make sure
// we're getting to the tapped angle by going through the
// in-bounds region
if (boundAngle != Integer.MIN_VALUE) {
// Going this direction hits a bound, let's go the opposite direction
boolean oldDirectionIsCcw = deltaThumbAndTick > 0;
deltaThumbAndTick = getDelta(mThumbAngle, tickAngle, !oldDirectionIsCcw);
boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick);
if (boundAngle != Integer.MIN_VALUE) {
// Not allowed to be here, it is between two bounds
mMode = MODE_IGNORE_UNTIL_UP;
return true;
}
}
}
mMode = MODE_WAITING_FOR_DRAG_THUMB;
mWaitingForDragThumbDownAngle = touchAngle;
boolean ccw = deltaThumbAndTick > 0;
setThumbAngleAnimated(tickAngle, 0, ccw);
// Our thumb scrolling animation takes us from mThumbAngle to tickAngle
onThumbDragStarted(mThumbAngle);
onThumbDragged(tickAngle, true, ccw);
} else {
// They tapped somewhere else
mMode = MODE_WAITING_FOR_MOVE_ZOOM_RING;
mCallback.onZoomRingSetMovableHintVisible(true);
}
} else if (mMode == MODE_WAITING_FOR_DRAG_THUMB) {
int deltaDownAngle = getDelta(mWaitingForDragThumbDownAngle, touchAngle);
if ((deltaDownAngle < -THUMB_DRAG_SLOP || deltaDownAngle > THUMB_DRAG_SLOP) &&
isDeltaInBounds(mWaitingForDragThumbDownAngle, deltaDownAngle)) {
mMode = MODE_DRAG_THUMB;
}
} else if (mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) {
if (Math.abs(x - mPreviousDownX) > mTouchSlop ||
Math.abs(y - mPreviousDownY) > mTouchSlop) {
/* Make sure the user has moved the slop amount before going into that mode. */
mMode = MODE_MOVE_ZOOM_RING;
mCallback.onZoomRingMovingStarted();
}
}
// Purposefully not an "else if"
if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) {
if (isInRingBounds) {
onThumbDragged(touchAngle, false, false);
}
} else if (mMode == MODE_MOVE_ZOOM_RING) {
onZoomRingMoved(rawX, rawY);
}
return true;
}
private void onTouchUp(long time) {
if (mMode == MODE_MOVE_ZOOM_RING || mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) {
mCallback.onZoomRingSetMovableHintVisible(false);
if (mMode == MODE_MOVE_ZOOM_RING) {
mCallback.onZoomRingMovingStopped();
}
} else if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG ||
mMode == MODE_WAITING_FOR_DRAG_THUMB) {
onThumbDragStopped();
if (mMode == MODE_DRAG_THUMB) {
// Animate back to a tick
setThumbAngleAnimated(mPreviousCallbackAngle, 0);
}
}
mPreviousUpTime = time;
mCallback.onUserInteractionStopped();
}
private boolean isDeltaInBounds(int startAngle, int deltaAngle) {
return getBoundIfExceeds(startAngle, deltaAngle) == Integer.MIN_VALUE;
}
private int getBoundIfExceeds(int startAngle, int deltaAngle) {
if (deltaAngle > 0) {
// Counterclockwise movement
if (mThumbCcwBound != Integer.MIN_VALUE &&
getDelta(startAngle, mThumbCcwBound, true) < deltaAngle) {
return mThumbCcwBound;
}
} else if (deltaAngle < 0) {
// Clockwise movement, both of these will be negative
int deltaThumbAndBound = getDelta(startAngle, mThumbCwBound, false);
if (mThumbCwBound != Integer.MIN_VALUE &&
deltaThumbAndBound > deltaAngle) {
// Tapped outside of the bound in that direction
return mThumbCwBound;
}
}
return Integer.MIN_VALUE;
}
private int getDelta(int startAngle, int endAngle, boolean useDirection, boolean ccw) {
return useDirection ? getDelta(startAngle, endAngle, ccw) : getDelta(startAngle, endAngle);
}
/**
* Gets the smallest delta between two angles, and infers the direction
* based on the shortest path between the two angles. If going from
* startAngle to endAngle is counterclockwise, the result will be positive.
* If it is clockwise, the result will be negative.
*
* @param startAngle The start angle.
* @param endAngle The end angle.
* @return The difference in angles.
*/
private int getDelta(int startAngle, int endAngle) {
int largerAngle, smallerAngle;
if (endAngle > startAngle) {
largerAngle = endAngle;
smallerAngle = startAngle;
} else {
largerAngle = startAngle;
smallerAngle = endAngle;
}
int delta = largerAngle - smallerAngle;
if (delta <= PI_INT_MULTIPLIED) {
// If going clockwise, negate the delta
return startAngle == largerAngle ? -delta : delta;
} else {
// The other direction is the delta we want (it includes the
// discontinuous 0-2PI angle)
delta = TWO_PI_INT_MULTIPLIED - delta;
// If going clockwise, negate the delta
return startAngle == smallerAngle ? -delta : delta;
}
}
/**
* Gets the delta between two angles in the direction specified.
*
* @param startAngle The start angle.
* @param endAngle The end angle.
* @param counterClockwise The direction to take when computing the delta.
* @return The difference in angles in the given direction.
*/
private int getDelta(int startAngle, int endAngle, boolean counterClockwise) {
int delta = endAngle - startAngle;
if (!counterClockwise && delta > 0) {
// Crossed the discontinuous 0/2PI angle, take the leftover slice of
// the pie and negate it
return -TWO_PI_INT_MULTIPLIED + delta;
} else if (counterClockwise && delta < 0) {
// Crossed the discontinuous 0/2PI angle, take the leftover slice of
// the pie (and ensure it is positive)
return TWO_PI_INT_MULTIPLIED + delta;
} else {
return delta;
}
}
private void onThumbDragStarted(int startAngle) {
setThumbArrowsVisible(false);
mThumbDragStartAngle = startAngle;
mCallback.onZoomRingThumbDraggingStarted();
}
private void onThumbDragged(int touchAngle, boolean useDirection, boolean ccw) {
boolean animateThumbToNewAngle = false;
int totalDeltaAngle;
totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw);
int fuzzyCallbackThreshold = (int) (mCallbackThreshold * 0.65f);
if (totalDeltaAngle >= fuzzyCallbackThreshold
|| totalDeltaAngle <= -fuzzyCallbackThreshold) {
if (!useDirection) {
// Set ccw to match the direction found by getDelta
ccw = totalDeltaAngle > 0;
}
/*
* When the user slides the thumb through the tick that corresponds
* to a zoom bound, we don't want to abruptly stop there. Instead,
* let the user slide it to the next tick, and then animate it back
* to the original zoom bound tick. Because of this, we make sure
* the delta from the bound is more than halfway to the next tick.
* We make sure the bound is between the touch and the previous
* callback to ensure we just passed the bound.
*/
int oldTouchAngle = touchAngle;
if (ccw && mThumbCcwBound != Integer.MIN_VALUE) {
int deltaCcwBoundAndTouch =
getDelta(mThumbCcwBound, touchAngle, useDirection, true);
if (deltaCcwBoundAndTouch >= mCallbackThreshold / 2) {
// The touch has past a bound
int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle,
touchAngle, useDirection, true);
if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) {
// The bound is between the previous callback angle and the touch
touchAngle = mThumbCcwBound;
// We're moving the touch BACK to the bound, so opposite direction
ccw = false;
}
}
} else if (!ccw && mThumbCwBound != Integer.MIN_VALUE) {
// See block above for general comments
int deltaCwBoundAndTouch =
getDelta(mThumbCwBound, touchAngle, useDirection, false);
if (deltaCwBoundAndTouch <= -mCallbackThreshold / 2) {
int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle,
touchAngle, useDirection, false);
/*
* Both of these will be negative since we got delta in
* clockwise direction, and we want the magnitude of
* deltaPreviousCbAndTouch to be greater than the magnitude
* of deltaCwBoundAndTouch
*/
if (deltaPreviousCbAndTouch <= deltaCwBoundAndTouch) {
touchAngle = mThumbCwBound;
ccw = true;
}
}
}
if (touchAngle != oldTouchAngle) {
// We bounded the touch angle
totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw);
animateThumbToNewAngle = true;
mMode = MODE_IGNORE_UNTIL_UP;
}
// Prevent it from jumping too far
if (mEnforceMaxAbsJump) {
if (totalDeltaAngle <= -MAX_ABS_JUMP_DELTA_ANGLE) {
totalDeltaAngle = -MAX_ABS_JUMP_DELTA_ANGLE;
animateThumbToNewAngle = true;
} else if (totalDeltaAngle >= MAX_ABS_JUMP_DELTA_ANGLE) {
totalDeltaAngle = MAX_ABS_JUMP_DELTA_ANGLE;
animateThumbToNewAngle = true;
}
}
/*
* We need to cover the edge case of a user grabbing the thumb,
* going into the center of the widget, and then coming out from the
* center to an angle that's slightly below the angle he's trying to
* hit. If we do int division, we'll end up with one level lower
* than the one he was going for.
*/
int deltaLevels = Math.round((float) totalDeltaAngle / mCallbackThreshold);
if (deltaLevels != 0) {
boolean canStillZoom = mCallback.onZoomRingThumbDragged(
deltaLevels, mThumbDragStartAngle, touchAngle);
// TODO: we're trying the haptics to see how it goes with
// users, so we're ignoring the settings (for now)
performHapticFeedback(HapticFeedbackConstants.ZOOM_RING_TICK,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING |
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
// Set the callback angle to the actual angle based on how many delta levels we gave
mPreviousCallbackAngle = getValidAngle(
mPreviousCallbackAngle + (deltaLevels * mCallbackThreshold));
}
}
if (DRAW_TRAIL) {
int deltaAngle = getDelta(mThumbAngle, touchAngle, useDirection, ccw);
mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER);
}
if (animateThumbToNewAngle) {
if (useDirection) {
setThumbAngleAnimated(touchAngle, 0, ccw);
} else {
setThumbAngleAnimated(touchAngle, 0);
}
} else {
setThumbAngleAuto(touchAngle, useDirection, ccw);
}
}
private int getValidAngle(int invalidAngle) {
if (invalidAngle < 0) {
return (invalidAngle % TWO_PI_INT_MULTIPLIED) + TWO_PI_INT_MULTIPLIED;
} else if (invalidAngle >= TWO_PI_INT_MULTIPLIED) {
return invalidAngle % TWO_PI_INT_MULTIPLIED;
} else {
return invalidAngle;
}
}
private int getClosestTickAngle(int angle) {
int smallerAngleDistance = angle % mCallbackThreshold;
int smallerAngle = angle - smallerAngleDistance;
if (smallerAngleDistance < mCallbackThreshold / 2) {
// Closer to the smaller angle
return smallerAngle;
} else {
// Closer to the bigger angle (premodding)
return (smallerAngle + mCallbackThreshold) % TWO_PI_INT_MULTIPLIED;
}
}
private void onThumbDragStopped() {
mCallback.onZoomRingThumbDraggingStopped();
}
private void onZoomRingMoved(int x, int y) {
if (mPreviousWidgetDragX != Integer.MIN_VALUE) {
int deltaX = x - mPreviousWidgetDragX;
int deltaY = y - mPreviousWidgetDragY;
mCallback.onZoomRingMoved(deltaX, deltaY);
}
mPreviousWidgetDragX = x;
mPreviousWidgetDragY = y;
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
mCallback.onZoomRingDismissed(true);
}
}
private int getAngle(int localX, int localY) {
int radians = (int) (Math.atan2(localY, localX) * RADIAN_INT_MULTIPLIER);
// Convert from [-pi,pi] to {0,2pi]
if (radians < 0) {
radians = -radians;
} else if (radians > 0) {
radians = 2 * PI_INT_MULTIPLIED - radians;
} else {
radians = 0;
}
radians = radians - mZeroAngle;
return radians >= 0 ? radians : radians + 2 * PI_INT_MULTIPLIED;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawThumb) {
if (DRAW_TRAIL) {
mTrail.draw(canvas);
}
if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) {
mThumbPlusArrowDrawable.draw(canvas);
}
if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) {
mThumbMinusArrowDrawable.draw(canvas);
}
mThumbDrawable.draw(canvas);
}
}
private void setThumbArrowsAngle(int angle) {
int level = -angle * 10000 / ZoomRing.TWO_PI_INT_MULTIPLIED;
mThumbPlusArrowDrawable.setLevel(level);
mThumbMinusArrowDrawable.setLevel(level);
}
public void setThumbArrowsVisible(boolean visible) {
if (visible) {
mThumbArrowsAlpha = 255;
int callbackAngle = mPreviousCallbackAngle;
if (callbackAngle < mThumbCwBound - RADIAN_INT_ERROR ||
callbackAngle > mThumbCwBound + RADIAN_INT_ERROR) {
mThumbPlusArrowDrawable.setAlpha(255);
mThumbArrowsToDraw |= THUMB_ARROW_PLUS;
} else {
mThumbArrowsToDraw &= ~THUMB_ARROW_PLUS;
}
if (callbackAngle < mThumbCcwBound - RADIAN_INT_ERROR ||
callbackAngle > mThumbCcwBound + RADIAN_INT_ERROR) {
mThumbMinusArrowDrawable.setAlpha(255);
mThumbArrowsToDraw |= THUMB_ARROW_MINUS;
} else {
mThumbArrowsToDraw &= ~THUMB_ARROW_MINUS;
}
invalidate();
} else if (mThumbArrowsAlpha == 255) {
// Only start fade if we're fully visible (otherwise another fade is happening already)
mThumbArrowsFadeStartTime = SystemClock.elapsedRealtime();
onThumbArrowsFadeTick();
}
}
private void onThumbArrowsFadeTick() {
if (mThumbArrowsAlpha <= 0) {
mThumbArrowsToDraw = 0;
return;
}
mThumbArrowsAlpha = (int)
(255 - (255 * (SystemClock.elapsedRealtime() - mThumbArrowsFadeStartTime)
/ THUMB_ARROWS_FADE_DURATION));
if (mThumbArrowsAlpha < 0) mThumbArrowsAlpha = 0;
if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) {
mThumbPlusArrowDrawable.setAlpha(mThumbArrowsAlpha);
invalidateDrawable(mThumbPlusArrowDrawable);
}
if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) {
mThumbMinusArrowDrawable.setAlpha(mThumbArrowsAlpha);
invalidateDrawable(mThumbMinusArrowDrawable);
}
if (!mHandler.hasMessages(MSG_THUMB_ARROWS_FADE_TICK)) {
mHandler.sendEmptyMessage(MSG_THUMB_ARROWS_FADE_TICK);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setThumbArrowsAngle(mThumbAngle);
setThumbArrowsVisible(true);
}
public interface OnZoomRingCallback {
void onZoomRingSetMovableHintVisible(boolean visible);
void onZoomRingMovingStarted();
boolean onZoomRingMoved(int deltaX, int deltaY);
void onZoomRingMovingStopped();
void onZoomRingThumbDraggingStarted();
boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle);
void onZoomRingThumbDraggingStopped();
void onZoomRingDismissed(boolean dismissImmediately);
void onUserInteractionStarted();
void onUserInteractionStopped();
}
private static void printAngle(String angleName, int angle) {
Log.d(TAG, angleName + ": " + (long) angle * 180 / PI_INT_MULTIPLIED);
}
}