summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget/ZoomRing.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/widget/ZoomRing.java')
-rw-r--r--core/java/android/widget/ZoomRing.java1180
1 files changed, 1180 insertions, 0 deletions
diff --git a/core/java/android/widget/ZoomRing.java b/core/java/android/widget/ZoomRing.java
new file mode 100644
index 0000000..a5a867b
--- /dev/null
+++ b/core/java/android/widget/ZoomRing.java
@@ -0,0 +1,1180 @@
+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 mThumbVisible = 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 THUMB_PLUS_MINUS_DISTANCE = 69;
+ private static final int THUMB_PLUS_MINUS_OFFSET_ANGLE = TWO_PI_INT_MULTIPLIED / 11;
+ /** Drawn (without rotation) on top of the arrow. */
+ private Drawable mThumbPlusDrawable;
+ /** Drawn (without rotation) on top of the arrow. */
+ private Drawable mThumbMinusDrawable;
+
+ 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_AFTER_JUMP = 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 until the user touches the thumb again. */
+ private static final int MODE_IGNORE_UNTIL_TOUCHES_THUMB = 6;
+ private int mMode;
+
+ /** Records the last mode the user was in. */
+ private int mPreviousMode;
+
+ private long mPreviousCenterUpTime;
+ private int mPreviousDownX;
+ private int mPreviousDownY;
+
+ private int mWaitingForDragThumbDownAngle;
+
+ private OnZoomRingCallback mCallback;
+ private int mPreviousCallbackAngle;
+ private int mCallbackThreshold = Integer.MAX_VALUE;
+ /** If the user drags to within __% of a tick, snap to that tick. */
+ private int mFuzzyCallbackThreshold = Integer.MAX_VALUE;
+
+ private boolean mResetThumbAutomatically = true;
+ private int mThumbDragStartAngle;
+
+ private final int mTouchSlop;
+
+ private Drawable mTrail;
+ private double mAcculumalatedTrailAngle;
+
+ private Scroller mThumbScroller;
+
+ private boolean mVibration = true;
+
+ 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();
+ mThumbPlusDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_plus);
+ mThumbMinusDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_minus);
+ 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;
+
+ setCallbackThreshold(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;
+ mFuzzyCallbackThreshold = (int) (callbackThreshold * 0.65f);
+ }
+
+ public void setVibration(boolean vibrate) {
+ mVibration = vibrate;
+ }
+
+ public void setThumbVisible(boolean thumbVisible) {
+ if (mThumbVisible != thumbVisible) {
+ mThumbVisible = thumbVisible;
+ invalidate();
+ }
+ }
+
+ // 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);
+ setThumbAngleAnimated(angle, duration, deltaAngle > 0);
+ }
+
+ 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() {
+ if (mResetThumbAutomatically) {
+ mPreviousCallbackAngle = 0;
+ setThumbAngleInt(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);
+ }
+
+ // These drawables are the same size as the track
+ mThumbPlusArrowDrawable.setBounds(0, 0, right - left, bottom - top);
+ mThumbMinusArrowDrawable.setBounds(0, 0, right - left, bottom - top);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+// Log.d(TAG, "History size: " + event.getHistorySize());
+
+ return handleTouch(event.getAction(), event.getEventTime(),
+ (int) event.getX(), (int) event.getY(), (int) event.getRawX(),
+ (int) event.getRawY());
+ }
+
+ private void resetToIdle() {
+ setMode(MODE_IDLE);
+ mPreviousWidgetDragX = mPreviousWidgetDragY = Integer.MIN_VALUE;
+ mAcculumalatedTrailAngle = 0.0;
+ }
+
+ public void setTapDragMode(boolean tapDragMode, int x, int y) {
+ resetToIdle();
+ if (tapDragMode) {
+ setMode(MODE_TAP_DRAG);
+ mCallback.onUserInteractionStarted();
+ onThumbDragStarted(getAngle(x - mCenterX, y - mCenterY));
+ } else {
+ onTouchUp(SystemClock.elapsedRealtime(), true);
+ }
+ }
+
+ public boolean handleTouch(int action, long time, int x, int y, int rawX, int rawY) {
+ // local{X,Y} will be where the center of the widget is (0,0)
+ int localX = x - mCenterX;
+ int localY = y - mCenterY;
+
+ /*
+ * If we are not drawing the thumb, there is no way for the user to be
+ * touching the thumb. Also, if this is the case, assume they are not
+ * touching the ring (so the user cannot absolute set the thumb, and
+ * there will be a larger touch region for going into the move-ring
+ * mode).
+ */
+ boolean isTouchingThumb = mThumbVisible;
+ boolean isTouchingRing = mThumbVisible;
+
+ int touchAngle = getAngle(localX, localY);
+// printAngle("touchAngle", touchAngle);
+// printAngle("mThumbAngle", mThumbAngle);
+// printAngle("mPreviousCallbackAngle", mPreviousCallbackAngle);
+// Log.d(TAG, "");
+
+
+ int radiusSquared = localX * localX + localY * localY;
+ if (radiusSquared < mBoundInnerRadiusSquared ||
+ radiusSquared > mBoundOuterRadiusSquared) {
+ // Out-of-bounds
+ isTouchingThumb = false;
+ isTouchingRing = false;
+ }
+
+ if (isTouchingThumb) {
+ int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle);
+ int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ?
+ deltaThumbAndTouch : -deltaThumbAndTouch;
+ if (absoluteDeltaThumbAndTouch > THUMB_GRAB_SLOP) {
+ // Didn't grab close enough to the thumb
+ isTouchingThumb = false;
+ }
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (!isTouchingRing &&
+ (time - mPreviousCenterUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT)) {
+ // Make sure the double-tap is in the center of the widget (and not on the ring)
+ mCallback.onZoomRingDismissed(true);
+ onTouchUp(time, isTouchingRing);
+
+ // Dismissing, so halt here
+ return true;
+ }
+
+ resetToIdle();
+ mCallback.onUserInteractionStarted();
+ mPreviousDownX = x;
+ mPreviousDownY = y;
+ // Fall through to code below switch (since the down is used for
+ // jumping to the touched tick)
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ // Fall through to code below switch
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ onTouchUp(time, isTouchingRing);
+ return true;
+
+ default:
+ return false;
+ }
+
+ if (mMode == MODE_IDLE) {
+ if (isTouchingThumb) {
+ // They grabbed the thumb
+ setMode(MODE_DRAG_THUMB);
+ onThumbDragStarted(touchAngle);
+
+ } else if (isTouchingRing) {
+ // 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
+ setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB);
+ return true;
+ }
+
+ if (boundAngle != Integer.MIN_VALUE) {
+ // Cap the user's jump to the bound
+ 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) {
+ // Cannot get to the tapped location because it is out-of-bounds
+ setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB);
+ return true;
+ }
+ }
+ }
+
+ setMode(MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP);
+ mWaitingForDragThumbDownAngle = touchAngle;
+ boolean ccw = deltaThumbAndTick > 0;
+ setThumbAngleAnimated(tickAngle, 0, ccw);
+
+ /*
+ * Our thumb scrolling animation takes us from mThumbAngle to
+ * tickAngle, so manifest that as the user dragging the thumb
+ * there.
+ */
+ onThumbDragStarted(mThumbAngle);
+ // We know which direction we want to go
+ onThumbDragged(tickAngle, true, ccw);
+
+ } else {
+ // They tapped somewhere else on the widget
+ setMode(MODE_WAITING_FOR_MOVE_ZOOM_RING);
+ mCallback.onZoomRingSetMovableHintVisible(true);
+ }
+
+ } else if (mMode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) {
+ int deltaDownAngle = getDelta(mWaitingForDragThumbDownAngle, touchAngle);
+ if ((deltaDownAngle < -THUMB_DRAG_SLOP || deltaDownAngle > THUMB_DRAG_SLOP) &&
+ isDeltaInBounds(mWaitingForDragThumbDownAngle, deltaDownAngle)) {
+ setMode(MODE_DRAG_THUMB);
+
+ // No need to call onThumbDragStarted, since that was done when they tapped-to-jump
+ }
+
+ } 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. */
+ setMode(MODE_MOVE_ZOOM_RING);
+ mCallback.onZoomRingMovingStarted();
+ }
+ } else if (mMode == MODE_IGNORE_UNTIL_TOUCHES_THUMB) {
+ if (isTouchingThumb) {
+ // The user is back on the thumb, let's go back to the previous mode
+ setMode(mPreviousMode);
+ }
+ }
+
+ // Purposefully not an "else if"
+ if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) {
+ if (isTouchingRing) {
+ onThumbDragged(touchAngle, false, false);
+ }
+ } else if (mMode == MODE_MOVE_ZOOM_RING) {
+ onZoomRingMoved(rawX, rawY);
+ }
+
+ return true;
+ }
+
+ private void onTouchUp(long time, boolean isTouchingRing) {
+ int mode = mMode;
+ if (mode == MODE_IGNORE_UNTIL_TOUCHES_THUMB) {
+ // For cleaning up, pretend like the user was still in the previous mode
+ mode = mPreviousMode;
+ }
+
+ if (mode == MODE_MOVE_ZOOM_RING || mode == MODE_WAITING_FOR_MOVE_ZOOM_RING) {
+ mCallback.onZoomRingSetMovableHintVisible(false);
+ if (mode == MODE_MOVE_ZOOM_RING) {
+ mCallback.onZoomRingMovingStopped();
+ }
+ } else if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG ||
+ mode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) {
+ onThumbDragStopped();
+
+ if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG) {
+ // Animate back to a tick
+ setThumbAngleAnimated(mPreviousCallbackAngle, 0);
+ }
+ }
+ mCallback.onUserInteractionStopped();
+
+ if (!isTouchingRing) {
+ mPreviousCenterUpTime = time;
+ }
+ }
+
+ private void setMode(int mode) {
+ if (mode != mMode) {
+ mPreviousMode = mMode;
+ mMode = mode;
+ }
+ }
+
+ 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);
+ if (totalDeltaAngle >= mFuzzyCallbackThreshold
+ || totalDeltaAngle <= -mFuzzyCallbackThreshold) {
+
+ 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;
+ setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB);
+ }
+
+
+ // 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);
+
+ if (mVibration) {
+ // 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 void onThumbDragged(int touchAngle, boolean useDirection, boolean ccw) {
+// int deltaPrevCbAndTouch = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw);
+//
+// if (!useDirection) {
+// // Set ccw to match the direction found by getDelta
+// ccw = deltaPrevCbAndTouch > 0;
+// useDirection = true;
+// }
+//
+// boolean animateThumbToNewAngle = false;
+// boolean animationCcw = ccw;
+//
+// if (deltaPrevCbAndTouch >= mFuzzyCallbackThreshold
+// || deltaPrevCbAndTouch <= -mFuzzyCallbackThreshold) {
+//
+// /*
+// * 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, true, ccw);
+// if (deltaCcwBoundAndTouch >= mCallbackThreshold / 2) {
+// // The touch has past far enough from the bound
+// int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle,
+// touchAngle, true, ccw);
+// if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) {
+// // The bound is between the previous callback angle and the touch
+// // Cap to the bound
+// touchAngle = mThumbCcwBound;
+// /*
+// * We're moving the touch BACK to the bound, so animate
+// * back in the opposite direction that passed the bound.
+// */
+// animationCcw = false;
+// }
+// }
+// } else if (!ccw && mThumbCwBound != Integer.MIN_VALUE) {
+// // See block above for general comments
+// int deltaCwBoundAndTouch =
+// getDelta(mThumbCwBound, touchAngle, true, ccw);
+// if (deltaCwBoundAndTouch <= -mCallbackThreshold / 2) {
+// int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle,
+// touchAngle, true, ccw);
+// /*
+// * 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;
+// animationCcw = true;
+// }
+// }
+// }
+// if (touchAngle != oldTouchAngle) {
+// // We bounded the touch angle
+// deltaPrevCbAndTouch = getDelta(mPreviousCallbackAngle, touchAngle, true, ccw);
+// // Animate back to the bound
+// animateThumbToNewAngle = true;
+// // Disallow movement now
+// setMode(MODE_IGNORE_UNTIL_UP);
+// }
+//
+//
+// /*
+// * Prevent it from jumping too far (this could happen if the user
+// * goes through the center)
+// */
+//
+// if (mEnforceMaxAbsJump) {
+// if (deltaPrevCbAndTouch <= -MAX_ABS_JUMP_DELTA_ANGLE) {
+// deltaPrevCbAndTouch = -MAX_ABS_JUMP_DELTA_ANGLE;
+// animateThumbToNewAngle = true;
+// } else if (deltaPrevCbAndTouch >= MAX_ABS_JUMP_DELTA_ANGLE) {
+// deltaPrevCbAndTouch = 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) deltaPrevCbAndTouch / mCallbackThreshold);
+// if (deltaLevels != 0) {
+// boolean canStillZoom = mCallback.onZoomRingThumbDragged(
+// deltaLevels, mThumbDragStartAngle, touchAngle);
+//
+// if (mVibration) {
+// // 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, true, ccw);
+// mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER);
+// }
+//
+// if (animateThumbToNewAngle) {
+// setThumbAngleAnimated(touchAngle, 0, animationCcw);
+// } else {
+// /*
+// * Use regular ccw here because animationCcw will never have been
+// * changed if animateThumbToNewAngle is false
+// */
+// setThumbAngleAuto(touchAngle, true, 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 rawX, int rawY) {
+ if (mPreviousWidgetDragX != Integer.MIN_VALUE) {
+ int deltaX = rawX - mPreviousWidgetDragX;
+ int deltaY = rawY - mPreviousWidgetDragY;
+
+ mCallback.onZoomRingMoved(deltaX, deltaY, rawX, rawY);
+ }
+
+ mPreviousWidgetDragX = rawX;
+ mPreviousWidgetDragY = rawY;
+ }
+
+ @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 (mThumbVisible) {
+ if (DRAW_TRAIL) {
+ mTrail.draw(canvas);
+ }
+ if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) {
+ mThumbPlusArrowDrawable.draw(canvas);
+ mThumbPlusDrawable.draw(canvas);
+ }
+ if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) {
+ mThumbMinusArrowDrawable.draw(canvas);
+ mThumbMinusDrawable.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);
+
+ // Assume it is a square
+ int halfSideLength = mThumbPlusDrawable.getIntrinsicHeight() / 2;
+ int unoffsetAngle = angle + mZeroAngle;
+
+ int plusCenterX = (int) (Math.cos(1f * (unoffsetAngle - THUMB_PLUS_MINUS_OFFSET_ANGLE)
+ / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) + mCenterX;
+ int plusCenterY = (int) (Math.sin(1f * (unoffsetAngle - THUMB_PLUS_MINUS_OFFSET_ANGLE)
+ / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) * -1 + mCenterY;
+ mThumbPlusDrawable.setBounds(plusCenterX - halfSideLength,
+ plusCenterY - halfSideLength,
+ plusCenterX + halfSideLength,
+ plusCenterY + halfSideLength);
+
+ int minusCenterX = (int) (Math.cos(1f * (unoffsetAngle + THUMB_PLUS_MINUS_OFFSET_ANGLE)
+ / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) + mCenterX;
+ int minusCenterY = (int) (Math.sin(1f * (unoffsetAngle + THUMB_PLUS_MINUS_OFFSET_ANGLE)
+ / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) * -1 + mCenterY;
+ mThumbMinusDrawable.setBounds(minusCenterX - halfSideLength,
+ minusCenterY - halfSideLength,
+ minusCenterX + halfSideLength,
+ minusCenterY + halfSideLength);
+ }
+
+ 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);
+ mThumbPlusDrawable.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);
+ mThumbMinusDrawable.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);
+ mThumbPlusDrawable.setAlpha(mThumbArrowsAlpha);
+ invalidateDrawable(mThumbPlusDrawable);
+ invalidateDrawable(mThumbPlusArrowDrawable);
+ }
+ if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) {
+ mThumbMinusArrowDrawable.setAlpha(mThumbArrowsAlpha);
+ mThumbMinusDrawable.setAlpha(mThumbArrowsAlpha);
+ invalidateDrawable(mThumbMinusDrawable);
+ 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, int rawX, int rawY);
+ 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);
+ }
+}