diff options
Diffstat (limited to 'core/java/android/widget')
-rw-r--r-- | core/java/android/widget/AbsListView.java | 15 | ||||
-rw-r--r-- | core/java/android/widget/FastScroller.java | 10 | ||||
-rw-r--r-- | core/java/android/widget/GridView.java | 4 | ||||
-rw-r--r-- | core/java/android/widget/ImageView.java | 6 | ||||
-rw-r--r-- | core/java/android/widget/ListView.java | 26 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 158 | ||||
-rw-r--r-- | core/java/android/widget/ZoomRing.java | 398 | ||||
-rw-r--r-- | core/java/android/widget/ZoomRingController.java | 381 |
8 files changed, 692 insertions, 306 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 9da78d0..d72570a 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2766,6 +2766,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private void showPopup() { // Make sure we have a window before showing the popup if (getWindowVisibility() == View.VISIBLE) { + createTextFilter(true); positionPopup(false); // Make sure we get focus if we are showing the popup checkFocus(); @@ -2913,8 +2914,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (mPopup == null) { Context c = getContext(); PopupWindow p = new PopupWindow(c); - LayoutInflater layoutInflater = (LayoutInflater) c - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LayoutInflater layoutInflater = (LayoutInflater) + c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mTextFilter = (EditText) layoutInflater.inflate( com.android.internal.R.layout.typing_filter, null); mTextFilter.addTextChangedListener(this); @@ -3136,6 +3137,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ int viewType; + /** + * When this boolean is set, the view has been added to the AbsListView + * at least once. It is used to know whether headers/footers have already + * been added to the list view and whether they should be treated as + * recycled views or not. + */ + boolean recycledHeaderFooter; + public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } @@ -3269,7 +3278,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (lp != null && lp.viewType != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. - activeViews[i] = getChildAt(i); + activeViews[i] = child; } } } diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index 57e21e4..0a552e8 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -62,6 +62,8 @@ class FastScroller { private int mVisibleItem; private Paint mPaint; private int mListOffset; + private int mItemCount = -1; + private boolean mLongList; private Object [] mSections; private String mSectionText; @@ -219,8 +221,12 @@ class FastScroller { void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - // Are there enough pages to require fast scroll? - if (visibleItemCount > 0 && totalItemCount / visibleItemCount < MIN_PAGES) { + // Are there enough pages to require fast scroll? Recompute only if total count changes + if (mItemCount != totalItemCount && visibleItemCount > 0) { + mItemCount = totalItemCount; + mLongList = mItemCount / visibleItemCount >= MIN_PAGES; + } + if (!mLongList) { if (mState != STATE_NONE) { setState(STATE_NONE); } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 6bbf062..11fab8f 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -929,6 +929,7 @@ public class GridView extends AbsListView { if (p == null) { p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); + child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(0); @@ -1328,11 +1329,8 @@ public class GridView extends AbsListView { */ @Override void setSelectionInt(int position) { - mBlockLayoutRequests = true; setNextSelectedPositionInt(position); layoutChildren(); - - mBlockLayoutRequests = false; } @Override diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index a4523b9..480b0b8 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -333,6 +333,12 @@ public class ImageView extends View { resizeFromDrawable(); } + /** + * Sets the image level, when it is constructed from a + * {@link android.graphics.drawable.LevelListDrawable}. + * + * @param level The new level for the image. + */ @android.view.RemotableViewMethod public void setImageLevel(int level) { mLevel = level; diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 404a4ee..c2f3a85 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -26,7 +26,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.SparseBooleanArray; -import android.util.SparseArray; import android.view.FocusFinder; import android.view.KeyEvent; import android.view.MotionEvent; @@ -1040,6 +1039,7 @@ public class ListView extends AbsListView { if (p == null) { p = new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); + child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(position); @@ -1320,6 +1320,8 @@ public class ListView extends AbsListView { final boolean blockLayoutRequests = mBlockLayoutRequests; if (!blockLayoutRequests) { mBlockLayoutRequests = true; + } else { + return; } try { @@ -1429,15 +1431,12 @@ public class ListView extends AbsListView { // we can remember the focused view to restore after relayout if the // data hasn't changed, or if the focused position is a header or footer if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) { - focusLayoutRestoreDirectChild = getFocusedChild(); - if (focusLayoutRestoreDirectChild != null) { - - // remember the specific view that had focus - focusLayoutRestoreView = findFocus(); - if (focusLayoutRestoreView != null) { - // tell it we are going to mess with it - focusLayoutRestoreView.onStartTemporaryDetach(); - } + focusLayoutRestoreDirectChild = focusedChild; + // remember the specific view that had focus + focusLayoutRestoreView = findFocus(); + if (focusLayoutRestoreView != null) { + // tell it we are going to mess with it + focusLayoutRestoreView.onStartTemporaryDetach(); } } requestFocus(); @@ -1657,9 +1656,12 @@ public class ListView extends AbsListView { } p.viewType = mAdapter.getItemViewType(position); - if (recycled) { + if (recycled || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); } else { + if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + p.recycledHeaderFooter = true; + } addViewInLayout(child, flowDown ? -1 : 0, p, true); } @@ -1766,10 +1768,8 @@ public class ListView extends AbsListView { */ @Override void setSelectionInt(int position) { - mBlockLayoutRequests = true; setNextSelectedPositionInt(position); layoutChildren(); - mBlockLayoutRequests = false; } /** diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 080f3de..3f4912f 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -33,6 +33,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; +import android.os.ResultReceiver; import android.os.SystemClock; import android.os.Message; import android.text.BoringLayout; @@ -234,6 +235,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private CharWrapper mCharWrapper = null; private boolean mSelectionMoved = false; + private boolean mTouchFocusSelectedAll = false; private Marquee mMarquee; private boolean mRestartMarquee; @@ -837,6 +839,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (password) { setTransformationMethod(PasswordTransformationMethod.getInstance()); typefaceIndex = MONOSPACE; + } else if ((mInputType&(EditorInfo.TYPE_MASK_CLASS + |EditorInfo.TYPE_MASK_VARIATION)) + == (EditorInfo.TYPE_CLASS_TEXT + |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { + typefaceIndex = MONOSPACE; } setTypefaceByIndex(typefaceIndex, styleIndex); @@ -2244,6 +2251,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int selEnd; CharSequence text; boolean frozenWithFocus; + CharSequence error; SavedState(Parcelable superState) { super(superState); @@ -2256,6 +2264,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener out.writeInt(selEnd); out.writeInt(frozenWithFocus ? 1 : 0); TextUtils.writeToParcel(text, out, flags); + + if (error == null) { + out.writeInt(0); + } else { + out.writeInt(1); + TextUtils.writeToParcel(error, out, flags); + } } @Override @@ -2286,6 +2301,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selEnd = in.readInt(); frozenWithFocus = (in.readInt() != 0); text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + + if (in.readInt() != 0) { + error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + } } } @@ -2338,6 +2357,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ss.frozenWithFocus = true; } + ss.error = mError; + return ss; } @@ -2383,6 +2404,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } + + if (ss.error != null) { + setError(ss.error); + } } /** @@ -2799,8 +2824,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setInputType(int type) { setInputType(type, false); - final boolean isPassword = (type&(EditorInfo.TYPE_MASK_CLASS - |EditorInfo.TYPE_MASK_VARIATION)) + final int variation = type&(EditorInfo.TYPE_MASK_CLASS + |EditorInfo.TYPE_MASK_VARIATION); + final boolean isPassword = variation == (EditorInfo.TYPE_CLASS_TEXT |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD); boolean forceUpdate = false; @@ -2809,8 +2835,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setTypefaceByIndex(MONOSPACE, 0); } else if (mTransformation == PasswordTransformationMethod.getInstance()) { // We need to clean up if we were previously in password mode. - setTypefaceByIndex(-1, -1); + if (variation != (EditorInfo.TYPE_CLASS_TEXT + |EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)) { + setTypefaceByIndex(-1, -1); + } forceUpdate = true; + } else if (variation == (EditorInfo.TYPE_CLASS_TEXT + |EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)) { + setTypefaceByIndex(MONOSPACE, 0); } boolean multiLine = (type&(EditorInfo.TYPE_MASK_CLASS @@ -2999,23 +3031,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } - - if (actionCode == EditorInfo.IME_ACTION_NEXT && - (ict != null || !shouldAdvanceFocusOnEnter())) { - // This is the default handling for the NEXT action, to advance - // focus. Note that for backwards compatibility we don't do this + if (ict != null || !shouldAdvanceFocusOnEnter()) { + // This is the handling for some default action. + // Note that for backwards compatibility we don't do this // default handling if explicit ime options have not been given, - // and we do not advance by default on an enter key -- in that - // case, we want to turn this into the normal enter key codes that - // an app may be expecting. - View v = focusSearch(FOCUS_DOWN); - if (v != null) { - if (!v.requestFocus(FOCUS_DOWN)) { - throw new IllegalStateException("focus search returned a view " + - "that wasn't able to take focus!"); + // to instead turn this into the normal enter key codes that an + // app may be expecting. + if (actionCode == EditorInfo.IME_ACTION_NEXT) { + View v = focusSearch(FOCUS_DOWN); + if (v != null) { + if (!v.requestFocus(FOCUS_DOWN)) { + throw new IllegalStateException("focus search returned a view " + + "that wasn't able to take focus!"); + } + } + return; + + } else if (actionCode == EditorInfo.IME_ACTION_DONE) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.hideSoftInputFromWindow(getWindowToken(), 0); } } - return; } Handler h = getHandler(); @@ -3998,7 +4035,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * but also in mail addresses and subjects which will display on multiple * lines but where it doesn't make sense to insert newlines. */ - protected boolean shouldAdvanceFocusOnEnter() { + private boolean shouldAdvanceFocusOnEnter() { if (mInput == null) { return false; } @@ -4192,6 +4229,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ super.onKeyUp(keyCode, event); return true; + } else if ((event.getFlags() + & KeyEvent.FLAG_SOFT_KEYBOARD) != 0) { + // No target for next focus, but make sure the IME + // if this came from it. + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.hideSoftInputFromWindow(getWindowToken(), 0); + } } } @@ -4234,9 +4279,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // An action has not been set, but the enter key will move to // the next focus, so set the action to that. outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; - if (!shouldAdvanceFocusOnEnter()) { - outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; - } + } else { + // An action has not been set, and there is no focus to move + // to, so let's just supply a "done" action. + outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; + } + if (!shouldAdvanceFocusOnEnter()) { + outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; } } outAttrs.hintText = mHint; @@ -5977,6 +6026,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mSelectAllOnFocus) { Selection.setSelection((Spannable) mText, 0, mText.length()); + mTouchFocusSelectedAll = true; } if (selMoved && selStart >= 0 && selEnd >= 0) { @@ -6078,12 +6128,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + class CommitSelectionReceiver extends ResultReceiver { + int mNewStart; + int mNewEnd; + + CommitSelectionReceiver() { + super(getHandler()); + } + + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode != InputMethodManager.RESULT_SHOWN) { + Selection.setSelection((Spannable)mText, mNewStart, mNewEnd); + } + } + } + @Override public boolean onTouchEvent(MotionEvent event) { - final boolean superResult = super.onTouchEvent(event); - final int action = event.getAction(); + if (action == MotionEvent.ACTION_DOWN) { + // Reset this state; it will be re-set if super.onTouchEvent + // causes focus to move to the view. + mTouchFocusSelectedAll = false; + } + final boolean superResult = super.onTouchEvent(event); + /* * Don't handle the release after a long press, because it will * move the selection away from whatever the menu action was @@ -6100,17 +6170,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mScrolled = false; } - boolean moved = mMovement.onTouchEvent(this, (Spannable) mText, event); + boolean handled = false; + + int oldSelStart = Selection.getSelectionStart(mText); + int oldSelEnd = Selection.getSelectionEnd(mText); + + handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); if (mText instanceof Editable && onCheckIsTextEditor()) { if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(this, 0); + + // This is going to be gross... if tapping on the text view + // causes the IME to be displayed, we don't want the selection + // to change. But the selection has already changed, and + // we won't know right away whether the IME is getting + // displayed, so... + + int newSelStart = Selection.getSelectionStart(mText); + int newSelEnd = Selection.getSelectionEnd(mText); + CommitSelectionReceiver csr = null; + if (newSelStart != oldSelStart || newSelEnd != oldSelEnd) { + csr = new CommitSelectionReceiver(); + csr.mNewStart = newSelStart; + csr.mNewEnd = newSelEnd; + } + + if (imm.showSoftInput(this, 0, csr) && csr != null) { + // The IME might get shown -- revert to the old + // selection, and change to the new when we finally + // find out of it is okay. + Selection.setSelection((Spannable)mText, oldSelStart, oldSelEnd); + handled = true; + } } } - if (moved) { + if (handled) { return true; } } @@ -6118,6 +6215,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } + /** + * Returns true, only while processing a touch gesture, if the initial + * touch down event caused focus to move to the text view and as a result + * it selected all of its text. + */ + public boolean didTouchFocusSelectAll() { + return mTouchFocusSelectedAll; + } + @Override public void cancelLongPress() { super.cancelLongPress(); diff --git a/core/java/android/widget/ZoomRing.java b/core/java/android/widget/ZoomRing.java index a5a867b..83a1225 100644 --- a/core/java/android/widget/ZoomRing.java +++ b/core/java/android/widget/ZoomRing.java @@ -1,9 +1,26 @@ +/* + * Copyright (C) 2009 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 com.android.internal.R; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.graphics.drawable.RotateDrawable; @@ -18,36 +35,47 @@ import android.view.View; import android.view.ViewConfiguration; /** + * A view that has a draggable thumb on a circle. + * * @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. */ + /** + * To avoid floating point calculations and int round-offs, we multiply + * radians by this value. + */ public static final int RADIAN_INT_MULTIPLIER = 10000; + /** The allowable margin of error when comparing two angles. */ 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 DOUBLE_TAP_DISMISS_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); + private final int mTouchSlop; + + /** The slop when the user is grabbing the thumb. */ private static final int THUMB_GRAB_SLOP = PI_INT_MULTIPLIED / 8; + /** The slop until a user starts dragging the thumb. */ private static final int THUMB_DRAG_SLOP = PI_INT_MULTIPLIED / 12; + /** The distance (in px) from the center of the ring to the center of the thumb. */ + private int mThumbDistance; + + /** The angle on a unit circle that is considered to be the zoom ring's 0 degree. */ + private int mZeroAngle = HALF_PI_INT_MULTIPLIED * 3; + /** + * The maximum delta angle that the thumb can move. The primary use is to + * ensure that when a user taps on the ring, the movement to reach that + * target angle is not ambiguous (for example, if the thumb is at 0 and he + * taps 180, should the thumb go clockwise or counterclockwise? + * <p> * Includes error because we compare this to the result of * getDelta(getClosestTickeAngle(..), oldAngle) which ends up having some * rounding error. @@ -55,58 +83,93 @@ public class ZoomRing extends View { private static final int MAX_ABS_JUMP_DELTA_ANGLE = (2 * PI_INT_MULTIPLIED / 3) + RADIAN_INT_ERROR; - /** The cached X of our center. */ + /** The cached X of the zoom ring's center (in zoom ring coordinates). */ private int mCenterX; - /** The cached Y of our center. */ + /** The cached Y of the zoom ring's center (in zoom ring coordinates). */ private int mCenterY; /** The angle of the thumb (in int radians) */ private int mThumbAngle; + /** The cached width/2 of the zoom ring. */ private int mThumbHalfWidth; + /** The cached height/2 of the zoom ring. */ private int mThumbHalfHeight; + /** + * The bound for the thumb's movement when it is being dragged clockwise. + * Can be Integer.MIN_VALUE if there is no bound in this direction. + */ private int mThumbCwBound = Integer.MIN_VALUE; + /** + * The bound for the thumb's movement when it is being dragged + * counterclockwise. Can be Integer.MIN_VALUE if there is no bound in this + * direction. + */ private int mThumbCcwBound = Integer.MIN_VALUE; + + /** + * Whether to enforce the maximum absolute jump delta. See + * {@link #MAX_ABS_JUMP_DELTA_ANGLE}. + */ private boolean mEnforceMaxAbsJump = true; /** The inner radius of the track. */ - private int mBoundInnerRadiusSquared = 0; + private int mTrackInnerRadius; + /** Cached square of the inner radius of the track. */ + private int mTrackInnerRadiusSquared; /** The outer radius of the track. */ - private int mBoundOuterRadiusSquared = Integer.MAX_VALUE; + private int mTrackOuterRadius; + /** Cached square of the outer radius of the track. */ + private int mTrackOuterRadiusSquared; + /** The raw X of where the widget previously was located. */ private int mPreviousWidgetDragX; + /** The raw Y of where the widget previously was located. */ private int mPreviousWidgetDragY; + /** Whether the thumb should be visible. */ private boolean mThumbVisible = true; + + /** The drawable for the thumb. */ private Drawable mThumbDrawable; /** Shown beneath the thumb if we can still zoom in. */ - private Drawable mThumbPlusArrowDrawable; + private Drawable mZoomInArrowDrawable; /** Shown beneath the thumb if we can still zoom out. */ - private Drawable mThumbMinusArrowDrawable; + private Drawable mZoomOutArrowDrawable; + + /** @see #mThumbArrowsToDraw */ private static final int THUMB_ARROW_PLUS = 1 << 0; + /** @see #mThumbArrowsToDraw */ private static final int THUMB_ARROW_MINUS = 1 << 1; /** Bitwise-OR of {@link #THUMB_ARROW_MINUS} and {@link #THUMB_ARROW_PLUS} */ private int mThumbArrowsToDraw; + + /** The duration for the thumb arrows fading out */ private static final int THUMB_ARROWS_FADE_DURATION = 300; + /** The time when the fade out started. */ private long mThumbArrowsFadeStartTime; + /** The current alpha for the thumb arrows. */ 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; + /** The distance from the center to the zoom arrow hints (usually plus and minus). */ + private int mZoomArrowHintDistance; + /** The offset angle from the thumb angle to draw the zoom arrow hints. */ + private int mZoomArrowHintOffsetAngle = TWO_PI_INT_MULTIPLIED / 11; /** Drawn (without rotation) on top of the arrow. */ - private Drawable mThumbPlusDrawable; + private Drawable mZoomInArrowHintDrawable; /** Drawn (without rotation) on top of the arrow. */ - private Drawable mThumbMinusDrawable; + private Drawable mZoomOutArrowHintDrawable; + /** Zoom ring is just chillin' */ 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; + /** User is dragging the thumb. */ private static final int MODE_DRAG_THUMB = 1; /** * User has his finger down, but we are waiting for him to pass the touch @@ -114,51 +177,65 @@ public class ZoomRing extends View { * show the movable hint. */ private static final int MODE_WAITING_FOR_MOVE_ZOOM_RING = 4; + /** User is moving the zoom ring. */ private static final int MODE_MOVE_ZOOM_RING = 2; + /** User is dragging the thumb via tap-drag. */ 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; + /** The current mode of interaction. */ private int mMode; - /** Records the last mode the user was in. */ private int mPreviousMode; - + + /** The previous time of the up-touch on the center. */ private long mPreviousCenterUpTime; + /** The previous X of down-touch. */ private int mPreviousDownX; + /** The previous Y of down-touch. */ private int mPreviousDownY; - private int mWaitingForDragThumbDownAngle; + /** The angle where the user first grabbed the thumb. */ + private int mInitialGrabThumbAngle; + /** The callback. */ private OnZoomRingCallback mCallback; - private int mPreviousCallbackAngle; - private int mCallbackThreshold = Integer.MAX_VALUE; + /** The tick angle that we previously called back with. */ + private int mPreviousCallbackTickAngle; + /** The delta angle between ticks. A tick is a callback point. */ + private int mTickDelta = Integer.MAX_VALUE; /** If the user drags to within __% of a tick, snap to that tick. */ - private int mFuzzyCallbackThreshold = Integer.MAX_VALUE; + private int mFuzzyTickDelta = Integer.MAX_VALUE; - private boolean mResetThumbAutomatically = true; + /** The angle where the thumb is officially starting to be dragged. */ private int mThumbDragStartAngle; - private final int mTouchSlop; - + /** The drawable for the zoom trail. */ private Drawable mTrail; + /** The accumulated angle for the trail. */ private double mAcculumalatedTrailAngle; + /** The animation-step tracker for scrolling the thumb to a particular position. */ private Scroller mThumbScroller; + /** Whether to ever vibrate when passing a tick. */ private boolean mVibration = true; - private static final int MSG_THUMB_SCROLLER_TICK = 1; - private static final int MSG_THUMB_ARROWS_FADE_TICK = 2; + /** The drawable used to hint that this can pan its owner. */ + private Drawable mPanningArrowsDrawable; + + private static final int MSG_THUMB_SCROLLER_STEP = 1; + private static final int MSG_THUMB_ARROWS_FADE_STEP = 2; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_THUMB_SCROLLER_TICK: - onThumbScrollerTick(); + case MSG_THUMB_SCROLLER_STEP: + onThumbScrollerStep(); break; - case MSG_THUMB_ARROWS_FADE_TICK: - onThumbArrowsFadeTick(); + case MSG_THUMB_ARROWS_FADE_STEP: + onThumbArrowsFadeStep(); break; } } @@ -170,50 +247,64 @@ public class ZoomRing extends View { ViewConfiguration viewConfiguration = ViewConfiguration.get(context); mTouchSlop = viewConfiguration.getScaledTouchSlop(); - // TODO get drawables from style instead + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ZoomRing, defStyle, 0); + mThumbDistance = (int) a.getDimension(R.styleable.ZoomRing_thumbDistance, 0); + setTrackRadii( + (int) a.getDimension(R.styleable.ZoomRing_trackInnerRadius, 0), + (int) a.getDimension(R.styleable.ZoomRing_trackOuterRadius, Integer.MAX_VALUE)); + mThumbDrawable = a.getDrawable(R.styleable.ZoomRing_thumbDrawable); + mZoomInArrowDrawable = a.getDrawable(R.styleable.ZoomRing_zoomInArrowDrawable); + mZoomOutArrowDrawable = a.getDrawable(R.styleable.ZoomRing_zoomOutArrowDrawable); + mZoomInArrowHintDrawable = a.getDrawable(R.styleable.ZoomRing_zoomInArrowHintDrawable); + mZoomOutArrowHintDrawable = a.getDrawable(R.styleable.ZoomRing_zoomOutArrowHintDrawable); + mZoomArrowHintDistance = + (int) a.getDimension(R.styleable.ZoomRing_zoomArrowHintDistance, 0); + mZoomArrowHintOffsetAngle = + (int) (a.getInteger(R.styleable.ZoomRing_zoomArrowHintOffsetAngle, 0) + * TWO_PI_INT_MULTIPLIED / 360); + mPanningArrowsDrawable = a.getDrawable(R.styleable.ZoomRing_panningArrowsDrawable); + a.recycle(); + 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) { + // TODO get drawables from style instead 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); + setTickDelta(PI_INT_MULTIPLIED / 6); } public ZoomRing(Context context, AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs, com.android.internal.R.attr.zoomRingStyle); } public ZoomRing(Context context) { this(context, null); } + public void setTrackDrawable(Drawable drawable) { + setBackgroundDrawable(drawable); + } + public void setCallback(OnZoomRingCallback callback) { mCallback = callback; } - // TODO: rename - public void setCallbackThreshold(int callbackThreshold) { - mCallbackThreshold = callbackThreshold; - mFuzzyCallbackThreshold = (int) (callbackThreshold * 0.65f); + /** + * Sets the distance between ticks. This will be used as a callback threshold. + * + * @param angle The angle between ticks. + */ + public void setTickDelta(int angle) { + mTickDelta = angle; + mFuzzyTickDelta = (int) (angle * 0.65f); } - public void setVibration(boolean vibrate) { - mVibration = vibrate; + public void setVibration(boolean vibration) { + mVibration = vibration; } public void setThumbVisible(boolean thumbVisible) { @@ -223,28 +314,42 @@ public class ZoomRing extends View { } } - // TODO: from XML too - public void setRingBounds(int innerRadius, int outerRadius) { - mBoundInnerRadiusSquared = innerRadius * innerRadius; - if (mBoundInnerRadiusSquared < innerRadius) { + public Drawable getPanningArrowsDrawable() { + return mPanningArrowsDrawable; + } + + public void setTrackRadii(int innerRadius, int outerRadius) { + mTrackInnerRadius = innerRadius; + mTrackOuterRadius = outerRadius; + + mTrackInnerRadiusSquared = innerRadius * innerRadius; + if (mTrackInnerRadiusSquared < innerRadius) { // Prevent overflow - mBoundInnerRadiusSquared = Integer.MAX_VALUE; + mTrackInnerRadiusSquared = Integer.MAX_VALUE; } - mBoundOuterRadiusSquared = outerRadius * outerRadius; - if (mBoundOuterRadiusSquared < outerRadius) { + mTrackOuterRadiusSquared = outerRadius * outerRadius; + if (mTrackOuterRadiusSquared < outerRadius) { // Prevent overflow - mBoundOuterRadiusSquared = Integer.MAX_VALUE; + mTrackOuterRadiusSquared = Integer.MAX_VALUE; } } + public int getTrackInnerRadius() { + return mTrackInnerRadius; + } + + public int getTrackOuterRadius() { + return mTrackOuterRadius; + } + public void setThumbClockwiseBound(int angle) { if (angle < 0) { mThumbCwBound = Integer.MIN_VALUE; } else { mThumbCwBound = getClosestTickAngle(angle); } - setEnforceMaxAbsJump(); + updateEnforceMaxAbsJump(); } public void setThumbCounterclockwiseBound(int angle) { @@ -253,14 +358,14 @@ public class ZoomRing extends View { } else { mThumbCcwBound = getClosestTickAngle(angle); } - setEnforceMaxAbsJump(); + updateEnforceMaxAbsJump(); } - private void setEnforceMaxAbsJump() { + private void updateEnforceMaxAbsJump() { // 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; + mThumbCcwBound == Integer.MIN_VALUE || mThumbCwBound == Integer.MIN_VALUE; } public int getThumbAngle() { @@ -269,7 +374,7 @@ public class ZoomRing extends View { public void setThumbAngle(int angle) { angle = getValidAngle(angle); - mPreviousCallbackAngle = getClosestTickAngle(angle); + mPreviousCallbackTickAngle = getClosestTickAngle(angle); setThumbAngleAuto(angle, false, false); } @@ -299,9 +404,9 @@ public class ZoomRing extends View { mThumbAngle = angle; int unoffsetAngle = angle + mZeroAngle; int thumbCenterX = (int) (Math.cos(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) * - THUMB_DISTANCE) + mCenterX; + mThumbDistance) + mCenterX; int thumbCenterY = (int) (Math.sin(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) * - THUMB_DISTANCE) * -1 + mCenterY; + mThumbDistance) * -1 + mCenterY; mThumbDrawable.setBounds(thumbCenterX - mThumbHalfWidth, thumbCenterY - mThumbHalfHeight, @@ -356,7 +461,7 @@ public class ZoomRing extends View { duration = getAnimationDuration(deltaAngle); } mThumbScroller.startScroll(startAngle, 0, deltaAngle, 0, duration); - onThumbScrollerTick(); + onThumbScrollerStep(); } private int getAnimationDuration(int deltaAngle) { @@ -364,10 +469,10 @@ public class ZoomRing extends View { return 300 + deltaAngle * 300 / RADIAN_INT_MULTIPLIER; } - private void onThumbScrollerTick() { + private void onThumbScrollerStep() { if (!mThumbScroller.computeScrollOffset()) return; setThumbAngleInt(getThumbScrollerAngle()); - mHandler.sendEmptyMessage(MSG_THUMB_SCROLLER_TICK); + mHandler.sendEmptyMessage(MSG_THUMB_SCROLLER_STEP); } private int getThumbScrollerAngle() { @@ -375,16 +480,10 @@ public class ZoomRing extends View { } public void resetThumbAngle() { - if (mResetThumbAutomatically) { - mPreviousCallbackAngle = 0; - setThumbAngleInt(0); - } + mPreviousCallbackTickAngle = 0; + setThumbAngleInt(0); } - public void setResetThumbAutomatically(boolean resetThumbAutomatically) { - mResetThumbAutomatically = resetThumbAutomatically; - } - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), @@ -411,14 +510,12 @@ public class ZoomRing extends View { } // 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); + mZoomInArrowDrawable.setBounds(0, 0, right - left, bottom - top); + mZoomOutArrowDrawable.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()); @@ -457,15 +554,10 @@ public class ZoomRing extends View { 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) { + if (radiusSquared < mTrackInnerRadiusSquared || + radiusSquared > mTrackOuterRadiusSquared) { // Out-of-bounds isTouchingThumb = false; isTouchingRing = false; @@ -486,7 +578,7 @@ public class ZoomRing extends View { 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); + mCallback.onZoomRingDismissed(); onTouchUp(time, isTouchingRing); // Dismissing, so halt here @@ -557,7 +649,7 @@ public class ZoomRing extends View { } setMode(MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP); - mWaitingForDragThumbDownAngle = touchAngle; + mInitialGrabThumbAngle = touchAngle; boolean ccw = deltaThumbAndTick > 0; setThumbAngleAnimated(tickAngle, 0, ccw); @@ -577,9 +669,9 @@ public class ZoomRing extends View { } } else if (mMode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) { - int deltaDownAngle = getDelta(mWaitingForDragThumbDownAngle, touchAngle); + int deltaDownAngle = getDelta(mInitialGrabThumbAngle, touchAngle); if ((deltaDownAngle < -THUMB_DRAG_SLOP || deltaDownAngle > THUMB_DRAG_SLOP) && - isDeltaInBounds(mWaitingForDragThumbDownAngle, deltaDownAngle)) { + isDeltaInBounds(mInitialGrabThumbAngle, deltaDownAngle)) { setMode(MODE_DRAG_THUMB); // No need to call onThumbDragStarted, since that was done when they tapped-to-jump @@ -591,6 +683,8 @@ public class ZoomRing extends View { /* Make sure the user has moved the slop amount before going into that mode. */ setMode(MODE_MOVE_ZOOM_RING); mCallback.onZoomRingMovingStarted(); + // Move the zoom ring so it is under the finger where the user first touched + mCallback.onZoomRingMoved(x - mPreviousDownX, y - mPreviousDownY, rawX, rawY); } } else if (mMode == MODE_IGNORE_UNTIL_TOUCHES_THUMB) { if (isTouchingThumb) { @@ -629,7 +723,7 @@ public class ZoomRing extends View { if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG) { // Animate back to a tick - setThumbAngleAnimated(mPreviousCallbackAngle, 0); + setThumbAngleAnimated(mPreviousCallbackTickAngle, 0); } } mCallback.onUserInteractionStopped(); @@ -741,9 +835,9 @@ public class ZoomRing extends View { boolean animateThumbToNewAngle = false; int totalDeltaAngle; - totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); - if (totalDeltaAngle >= mFuzzyCallbackThreshold - || totalDeltaAngle <= -mFuzzyCallbackThreshold) { + totalDeltaAngle = getDelta(mPreviousCallbackTickAngle, touchAngle, useDirection, ccw); + if (totalDeltaAngle >= mFuzzyTickDelta + || totalDeltaAngle <= -mFuzzyTickDelta) { if (!useDirection) { // Set ccw to match the direction found by getDelta @@ -763,9 +857,9 @@ public class ZoomRing extends View { if (ccw && mThumbCcwBound != Integer.MIN_VALUE) { int deltaCcwBoundAndTouch = getDelta(mThumbCcwBound, touchAngle, useDirection, true); - if (deltaCcwBoundAndTouch >= mCallbackThreshold / 2) { + if (deltaCcwBoundAndTouch >= mTickDelta / 2) { // The touch has past a bound - int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, + int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackTickAngle, touchAngle, useDirection, true); if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) { // The bound is between the previous callback angle and the touch @@ -778,8 +872,8 @@ public class ZoomRing extends View { // See block above for general comments int deltaCwBoundAndTouch = getDelta(mThumbCwBound, touchAngle, useDirection, false); - if (deltaCwBoundAndTouch <= -mCallbackThreshold / 2) { - int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, + if (deltaCwBoundAndTouch <= -mTickDelta / 2) { + int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackTickAngle, touchAngle, useDirection, false); /* * Both of these will be negative since we got delta in @@ -795,7 +889,7 @@ public class ZoomRing extends View { } if (touchAngle != oldTouchAngle) { // We bounded the touch angle - totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); + totalDeltaAngle = getDelta(mPreviousCallbackTickAngle, touchAngle, useDirection, ccw); animateThumbToNewAngle = true; setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); } @@ -819,7 +913,7 @@ public class ZoomRing extends View { * 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); + int deltaLevels = Math.round((float) totalDeltaAngle / mTickDelta); if (deltaLevels != 0) { boolean canStillZoom = mCallback.onZoomRingThumbDragged( deltaLevels, mThumbDragStartAngle, touchAngle); @@ -833,8 +927,8 @@ public class ZoomRing extends View { } // Set the callback angle to the actual angle based on how many delta levels we gave - mPreviousCallbackAngle = getValidAngle( - mPreviousCallbackAngle + (deltaLevels * mCallbackThreshold)); + mPreviousCallbackTickAngle = getValidAngle( + mPreviousCallbackTickAngle + (deltaLevels * mTickDelta)); } } @@ -993,14 +1087,14 @@ public class ZoomRing extends View { } private int getClosestTickAngle(int angle) { - int smallerAngleDistance = angle % mCallbackThreshold; + int smallerAngleDistance = angle % mTickDelta; int smallerAngle = angle - smallerAngleDistance; - if (smallerAngleDistance < mCallbackThreshold / 2) { + if (smallerAngleDistance < mTickDelta / 2) { // Closer to the smaller angle return smallerAngle; } else { // Closer to the bigger angle (premodding) - return (smallerAngle + mCallbackThreshold) % TWO_PI_INT_MULTIPLIED; + return (smallerAngle + mTickDelta) % TWO_PI_INT_MULTIPLIED; } } @@ -1025,7 +1119,7 @@ public class ZoomRing extends View { super.onWindowFocusChanged(hasWindowFocus); if (!hasWindowFocus) { - mCallback.onZoomRingDismissed(true); + mCallback.onZoomRingDismissed(); } } @@ -1054,12 +1148,12 @@ public class ZoomRing extends View { mTrail.draw(canvas); } if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) { - mThumbPlusArrowDrawable.draw(canvas); - mThumbPlusDrawable.draw(canvas); + mZoomInArrowDrawable.draw(canvas); + mZoomInArrowHintDrawable.draw(canvas); } if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) { - mThumbMinusArrowDrawable.draw(canvas); - mThumbMinusDrawable.draw(canvas); + mZoomOutArrowDrawable.draw(canvas); + mZoomOutArrowHintDrawable.draw(canvas); } mThumbDrawable.draw(canvas); } @@ -1067,48 +1161,48 @@ public class ZoomRing extends View { private void setThumbArrowsAngle(int angle) { int level = -angle * 10000 / ZoomRing.TWO_PI_INT_MULTIPLIED; - mThumbPlusArrowDrawable.setLevel(level); - mThumbMinusArrowDrawable.setLevel(level); + mZoomInArrowDrawable.setLevel(level); + mZoomOutArrowDrawable.setLevel(level); // Assume it is a square - int halfSideLength = mThumbPlusDrawable.getIntrinsicHeight() / 2; + int halfSideLength = mZoomInArrowHintDrawable.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, + int plusCenterX = (int) (Math.cos(1f * (unoffsetAngle - mZoomArrowHintOffsetAngle) + / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) + mCenterX; + int plusCenterY = (int) (Math.sin(1f * (unoffsetAngle - mZoomArrowHintOffsetAngle) + / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) * -1 + mCenterY; + mZoomInArrowHintDrawable.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, + int minusCenterX = (int) (Math.cos(1f * (unoffsetAngle + mZoomArrowHintOffsetAngle) + / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) + mCenterX; + int minusCenterY = (int) (Math.sin(1f * (unoffsetAngle + mZoomArrowHintOffsetAngle) + / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) * -1 + mCenterY; + mZoomOutArrowHintDrawable.setBounds(minusCenterX - halfSideLength, minusCenterY - halfSideLength, minusCenterX + halfSideLength, minusCenterY + halfSideLength); } - public void setThumbArrowsVisible(boolean visible) { + void setThumbArrowsVisible(boolean visible) { if (visible) { mThumbArrowsAlpha = 255; - int callbackAngle = mPreviousCallbackAngle; + int callbackAngle = mPreviousCallbackTickAngle; if (callbackAngle < mThumbCwBound - RADIAN_INT_ERROR || callbackAngle > mThumbCwBound + RADIAN_INT_ERROR) { - mThumbPlusArrowDrawable.setAlpha(255); - mThumbPlusDrawable.setAlpha(255); + mZoomInArrowDrawable.setAlpha(255); + mZoomInArrowHintDrawable.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); + mZoomOutArrowDrawable.setAlpha(255); + mZoomOutArrowHintDrawable.setAlpha(255); mThumbArrowsToDraw |= THUMB_ARROW_MINUS; } else { mThumbArrowsToDraw &= ~THUMB_ARROW_MINUS; @@ -1117,11 +1211,11 @@ public class ZoomRing extends View { } else if (mThumbArrowsAlpha == 255) { // Only start fade if we're fully visible (otherwise another fade is happening already) mThumbArrowsFadeStartTime = SystemClock.elapsedRealtime(); - onThumbArrowsFadeTick(); + onThumbArrowsFadeStep(); } } - private void onThumbArrowsFadeTick() { + private void onThumbArrowsFadeStep() { if (mThumbArrowsAlpha <= 0) { mThumbArrowsToDraw = 0; return; @@ -1132,20 +1226,20 @@ public class ZoomRing extends View { / 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); + mZoomInArrowDrawable.setAlpha(mThumbArrowsAlpha); + mZoomInArrowHintDrawable.setAlpha(mThumbArrowsAlpha); + invalidateDrawable(mZoomInArrowHintDrawable); + invalidateDrawable(mZoomInArrowDrawable); } if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) { - mThumbMinusArrowDrawable.setAlpha(mThumbArrowsAlpha); - mThumbMinusDrawable.setAlpha(mThumbArrowsAlpha); - invalidateDrawable(mThumbMinusDrawable); - invalidateDrawable(mThumbMinusArrowDrawable); + mZoomOutArrowDrawable.setAlpha(mThumbArrowsAlpha); + mZoomOutArrowHintDrawable.setAlpha(mThumbArrowsAlpha); + invalidateDrawable(mZoomOutArrowHintDrawable); + invalidateDrawable(mZoomOutArrowDrawable); } - if (!mHandler.hasMessages(MSG_THUMB_ARROWS_FADE_TICK)) { - mHandler.sendEmptyMessage(MSG_THUMB_ARROWS_FADE_TICK); + if (!mHandler.hasMessages(MSG_THUMB_ARROWS_FADE_STEP)) { + mHandler.sendEmptyMessage(MSG_THUMB_ARROWS_FADE_STEP); } } @@ -1168,7 +1262,7 @@ public class ZoomRing extends View { boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle); void onZoomRingThumbDraggingStopped(); - void onZoomRingDismissed(boolean dismissImmediately); + void onZoomRingDismissed(); void onUserInteractionStarted(); void onUserInteractionStopped(); diff --git a/core/java/android/widget/ZoomRingController.java b/core/java/android/widget/ZoomRingController.java index 19f66a0..3bf3b22 100644 --- a/core/java/android/widget/ZoomRingController.java +++ b/core/java/android/widget/ZoomRingController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2009 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. @@ -30,7 +30,8 @@ import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.provider.Settings; -import android.util.Log; +import android.util.DisplayMetrics; +import android.view.GestureDetector; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -44,11 +45,9 @@ 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 - * + * A controller to simplify the use of the zoom ring widget. + * <p> * If you are using this with a custom View, please call * {@link #setVisible(boolean) setVisible(false)} from the * {@link View#onDetachedFromWindow}. @@ -58,13 +57,7 @@ import android.view.animation.DecelerateInterpolator; 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; + // Temporary methods for different zoom types static int getZoomType(Context context) { return Settings.System.getInt(context.getContentResolver(), "zoom", 1); } @@ -75,19 +68,43 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return getZoomType(context) == 1; } - private static final int ZOOM_CONTROLS_TIMEOUT = - (int) ViewConfiguration.getZoomControlsTimeout(); + /** The duration for the animation to re-center the zoom ring. */ + private static final int RECENTERING_DURATION = 500; - // 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 + /** The inactivity timeout for the zoom ring. */ + private static final int INACTIVITY_TIMEOUT = + (int) ViewConfiguration.getZoomControlsTimeout(); + + /** + * The delay when the user taps outside to dismiss the zoom ring. This is + * because the user can do a second-tap to recenter the owner view instead + * of dismissing the zoom ring. + */ + private static final int OUTSIDE_TAP_DISMISS_DELAY = + ViewConfiguration.getDoubleTapTimeout() / 2; + + /** + * When the zoom ring is on the edge, this is the delay before we actually + * start panning the owner. + * @see #mInitiatePanGap + */ private static final int INITIATE_PAN_DELAY = 300; + /** + * While already panning, if the zoom ring remains this close to an edge, + * the owner will continue to be panned. + */ + private int mPanGap; + + /** To begin a pan, the zoom ring must be this close to an edge. */ + private int mInitiatePanGap; + + /** Initialized from ViewConfiguration. */ + private int mScaledTouchSlop; + + /** + * The setting name that tracks whether we've shown the zoom ring toast. + */ private static final String SETTING_NAME_SHOWN_TOAST = "shown_zoom_ring_toast"; private Context mContext; @@ -137,25 +154,37 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * screen once (for the first tap down) instead of twice (for the first tap * down and then to grab the thumb). */ + /** The X where the tap-drag started. */ private int mTapDragStartX; + /** The Y where the tap-drag started. */ private int mTapDragStartY; + /** The controller is idle */ private static final int TOUCH_MODE_IDLE = 0; - private static final int TOUCH_MODE_WAITING_FOR_SECOND_TAP = 1; + /** + * In the middle of a second-tap interaction, waiting for either an up-touch + * or the user to start dragging to go into tap-drag mode. + */ private static final int TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT = 2; + /** In the middle of a tap-drag. */ private static final int TOUCH_MODE_FORWARDING_FOR_TAP_DRAG = 3; private int mTouchMode; + /** Whether the zoom ring is visible. */ private boolean mIsZoomRingVisible; private ZoomRing mZoomRing; + /** Cached width of the zoom ring. */ private int mZoomRingWidth; + /** Cached height of the zoom ring. */ private int mZoomRingHeight; /** Invokes panning of owner view if the zoom ring is touching an edge. */ private Panner mPanner; + /** The time when the zoom ring first touched the edge. */ private long mTouchingEdgeStartTime; - private boolean mPanningEnabledForThisInteraction; + /** Whether the user has already initiated the panning. */ + private boolean mPanningInitiated; /** * When the finger moves the zoom ring to an edge, this is the horizontal @@ -167,16 +196,21 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** Vertical accumulator, see {@link #mMovingZoomRingOobX} */ private int mMovingZoomRingOobY; + /** Arrows that hint that the zoom ring is movable. */ private ImageView mPanningArrows; + /** The animation shown when the panning arrows are being shown. */ private Animation mPanningArrowsEnterAnimation; + /** The animation shown when the panning arrows are being hidden. */ private Animation mPanningArrowsExitAnimation; + /** + * Temporary rectangle, only use from the UI thread (and ideally don't rely + * on it being unused across many method calls.) + */ 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. @@ -217,6 +251,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + /** Listens for configuration changes so we can make sure we're still in a reasonable state. */ private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -228,7 +263,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, }; /** Keeps the scroller going (or starts it). */ - private static final int MSG_SCROLLER_TICK = 1; + private static final int MSG_SCROLLER_STEP = 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. */ @@ -244,8 +279,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_SCROLLER_TICK: - onScrollerTick(); + case MSG_SCROLLER_STEP: + onScrollerStep(); break; case MSG_POST_CONFIGURATION_CHANGED: @@ -296,8 +331,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, 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; + mContainerLayoutParams.windowAnimations = com.android.internal.R.style.Animation_ZoomRing; mContainer = new FrameLayout(context); mContainer.setLayoutParams(mContainerLayoutParams); @@ -308,13 +342,17 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mScroller = new Scroller(context, new DecelerateInterpolator()); - mViewConfig = ViewConfiguration.get(context); + ViewConfiguration vc = ViewConfiguration.get(context); + mScaledTouchSlop = vc.getScaledTouchSlop(); + + float density = context.getResources().getDisplayMetrics().density; + mPanGap = (int) (20 * density); + mInitiatePanGap = (int) (10 * density); } private void createPanningArrows() { - // TODO: style mPanningArrows = new ImageView(mContext); - mPanningArrows.setImageResource(com.android.internal.R.drawable.zoom_ring_arrows); + mPanningArrows.setImageDrawable(mZoomRing.getPanningArrowsDrawable()); mPanningArrows.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, @@ -328,15 +366,16 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } /** - * 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 + * Sets the angle (in radians) between ticks. This is also the angle a user + * must move the thumb 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 angle The angle for the callback threshold, in radians */ - public void setZoomCallbackThreshold(float callbackThreshold) { - mZoomRing.setCallbackThreshold((int) (callbackThreshold * ZoomRing.RADIAN_INT_MULTIPLIER)); + public void setTickDelta(float angle) { + mZoomRing.setTickDelta((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER)); } /** @@ -346,14 +385,23 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * @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) { + public void setTrackDrawable(int drawable) { mZoomRing.setBackgroundResource(drawable); } - + + /** + * Sets the callback for the zoom ring controller. + * + * @param callback The callback. + */ public void setCallback(OnZoomListener callback) { mCallback = callback; } + public void setVibration(boolean vibrate) { + mZoomRing.setVibration(vibrate); + } + public void setThumbAngle(float angle) { mZoomRing.setThumbAngle((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER)); } @@ -362,14 +410,6 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mZoomRing.setThumbAngleAnimated((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER), 0); } - public void setResetThumbAutomatically(boolean resetThumbAutomatically) { - mZoomRing.setResetThumbAutomatically(resetThumbAutomatically); - } - - public void setVibration(boolean vibrate) { - mZoomRing.setVibration(vibrate); - } - public void setThumbVisible(boolean thumbVisible) { mZoomRing.setThumbVisible(thumbVisible); } @@ -407,7 +447,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return; } - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + dismissZoomRingDelayed(INACTIVITY_TIMEOUT); } else { mPanner.stop(); } @@ -429,12 +469,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, 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); + refreshContainerLayout(); if (mCallback != null) { mCallback.onVisibilityChanged(true); @@ -479,19 +514,34 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } + private void refreshContainerLayout() { + if (mIsZoomRingVisible) { + mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); + } + } + /** - * TODO: docs - * + * Returns the container of the zoom ring widget. The client can add views + * here to be shown alongside the zoom ring. See {@link #getZoomRingId()}. + * <p> * 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 + * <ul> + * <li> The controller dispatches touch events differently than the regular view + * framework. + * <li> Please ensure you set your view to INVISIBLE not GONE when hiding it. + * </ul> + * + * @return The layout used by the container. */ public FrameLayout getContainer() { return mContainer; } + /** + * Returns the id of the zoom ring widget. + * + * @return The id of the zoom ring widget. + */ public int getZoomRingId() { return mZoomRing.getId(); } @@ -514,7 +564,10 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** * Should be called by the client for each event belonging to the second tap * (the down, move, up, and cancel events). - * + * <p> + * In most cases, the client can use a {@link GestureDetector} and forward events from + * {@link GestureDetector.OnDoubleTapListener#onDoubleTapEvent(MotionEvent)}. + * * @param event The event belonging to the second tap. * @return Whether the event was consumed. */ @@ -550,9 +603,9 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, case MotionEvent.ACTION_MOVE: int x = (int) event.getX(); int y = (int) event.getY(); - if (Math.abs(x - mTapDragStartX) > mViewConfig.getScaledTouchSlop() || + if (Math.abs(x - mTapDragStartX) > mScaledTouchSlop || Math.abs(y - mTapDragStartY) > - mViewConfig.getScaledTouchSlop()) { + mScaledTouchSlop) { mZoomRing.setTapDragMode(true, x, y); mTouchMode = TOUCH_MODE_FORWARDING_FOR_TAP_DRAG; setTouchTargetView(mZoomRing); @@ -600,8 +653,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, 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); + mCenteredContainerY - lp.y, RECENTERING_DURATION); + mHandler.sendEmptyMessage(MSG_SCROLLER_STEP); } } @@ -636,18 +689,22 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mZoomRing.handleTouch(event.getAction(), event.getEventTime(), x, y, rawX, rawY); } + /** @hide */ public void onZoomRingSetMovableHintVisible(boolean visible) { setPanningArrowsVisible(visible); } + /** @hide */ public void onUserInteractionStarted() { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); } + /** @hide */ public void onUserInteractionStopped() { - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + dismissZoomRingDelayed(INACTIVITY_TIMEOUT); } + /** @hide */ public void onZoomRingMovingStarted() { mScroller.abortAnimation(); mTouchingEdgeStartTime = 0; @@ -664,6 +721,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } + /** @hide */ public boolean onZoomRingMoved(int deltaX, int deltaY, int rawX, int rawY) { if (mMovingZoomRingOobX != 0) { @@ -721,12 +779,12 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, ownerBounds.bottom - mZoomRingHeight : newZoomRingY; lp.y = newZoomRingY - zoomRingTop; - mWindowManager.updateViewLayout(mContainer, lp); - + refreshContainerLayout(); + // Check for pan boolean horizontalPanning = true; int leftGap = newZoomRingX - ownerBounds.left; - if (leftGap < MAX_PAN_GAP) { + if (leftGap < mPanGap) { if (leftGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { // Future moves in this direction should be accumulated in mMovingZoomRingOobX mMovingZoomRingOobX = deltaX / Math.abs(deltaX); @@ -736,7 +794,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } } else { int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft); - if (rightGap < MAX_PAN_GAP) { + if (rightGap < mPanGap) { if (rightGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { mMovingZoomRingOobX = deltaX / Math.abs(deltaX); } @@ -750,7 +808,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } int topGap = newZoomRingY - ownerBounds.top; - if (topGap < MAX_PAN_GAP) { + if (topGap < mPanGap) { if (topGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { mMovingZoomRingOobY = deltaY / Math.abs(deltaY); } @@ -759,7 +817,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } } else { int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop); - if (bottomGap < MAX_PAN_GAP) { + if (bottomGap < mPanGap) { if (bottomGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { mMovingZoomRingOobY = deltaY / Math.abs(deltaY); } @@ -771,7 +829,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, if (!horizontalPanning) { // Neither are panning, reset any timer to start pan mode mTouchingEdgeStartTime = 0; - mPanningEnabledForThisInteraction = false; + mPanningInitiated = false; mPanner.stop(); } } @@ -781,13 +839,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } private boolean shouldPan(int gap) { - if (mPanningEnabledForThisInteraction) return true; + if (mPanningInitiated) return true; - if (gap < MAX_INITIATE_PAN_GAP) { + if (gap < mInitiatePanGap) { long time = SystemClock.elapsedRealtime(); if (mTouchingEdgeStartTime != 0 && mTouchingEdgeStartTime + INITIATE_PAN_DELAY < time) { - mPanningEnabledForThisInteraction = true; + mPanningInitiated = true; return true; } else if (mTouchingEdgeStartTime == 0) { mTouchingEdgeStartTime = time; @@ -800,6 +858,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return false; } + /** @hide */ public void onZoomRingMovingStopped() { mPanner.stop(); setPanningArrowsVisible(false); @@ -809,27 +868,25 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } private int getStrengthFromGap(int gap) { - return gap > MAX_PAN_GAP ? 0 : - (MAX_PAN_GAP - gap) * 100 / MAX_PAN_GAP; + return gap > mPanGap ? 0 : + (mPanGap - gap) * 100 / mPanGap; } + /** @hide */ public void onZoomRingThumbDraggingStarted() { if (mCallback != null) { mCallback.onBeginDrag(); } } + /** @hide */ 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, + getZoomRingCenterXInOwnerCoordinates(), + getZoomRingCenterYInOwnerCoordinates(), (float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER, (float) curAngle / ZoomRing.RADIAN_INT_MULTIPLIER); } @@ -837,24 +894,36 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return false; } + private int getZoomRingCenterXInOwnerCoordinates() { + int globalZoomCenterX = mContainerLayoutParams.x + mZoomRing.getLeft() + + mZoomRingWidth / 2; + return globalZoomCenterX - mOwnerViewBounds.left; + } + + private int getZoomRingCenterYInOwnerCoordinates() { + int globalZoomCenterY = mContainerLayoutParams.y + mZoomRing.getTop() + + mZoomRingHeight / 2; + return globalZoomCenterY - mOwnerViewBounds.top; + } + + /** @hide */ 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); - } + /** @hide */ + public void onZoomRingDismissed() { + mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + setVisible(false); } + /** @hide */ public void onRingDown(int tickAngle, int touchAngle) { } + /** @hide */ public boolean onTouch(View v, MotionEvent event) { if (sTutorialDialog != null && sTutorialDialog.isShowing() && SystemClock.elapsedRealtime() - sTutorialShowTime >= TUTORIAL_MIN_DISPLAY_TIME) { @@ -904,8 +973,9 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return retValue; } else { +// dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); if (action == MotionEvent.ACTION_DOWN) { - dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + dismissZoomRingDelayed(OUTSIDE_TAP_DISMISS_DELAY); } return false; @@ -932,7 +1002,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, int containerCenterY = mContainerLayoutParams.y + mContainer.getHeight() / 2; int distanceFromCenterX = rawX - containerCenterX; int distanceFromCenterY = rawY - containerCenterY; - int zoomRingRadius = mZoomRingWidth / 2 - ZOOM_RING_RADIUS_INSET; + int zoomRingRadius = mZoomRing.getTrackOuterRadius(); if (distanceFromCenterX * distanceFromCenterX + distanceFromCenterY * distanceFromCenterY <= zoomRingRadius * zoomRingRadius) { @@ -960,7 +1030,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return null; } - /** Steals key events from the owner view. */ + /** + * Steals key events from the owner view. + * + * @hide + */ public boolean onKey(View v, int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: @@ -971,12 +1045,14 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: // Keep the zoom alive a little longer - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + dismissZoomRingDelayed(INACTIVITY_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); + mCallback.onSimpleZoom(keyCode == KeyEvent.KEYCODE_DPAD_UP, + getZoomRingCenterXInOwnerCoordinates(), + getZoomRingCenterYInOwnerCoordinates()); } return true; @@ -985,18 +1061,18 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return false; } - private void onScrollerTick() { + private void onScrollerStep() { if (!mScroller.computeScrollOffset() || !mIsZoomRingVisible) return; mContainerLayoutParams.x = mScroller.getCurrX(); mContainerLayoutParams.y = mScroller.getCurrY(); - mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); + refreshContainerLayout(); - mHandler.sendEmptyMessage(MSG_SCROLLER_TICK); + mHandler.sendEmptyMessage(MSG_SCROLLER_STEP); } private void onPostConfigurationChanged() { - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + dismissZoomRingDelayed(INACTIVITY_TIMEOUT); refreshPositioningVariables(); ensureZoomRingIsCentered(); } @@ -1056,6 +1132,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, sTutorialShowTime = SystemClock.elapsedRealtime(); } + /** @hide Should only be used by Android platform apps */ public static void finishZoomTutorial(Context context, boolean userNotified) { if (sTutorialDialog == null) return; @@ -1078,22 +1155,45 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } } + /** @hide Should only be used by Android platform apps */ public void finishZoomTutorial() { finishZoomTutorial(mContext, true); } + /** + * Sets the initial velocity of a pan. + * + * @param startVelocity The initial velocity to move the owner view, in + * pixels per second. + */ public void setPannerStartVelocity(float startVelocity) { mPanner.mStartVelocity = startVelocity; } + /** + * Sets the accelartion of the pan. + * + * @param acceleration The acceleration, in pixels per second squared. + */ public void setPannerAcceleration(float acceleration) { mPanner.mAcceleration = acceleration; } + /** + * Sets the maximum velocity of a pan. + * + * @param maxVelocity The max velocity to move the owner view, in pixels per + * second. + */ public void setPannerMaxVelocity(float maxVelocity) { mPanner.mMaxVelocity = maxVelocity; } + /** + * Sets the duration before acceleration will be applied. + * + * @param duration The duration, in milliseconds. + */ public void setPannerStartAcceleratingDuration(int duration) { mPanner.mStartAcceleratingDuration = duration; } @@ -1201,16 +1301,83 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } + /** + * Interface used to inform the client of zoom events that the user + * triggers. + */ public interface OnZoomListener { + /** + * Called when the user begins dragging the thumb on the zoom ring. + */ void onBeginDrag(); + + /** + * Called when the user drags the thumb and passes a tick causing a + * zoom. + * + * @param deltaZoomLevel The number of levels to be zoomed. Positive to + * zoom in, negative to zoom out. + * @param centerX The point about which to zoom. The zoom should pin + * this point, leaving it at the same coordinate. This is + * relative to the owner view's upper-left. + * @param centerY The point about which to zoom. The zoom should pin + * this point, leaving it at the same coordinate. This is + * relative to the owner view's upper-left. + * @param startAngle The angle where the user started dragging the thumb. + * @param curAngle The current angle of the thumb. + * @return Whether the owner was zoomed. + */ boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, float curAngle); + + /** + * Called when the user releases the thumb. + */ void onEndDrag(); - void onSimpleZoom(boolean deltaZoomLevel); + + /** + * Called when the user zooms via some other mechanism, for example + * arrow keys or a trackball. + * + * @param zoomIn Whether to zoom in (true) or out (false). + * @param centerX See {@link #onDragZoom(int, int, int, float, float)}. + * @param centerY See {@link #onDragZoom(int, int, int, float, float)}. + */ + void onSimpleZoom(boolean zoomIn, int centerX, int centerY); + + /** + * Called when the user begins moving the zoom ring in order to pan the + * owner. + */ void onBeginPan(); + + /** + * Called when the owner should pan as a result of the user moving the zoom ring. + * + * @param deltaX The amount to pan horizontally. + * @param deltaY The amount to pan vertically. + * @return Whether the owner was panned. + */ boolean onPan(int deltaX, int deltaY); + + /** + * Called when the user releases the zoom ring. + */ void onEndPan(); + + /** + * Called when the client should center the owner on the given point. + * + * @param x The x to center on, relative to the owner view's upper-left. + * @param y The y to center on, relative to the owner view's upper-left. + */ void onCenter(int x, int y); + + /** + * Called when the zoom ring's visibility changes. + * + * @param visible Whether the zoom ring is visible (true) or not (false). + */ void onVisibilityChanged(boolean visible); } } |