diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-02 22:54:33 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-02 22:54:33 -0800 |
commit | 3dec7d563a2f3e1eb967ce2054a00b6620e3558c (patch) | |
tree | aa3b0365c47cb3c1607c0dc76c8d32b4046fc287 /core/java/android/widget | |
parent | 15ab3eae2ec3d73b3e8aa60b33ae41445bf83f4b (diff) | |
download | frameworks_base-3dec7d563a2f3e1eb967ce2054a00b6620e3558c.zip frameworks_base-3dec7d563a2f3e1eb967ce2054a00b6620e3558c.tar.gz frameworks_base-3dec7d563a2f3e1eb967ce2054a00b6620e3558c.tar.bz2 |
auto import from //depot/cupcake/@137055
Diffstat (limited to 'core/java/android/widget')
20 files changed, 1728 insertions, 444 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index f362e22..9da78d0 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -17,7 +17,6 @@ package android.widget; import android.content.Context; -import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -28,6 +27,7 @@ import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.view.Gravity; @@ -893,25 +893,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mSyncMode = SYNC_FIRST_POSITION; } - // Don't restore the type filter window when there is no keyboard - if (acceptFilter()) { - String filterText = ss.filter; - setFilterText(filterText); - } + setFilterText(ss.filter); requestLayout(); } private boolean acceptFilter() { final Context context = mContext; - final Configuration configuration = context.getResources().getConfiguration(); - final boolean keyboardShowing = configuration.keyboardHidden != - Configuration.KEYBOARDHIDDEN_YES; - final boolean hasKeyboard = configuration.keyboard != Configuration.KEYBOARD_NOKEYS; final InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - return (hasKeyboard && keyboardShowing) || - (!hasKeyboard && !inputManager.isFullscreenMode()); + return !inputManager.isFullscreenMode(); } /** @@ -922,7 +913,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ public void setFilterText(String filterText) { // TODO: Should we check for acceptFilter()? - if (mTextFilterEnabled && filterText != null && filterText.length() > 0) { + if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) { createTextFilter(false); // This is going to call our listener onTextChanged, but we might not // be ready to bring up a window yet @@ -942,6 +933,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + /** + * Returns the list's text filter, if available. + * @return the list's text filter or null if filtering isn't enabled + * @hide pending API Council approval + */ + public CharSequence getTextFilter() { + if (mTextFilterEnabled && mTextFilter != null) { + return mTextFilter.getText(); + } + return null; + } + @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index b046a6b..1d553f1 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -40,9 +40,15 @@ public abstract class AbsSeekBar extends ProgressBar { * Whether this is user seekable. */ boolean mIsUserSeekable = true; + + /** + * On key presses (right or left), the amount to increment/decrement the + * progress. + */ + private int mKeyProgressIncrement = 1; private static final int NO_ALPHA = 0xFF; - float mDisabledAlpha; + private float mDisabledAlpha; public AbsSeekBar(Context context) { super(context); @@ -101,6 +107,39 @@ public abstract class AbsSeekBar extends ProgressBar { invalidate(); } + /** + * Sets the amount of progress changed via the arrow keys. + * + * @param increment The amount to increment or decrement when the user + * presses the arrow keys. + */ + public void setKeyProgressIncrement(int increment) { + mKeyProgressIncrement = increment < 0 ? -increment : increment; + } + + /** + * Returns the amount of progress changed via the arrow keys. + * <p> + * By default, this will be a value that is derived from the max progress. + * + * @return The amount to increment or decrement when the user presses the + * arrow keys. This will be positive. + */ + public int getKeyProgressIncrement() { + return mKeyProgressIncrement; + } + + @Override + public synchronized void setMax(int max) { + super.setMax(max); + + if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) { + // It will take the user too long to change this via keys, change it + // to something more reasonable + setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20))); + } + } + @Override protected boolean verifyDrawable(Drawable who) { return who == mThumb || super.verifyDrawable(who); @@ -321,12 +360,12 @@ public abstract class AbsSeekBar extends ProgressBar { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (progress <= 0) break; - setProgress(progress - 1, true); + setProgress(progress - mKeyProgressIncrement, true); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (progress >= getMax()) break; - setProgress(progress + 1, true); + setProgress(progress + mKeyProgressIncrement, true); return true; } diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java index cf9c588..f847bc3 100644 --- a/core/java/android/widget/AnalogClock.java +++ b/core/java/android/widget/AnalogClock.java @@ -48,7 +48,6 @@ public class AnalogClock extends View { private int mDialHeight; private boolean mAttached; - private long mLastTime; private final Handler mHandler = new Handler(); private float mMinutes; @@ -96,7 +95,6 @@ public class AnalogClock extends View { protected void onAttachedToWindow() { super.onAttachedToWindow(); - onTimeChanged(); if (!mAttached) { mAttached = true; IntentFilter filter = new IntentFilter(); @@ -107,6 +105,15 @@ public class AnalogClock extends View { getContext().registerReceiver(mIntentReceiver, filter, null, mHandler); } + + // NOTE: It's safe to do these after registering the receiver since the receiver always runs + // in the main thread, therefore the receiver can't run before this method returns. + + // The time zone may have changed while the receiver wasn't registered, so update the Time + mCalendar = new Time(); + + // Make sure we update to the current time + onTimeChanged(); } @Override @@ -212,9 +219,7 @@ public class AnalogClock extends View { } private void onTimeChanged() { - long time = System.currentTimeMillis(); - mCalendar.set(time); - mLastTime = time; + mCalendar.setToNow(); int hour = mCalendar.hour; int minute = mCalendar.minute; @@ -231,8 +236,6 @@ public class AnalogClock extends View { if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) { String tz = intent.getStringExtra("time-zone"); mCalendar = new Time(TimeZone.getTimeZone(tz).getID()); - } else { - mCalendar = new Time(); } onTimeChanged(); diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index 7a51676..0c1c72a 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -78,6 +78,8 @@ import com.android.internal.R; * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth */ public class AutoCompleteTextView extends EditText implements Filter.FilterListener { static final boolean DEBUG = false; @@ -96,6 +98,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private DropDownListView mDropDownList; private int mDropDownVerticalOffset; private int mDropDownHorizontalOffset; + private int mDropDownAnchorId; + private View mDropDownAnchorView; // view is retrieved lazily from id once needed + private int mDropDownWidth; private Drawable mDropDownListHighlight; @@ -147,6 +152,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f); mDropDownHorizontalOffset = (int) a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f); + + // Get the anchor's id now, but the view won't be ready, so wait to actually get the + // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. + // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return + // this TextView, as a default anchoring point. + mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor, + View.NO_ID); + + // For dropdown width, the developer can specify a specific width, or FILL_PARENT + // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). + mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, + ViewGroup.LayoutParams.WRAP_CONTENT); mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint); @@ -187,6 +204,49 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void setCompletionHint(CharSequence hint) { mHintText = hint; } + + /** + * <p>Returns the current width for the auto-complete drop down list. This can + * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> + * + * @return the width for the drop down list + */ + public int getDropDownWidth() { + return mDropDownWidth; + } + + /** + * <p>Sets the current width for the auto-complete drop down list. This can + * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> + * + * @param width the width to use + */ + public void setDropDownWidth(int width) { + mDropDownWidth = width; + } + + /** + * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p> + * + * @return the view's id, or {@link View#NO_ID} if none specified + */ + public int getDropDownAnchor() { + return mDropDownAnchorId; + } + + /** + * <p>Sets the view to which the auto-complete drop down list should anchor. The view + * corresponding to this id will not be loaded until the next time it is needed to avoid + * loading a view which is not yet instantiated.</p> + * + * @param id the id to anchor the drop down list view to + */ + public void setDropDownAnchor(int id) { + mDropDownAnchorId = id; + mDropDownAnchorView = null; + } /** * <p>Returns the number of characters the user must type before the drop @@ -741,6 +801,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return result; } + + /** + * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of + * the id is NO_ID or we can't find a view for the given id, we return this TextView as + * the default anchoring point.</p> + */ + private View getDropDownAnchorView() { + if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) { + mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId); + } + return mDropDownAnchorView == null ? this : mDropDownAnchorView; + } /** * <p>Displays the drop down on screen.</p> @@ -748,16 +820,37 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void showDropDown() { int height = buildDropDown(); if (mPopup.isShowing()) { - mPopup.update(this, mDropDownHorizontalOffset, mDropDownVerticalOffset, - getWidth(), height); + int widthSpec; + if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + widthSpec = -1; + } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getDropDownAnchorView().getWidth(); + } else { + widthSpec = mDropDownWidth; + } + mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, + mDropDownVerticalOffset, widthSpec, height); } else { - mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT); - mPopup.setWidth(getWidth()); + int widthSpec; + if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { + mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } else { + mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT); + if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setWidth(getDropDownAnchorView().getWidth()); + } else { + mPopup.setWidth(mDropDownWidth); + } + } mPopup.setHeight(height); mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); mPopup.setOutsideTouchable(true); mPopup.setTouchInterceptor(new PopupTouchIntercepter()); - mPopup.showAsDropDown(this, mDropDownHorizontalOffset, mDropDownVerticalOffset); + mPopup.showAsDropDown(getDropDownAnchorView(), + mDropDownHorizontalOffset, mDropDownVerticalOffset); mDropDownList.setSelection(ListView.INVALID_POSITION); mDropDownList.hideSelector(); mDropDownList.requestFocus(); diff --git a/core/java/android/widget/BaseAdapter.java b/core/java/android/widget/BaseAdapter.java index 1921d73..532fd76 100644 --- a/core/java/android/widget/BaseAdapter.java +++ b/core/java/android/widget/BaseAdapter.java @@ -42,6 +42,10 @@ public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter { mDataSetObservable.unregisterObserver(observer); } + /** + * Notifies the attached View that the underlying data has been changed + * and it should refresh itself. + */ public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); } diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index 369221e..91add58 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -69,7 +69,10 @@ public class Chronometer extends TextView { private Object[] mFormatterArgs = new Object[1]; private StringBuilder mFormatBuilder; private OnChronometerTickListener mOnChronometerTickListener; - + private StringBuilder mRecycle = new StringBuilder(8); + + private static final int TICK_WHAT = 2; + /** * Initialize this Chronometer object. * Sets the base to the current time. @@ -115,6 +118,7 @@ public class Chronometer extends TextView { @android.view.RemotableViewMethod public void setBase(long base) { mBase = base; + dispatchChronometerTick(); updateText(SystemClock.elapsedRealtime()); } @@ -216,10 +220,10 @@ public class Chronometer extends TextView { updateRunning(); } - private void updateText(long now) { + private synchronized void updateText(long now) { long seconds = now - mBase; seconds /= 1000; - String text = DateUtils.formatElapsedTime(seconds); + String text = DateUtils.formatElapsedTime(mRecycle, seconds); if (mFormat != null) { Locale loc = Locale.getDefault(); @@ -247,7 +251,10 @@ public class Chronometer extends TextView { if (running != mRunning) { if (running) { updateText(SystemClock.elapsedRealtime()); - mHandler.sendMessageDelayed(Message.obtain(), 1000); + dispatchChronometerTick(); + mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000); + } else { + mHandler.removeMessages(TICK_WHAT); } mRunning = running; } @@ -255,10 +262,10 @@ public class Chronometer extends TextView { private Handler mHandler = new Handler() { public void handleMessage(Message m) { - if (mStarted) { + if (mRunning) { updateText(SystemClock.elapsedRealtime()); dispatchChronometerTick(); - sendMessageDelayed(Message.obtain(), 1000); + sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000); } } }; diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index 3d758e7..898e501 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -348,6 +348,21 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, mFilterQueryProvider = filterQueryProvider; } + /** + * Called when the {@link ContentObserver} on the cursor receives a change notification. + * The default implementation provides the auto-requery logic, but may be overridden by + * sub classes. + * + * @see ContentObserver#onChange(boolean) + * @hide pending API Council approval + */ + protected void onContentChanged() { + if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { + if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); + mDataValid = mCursor.requery(); + } + } + private class ChangeObserver extends ContentObserver { public ChangeObserver() { super(new Handler()); @@ -360,10 +375,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, @Override public void onChange(boolean selfChange) { - if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); - mDataValid = mCursor.requery(); - } + onContentChanged(); } } diff --git a/core/java/android/widget/Filter.java b/core/java/android/widget/Filter.java index a2316cf..1d0fd5e 100644 --- a/core/java/android/widget/Filter.java +++ b/core/java/android/widget/Filter.java @@ -45,8 +45,6 @@ public abstract class Filter { private Handler mThreadHandler; private Handler mResultHandler; - private String mConstraint; - private boolean mConstraintIsValid = false; /** * <p>Creates a new asynchronous filter.</p> @@ -84,13 +82,6 @@ public abstract class Filter { */ public final void filter(CharSequence constraint, FilterListener listener) { synchronized (this) { - String constraintAsString = constraint != null ? constraint.toString() : null; - if (mConstraintIsValid && ( - (constraintAsString == null && mConstraint == null) || - (constraintAsString != null && constraintAsString.equals(mConstraint)))) { - // nothing to do - return; - } if (mThreadHandler == null) { HandlerThread thread = new HandlerThread(THREAD_NAME); @@ -103,16 +94,13 @@ public abstract class Filter { RequestArguments args = new RequestArguments(); // make sure we use an immutable copy of the constraint, so that // it doesn't change while the filter operation is in progress - args.constraint = constraintAsString; + args.constraint = constraint != null ? constraint.toString() : null; args.listener = listener; message.obj = args; mThreadHandler.removeMessages(FILTER_TOKEN); mThreadHandler.removeMessages(FINISH_TOKEN); mThreadHandler.sendMessage(message); - - mConstraint = constraintAsString; - mConstraintIsValid = true; } } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 38bfc7c..6bbf062 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -924,32 +924,23 @@ public class GridView extends AbsListView { final int count = mItemCount; if (count > 0) { final View child = obtainView(0); - final int childViewType = mAdapter.getItemViewType(0); - AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); + if (p == null) { + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); - child.setLayoutParams(lp); - } - lp.viewType = childViewType; - - final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, - mListPadding.left + mListPadding.right, lp.width); - - int lpHeight = lp.height; - - int childHeightSpec; - if (lpHeight > 0) { - childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); - } else { - childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } + p.viewType = mAdapter.getItemViewType(0); + int childHeightSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); + int childWidthSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); child.measure(childWidthSpec, childHeightSpec); + childHeight = child.getMeasuredHeight(); - if (mRecycler.shouldRecycleViewType(childViewType)) { + if (mRecycler.shouldRecycleViewType(p.viewType)) { mRecycler.addScrapView(child); } } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 96fe595..652e30c 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -875,7 +875,7 @@ public class HorizontalScrollView extends FrameLayout { int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - final int childHeightMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 94d1bd1..a4523b9 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -840,7 +840,7 @@ public class ImageView extends View { @Override public int getBaseline() { - return mBaselineAligned ? getHeight() : -1; + return mBaselineAligned ? getMeasuredHeight() : -1; } /** diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 4e5989c..6df72d4 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -1011,34 +1011,13 @@ public class ListView extends AbsListView { if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)) { final View child = obtainView(0); - final int childViewType = mAdapter.getItemViewType(0); - AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, 0); - child.setLayoutParams(lp); - } - lp.viewType = childViewType; - - final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, - mListPadding.left + mListPadding.right, lp.width); - - int lpHeight = lp.height; - - int childHeightSpec; - if (lpHeight > 0) { - childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); - } else { - childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - } - - child.measure(childWidthSpec, childHeightSpec); + measureScrapChild(child, 0, widthMeasureSpec); childWidth = child.getMeasuredWidth(); childHeight = child.getMeasuredHeight(); - if (mRecycler.shouldRecycleViewType(childViewType)) { + if (recycleOnMeasure()) { mRecycler.addScrapView(child); } } @@ -1055,13 +1034,40 @@ public class ListView extends AbsListView { if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 - heightSize = measureHeightOfChildren( - MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), - 0, NO_POSITION, heightSize, -1); + heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } setMeasuredDimension(widthSize, heightSize); - mWidthMeasureSpec = widthMeasureSpec; + mWidthMeasureSpec = widthMeasureSpec; + } + + private void measureScrapChild(View child, int position, int widthMeasureSpec) { + LayoutParams p = (LayoutParams) child.getLayoutParams(); + if (p == null) { + p = new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + p.viewType = mAdapter.getItemViewType(position); + + int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, + mListPadding.left + mListPadding.right, p.width); + int lpHeight = p.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } + + /** + * @return True to recycle the views used to measure this ListView in + * UNSPECIFIED/AT_MOST modes, false otherwise. + * @hide + */ + protected boolean recycleOnMeasure() { + return true; } /** @@ -1090,8 +1096,8 @@ public class ListView extends AbsListView { * startPosition is 0). * @return The height of this ListView with the given children. */ - final int measureHeightOfChildren(final int widthMeasureSpec, final int startPosition, - int endPosition, final int maxHeight, int disallowPartialChildPosition) { + final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, + final int maxHeight, int disallowPartialChildPosition) { final ListAdapter adapter = mAdapter; if (adapter == null) { @@ -1110,29 +1116,20 @@ public class ListView extends AbsListView { // mItemCount - 1 since endPosition parameter is inclusive endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; final AbsListView.RecycleBin recycleBin = mRecycler; + final boolean recyle = recycleOnMeasure(); + for (i = startPosition; i <= endPosition; ++i) { child = obtainView(i); - final int childViewType = adapter.getItemViewType(i); - AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, 0); - child.setLayoutParams(lp); - } - lp.viewType = childViewType; + measureScrapChild(child, i, widthMeasureSpec); if (i > 0) { // Count the divider for all but one child returnedHeight += dividerHeight; } - child.measure(widthMeasureSpec, lp.height >= 0 - ? MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY) - : MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - // Recycle the view before we possibly return from the method - if (recycleBin.shouldRecycleViewType(childViewType)) { + if (recyle) { recycleBin.addScrapView(child); } @@ -1656,7 +1653,7 @@ public class ListView extends AbsListView { // Respect layout params that are already in the view. Otherwise make some up... // noinspection unchecked - AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); + AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); @@ -1675,7 +1672,7 @@ public class ListView extends AbsListView { if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { if (child instanceof Checkable) { - ((Checkable)child).setChecked(mCheckStates.get(position)); + ((Checkable) child).setChecked(mCheckStates.get(position)); } } diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 59a9310..05abc26 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -126,7 +126,7 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { Editable text = getText(); int end = getSelectionEnd(); - if (end < 0) { + if (end < 0 || mTokenizer == null) { return false; } @@ -147,7 +147,7 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { public void performValidation() { Validator v = getValidator(); - if (v == null) { + if (v == null || mTokenizer == null) { return; } diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 4a5cea1..53db77e 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -30,6 +30,7 @@ import android.view.View.OnTouchListener; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; import android.os.IBinder; import android.content.Context; import android.content.res.TypedArray; @@ -102,6 +103,8 @@ public class PopupWindow { private Rect mTempRect = new Rect(); private Drawable mBackground; + private Drawable mAboveAnchorBackgroundDrawable; + private Drawable mBelowAnchorBackgroundDrawable; private boolean mAboveAnchor; @@ -164,6 +167,43 @@ public class PopupWindow { mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); + // If this is a StateListDrawable, try to find and store the drawable to be + // used when the drop-down is placed above its anchor view, and the one to be + // used when the drop-down is placed below its anchor view. We extract + // the drawables ourselves to work around a problem with using refreshDrawableState + // that it will take into account the padding of all drawables specified in a + // StateListDrawable, thus adding superfluous padding to drop-down views. + // + // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and + // at least one other drawable, intended for the 'below-anchor state'. + if (mBackground instanceof StateListDrawable) { + StateListDrawable background = (StateListDrawable) mBackground; + + // Find the above-anchor view - this one's easy, it should be labeled as such. + int aboveAnchorStateIndex = background.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET); + + // Now, for the below-anchor view, look for any other drawable specified in the + // StateListDrawable which is not for the above-anchor state and use that. + int count = background.getStateCount(); + int belowAnchorStateIndex = -1; + for (int i = 0; i < count; i++) { + if (i != aboveAnchorStateIndex) { + belowAnchorStateIndex = i; + break; + } + } + + // Store the drawables we found, if we found them. Otherwise, set them both + // to null so that we'll just use refreshDrawableState. + if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) { + mAboveAnchorBackgroundDrawable = background.getStateDrawable(aboveAnchorStateIndex); + mBelowAnchorBackgroundDrawable = background.getStateDrawable(belowAnchorStateIndex); + } else { + mBelowAnchorBackgroundDrawable = null; + mAboveAnchorBackgroundDrawable = null; + } + } + a.recycle(); } @@ -661,7 +701,18 @@ public class PopupWindow { mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); if (mBackground != null) { - mPopupView.refreshDrawableState(); + // If the background drawable provided was a StateListDrawable with above-anchor + // and below-anchor states, use those. Otherwise rely on refreshDrawableState to + // do the job. + if (mAboveAnchorBackgroundDrawable != null) { + if (mAboveAnchor) { + mPopupView.setBackgroundDrawable(mAboveAnchorBackgroundDrawable); + } else { + mPopupView.setBackgroundDrawable(mBelowAnchorBackgroundDrawable); + } + } else { + mPopupView.refreshDrawableState(); + } } if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; @@ -697,12 +748,18 @@ public class PopupWindow { */ private void preparePopup(WindowManager.LayoutParams p) { if (mBackground != null) { + final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); + int height = ViewGroup.LayoutParams.FILL_PARENT; + if (layoutParams != null && + layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + height = ViewGroup.LayoutParams.WRAP_CONTENT; + } + // when a background is available, we embed the content view // within another view that owns the background drawable PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.FILL_PARENT + ViewGroup.LayoutParams.FILL_PARENT, height ); popupViewContainer.setBackgroundDrawable(mBackground); popupViewContainer.addView(mContentView, listParams); diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index dd2570a..f646ab5 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -526,6 +526,7 @@ public class ProgressBar extends View { * @see #getProgress() * @see #incrementProgressBy(int) */ + @android.view.RemotableViewMethod public synchronized void setProgress(int progress) { setProgress(progress, false); } diff --git a/core/java/android/widget/ResourceCursorAdapter.java b/core/java/android/widget/ResourceCursorAdapter.java index 9052ae3..a5dbd98 100644 --- a/core/java/android/widget/ResourceCursorAdapter.java +++ b/core/java/android/widget/ResourceCursorAdapter.java @@ -46,10 +46,30 @@ public abstract class ResourceCursorAdapter extends CursorAdapter { public ResourceCursorAdapter(Context context, int layout, Cursor c) { super(context, c); mLayout = mDropDownLayout = layout; - mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } /** + * Constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. + * @hide Pending API Council approval + */ + public ResourceCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** * Inflates view(s) from the specified XML file. * * @see android.widget.CursorAdapter#newView(android.content.Context, diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index c852be5..88b2a01 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -84,6 +84,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; +import android.view.ViewRoot; import android.view.ViewTreeObserver; import android.view.ViewGroup.LayoutParams; import android.view.animation.AnimationUtils; @@ -215,7 +216,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; int mDrawablePadding; - }; + } private Drawables mDrawables; private CharSequence mError; @@ -239,8 +240,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mMarqueeRepeatLimit = 3; class InputContentType { - String privateContentType; + int imeOptions = EditorInfo.IME_UNDEFINED; + String privateImeOptions; + CharSequence imeActionLabel; + int imeActionId; Bundle extras; + OnEditorActionListener onEditorActionListener; + boolean enterDown; } InputContentType mInputContentType; @@ -268,6 +274,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener p.measureText("H"); } + /** + * Interface definition for a callback to be invoked when an action is + * performed on the editor. + */ + public interface OnEditorActionListener { + /** + * Called when an action is being performed. + * + * @param v The view that was clicked. + * @param actionId Identifier of the action. This will be either the + * identifier you supplied, or {@link EditorInfo#IME_UNDEFINED + * EditorInfo.IME_UNDEFINED} if being called due to the enter key + * being pressed. + * @param event If triggered by an enter key, this is the event; + * otherwise, this is null. + * @return Return true if you have consumed the action, else false. + */ + boolean onEditorAction(TextView v, int actionId, KeyEvent event); + } + public TextView(Context context) { this(context, null); } @@ -376,7 +402,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int shadowcolor = 0; float dx = 0, dy = 0, r = 0; boolean password = false; - int contentType = EditorInfo.TYPE_NULL; + int inputType = EditorInfo.TYPE_NULL; int n = a.getIndexCount(); for (int i = 0; i < n; i++) { @@ -610,11 +636,34 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; case com.android.internal.R.styleable.TextView_inputType: - contentType = a.getInt(attr, mInputType); + inputType = a.getInt(attr, mInputType); break; - case com.android.internal.R.styleable.TextView_editorPrivateContentType: - setPrivateContentType(a.getString(attr)); + case com.android.internal.R.styleable.TextView_imeOptions: + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeOptions = a.getInt(attr, + mInputContentType.imeOptions); + break; + + case com.android.internal.R.styleable.TextView_imeActionLabel: + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeActionLabel = a.getText(attr); + break; + + case com.android.internal.R.styleable.TextView_imeActionId: + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeActionId = a.getInt(attr, + mInputContentType.imeActionId); + break; + + case com.android.internal.R.styleable.TextView_privateImeOptions: + setPrivateImeOptions(a.getString(attr)); break; case com.android.internal.R.styleable.TextView_editorExtras: @@ -632,7 +681,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener BufferType bufferType = BufferType.EDITABLE; - if ((contentType&(EditorInfo.TYPE_MASK_CLASS + if ((inputType&(EditorInfo.TYPE_MASK_CLASS |EditorInfo.TYPE_MASK_VARIATION)) == (EditorInfo.TYPE_CLASS_TEXT |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { @@ -656,57 +705,57 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener throw new RuntimeException(ex); } try { - mInputType = contentType != EditorInfo.TYPE_NULL - ? contentType + mInputType = inputType != EditorInfo.TYPE_NULL + ? inputType : mInput.getInputType(); } catch (IncompatibleClassChangeError e) { mInputType = EditorInfo.TYPE_CLASS_TEXT; } } else if (digits != null) { mInput = DigitsKeyListener.getInstance(digits.toString()); - mInputType = contentType; - } else if (contentType != EditorInfo.TYPE_NULL) { - setInputType(contentType, true); - singleLine = (contentType&(EditorInfo.TYPE_MASK_CLASS + mInputType = inputType; + } else if (inputType != EditorInfo.TYPE_NULL) { + setInputType(inputType, true); + singleLine = (inputType&(EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) != (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); } else if (phone) { mInput = DialerKeyListener.getInstance(); - contentType = EditorInfo.TYPE_CLASS_PHONE; + inputType = EditorInfo.TYPE_CLASS_PHONE; } else if (numeric != 0) { mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0, (numeric & DECIMAL) != 0); - contentType = EditorInfo.TYPE_CLASS_NUMBER; + inputType = EditorInfo.TYPE_CLASS_NUMBER; if ((numeric & SIGNED) != 0) { - contentType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED; + inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED; } if ((numeric & DECIMAL) != 0) { - contentType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; + inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; } - mInputType = contentType; + mInputType = inputType; } else if (autotext || autocap != -1) { TextKeyListener.Capitalize cap; - contentType = EditorInfo.TYPE_CLASS_TEXT; + inputType = EditorInfo.TYPE_CLASS_TEXT; if (!singleLine) { - contentType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; } switch (autocap) { case 1: cap = TextKeyListener.Capitalize.SENTENCES; - contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; + inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; break; case 2: cap = TextKeyListener.Capitalize.WORDS; - contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; + inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; break; case 3: cap = TextKeyListener.Capitalize.CHARACTERS; - contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS; + inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS; break; default: @@ -715,7 +764,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mInput = TextKeyListener.getInstance(autotext, cap); - mInputType = contentType; + mInputType = inputType; } else if (editable) { mInput = TextKeyListener.getInstance(); mInputType = EditorInfo.TYPE_CLASS_TEXT; @@ -1075,6 +1124,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_singleLine */ public final void setTransformationMethod(TransformationMethod method) { + if (method == mTransformation) { + // Avoid the setText() below if the transformation is + // the same. + return; + } if (mTransformation != null) { if (mText instanceof Spannable) { ((Spannable) mText).removeSpan(mTransformation); @@ -2778,7 +2832,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Directly change the content type integer of the text view, without * modifying any other state. - * @see #setContentType + * @see #setInputType(int) * @see android.text.InputType * @attr ref android.R.styleable#TextView_inputType */ @@ -2842,28 +2896,159 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Change the editor type integer associated with the text view, which + * will be reported to an IME with {@link EditorInfo#imeOptions} when it + * has focus. + * @see #getImeOptions + * @see android.view.inputmethod.EditorInfo + * @attr ref android.R.styleable#TextView_imeOptions + */ + public void setImeOptions(int imeOptions) { + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeOptions = imeOptions; + } + + /** + * Get the type of the IME editor. + * + * @see #setImeOptions(int) + * @see android.view.inputmethod.EditorInfo + */ + public int getImeOptions() { + return mInputContentType != null + ? mInputContentType.imeOptions : EditorInfo.IME_UNDEFINED; + } + + /** + * Change the custom IME action associated with the text view, which + * will be reported to an IME with {@link EditorInfo#actionLabel} + * and {@link EditorInfo#actionId} when it has focus. + * @see #getImeActionLabel + * @see #getImeActionId + * @see android.view.inputmethod.EditorInfo + * @attr ref android.R.styleable#TextView_imeActionLabel + * @attr ref android.R.styleable#TextView_imeActionId + */ + public void setImeActionLabel(CharSequence label, int actionId) { + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeActionLabel = label; + mInputContentType.imeActionId = actionId; + } + + /** + * Get the IME action label previous set with {@link #setImeActionLabel}. + * + * @see #setImeActionLabel + * @see android.view.inputmethod.EditorInfo + */ + public CharSequence getImeActionLabel() { + return mInputContentType != null + ? mInputContentType.imeActionLabel : null; + } + + /** + * Get the IME action ID previous set with {@link #setImeActionLabel}. + * + * @see #setImeActionLabel + * @see android.view.inputmethod.EditorInfo + */ + public int getImeActionId() { + return mInputContentType != null + ? mInputContentType.imeActionId : 0; + } + + /** + * Set a special OnClickListener to be called when an action is performed + * on the text view. This will be called when the enter key is pressed, + * or when an action supplied to the IME is selected by the user. + */ + public void setOnEditorActionListener(OnEditorActionListener l) { + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.onEditorActionListener = l; + } + + /** + * Called when an attached input method calls + * {@link InputConnection#performEditorAction(int) + * InputConnection.performEditorAction()} + * for this text view. The default implementation will call your click + * listener supplied to {@link #setOnEditorActionListener}, + * or generate an enter key down/up pair to invoke the action if not. + * + * @param actionCode The code of the action being performed. + * + * @see #setOnEditorActionListener + */ + public void onEditorAction(int actionCode) { + final InputContentType ict = mInputContentType; + if (ict != null) { + if (ict.onEditorActionListener != null) { + if (ict.onEditorActionListener.onEditorAction(this, + actionCode, null)) { + return; + } + } + } + + 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 + // 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!"); + } + } + return; + } + + Handler h = getHandler(); + long eventTime = SystemClock.uptimeMillis(); + h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); + h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); + } + + /** * Set the private content type of the text, which is the - * {@link EditorInfo#privateContentType TextBoxAttribute.privateContentType} + * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions} * field that will be filled in when creating an input connection. * - * @see #getPrivateContentType() - * @see EditorInfo#privateContentType - * @attr ref android.R.styleable#TextView_editorPrivateContentType + * @see #getPrivateImeOptions() + * @see EditorInfo#privateImeOptions + * @attr ref android.R.styleable#TextView_privateImeOptions */ - public void setPrivateContentType(String type) { + public void setPrivateImeOptions(String type) { if (mInputContentType == null) mInputContentType = new InputContentType(); - mInputContentType.privateContentType = type; + mInputContentType.privateImeOptions = type; } /** * Get the private type of the content. * - * @see #setPrivateContentType(String) - * @see EditorInfo#privateContentType + * @see #setPrivateImeOptions(String) + * @see EditorInfo#privateImeOptions */ - public String getPrivateContentType() { + public String getPrivateImeOptions() { return mInputContentType != null - ? mInputContentType.privateContentType : null; + ? mInputContentType.privateImeOptions : null; } /** @@ -3807,7 +3992,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. */ - private boolean advanceFocusOnEnter() { + protected boolean shouldAdvanceFocusOnEnter() { if (mInput == null) { return false; } @@ -3828,15 +4013,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } + private boolean isInterestingEnter(KeyEvent event) { + if ((event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 && + mInputContentType != null && + (mInputContentType.imeOptions & + EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { + // If this enter key came from a soft keyboard, and the + // text editor has been configured to not do a default + // action for software enter keys, then we aren't interested. + return false; + } + return true; + } + private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) { if (!isEnabled()) { return 0; } switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: - if (advanceFocusOnEnter()) { + if (!isInterestingEnter(event)) { + // Ignore enter key we aren't interested in. + return -1; + } + if (mInputContentType != null + && mInputContentType.onEditorActionListener != null) { + mInputContentType.enterDown = true; + } + // fall through... + case KeyEvent.KEYCODE_DPAD_CENTER: + if (shouldAdvanceFocusOnEnter()) { return 0; } } @@ -3939,7 +4146,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onKeyUp(keyCode, event); case KeyEvent.KEYCODE_ENTER: - if (advanceFocusOnEnter()) { + if (mInputContentType != null + && mInputContentType.onEditorActionListener != null + && mInputContentType.enterDown) { + mInputContentType.enterDown = false; + if (mInputContentType.onEditorActionListener.onEditorAction( + this, EditorInfo.IME_UNDEFINED, event)) { + return true; + } + } + + if (shouldAdvanceFocusOnEnter()) { /* * If there is a click listener, just call through to * super, which will invoke it. @@ -3994,11 +4211,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mInputMethodState = new InputMethodState(); } outAttrs.inputType = mInputType; - outAttrs.hintText = mHint; if (mInputContentType != null) { - outAttrs.privateContentType = mInputContentType.privateContentType; + outAttrs.imeOptions = mInputContentType.imeOptions; + outAttrs.privateImeOptions = mInputContentType.privateImeOptions; + outAttrs.actionLabel = mInputContentType.imeActionLabel; + outAttrs.actionId = mInputContentType.imeActionId; outAttrs.extras = mInputContentType.extras; + } else { + outAttrs.imeOptions = EditorInfo.IME_UNDEFINED; + } + if (outAttrs.imeOptions == EditorInfo.IME_UNDEFINED) { + if (focusSearch(FOCUS_DOWN) != null) { + // 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; + } + } } + outAttrs.hintText = mHint; if (mText instanceof Editable) { InputConnection ic = new EditableInputConnection(this); outAttrs.initialSelStart = Selection.getSelectionStart(mText); @@ -5787,6 +6019,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Don't leave us in the middle of a batch edit. onEndBatchEdit(); + if (mInputContentType != null) { + mInputContentType.enterDown = false; + } } startStopMarquee(hasWindowFocus); @@ -5880,8 +6115,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mScroller = s; } - private static class Blink extends Handler - implements Runnable { + private static class Blink extends Handler implements Runnable { private WeakReference<TextView> mView; private boolean mCancelled; @@ -6139,23 +6373,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } int start = end; - char c; int len = mText.length(); - while (start > 0 && (((c = mTransformed.charAt(start - 1)) == '\'') || - (Character.isLetterOrDigit(c)))) { - start--; + for (; start > 0; start--) { + char c = mTransformed.charAt(start - 1); + int type = Character.getType(c); + + if (c != '\'' && + type != Character.UPPERCASE_LETTER && + type != Character.LOWERCASE_LETTER && + type != Character.TITLECASE_LETTER && + type != Character.MODIFIER_LETTER && + type != Character.DECIMAL_DIGIT_NUMBER) { + break; + } } - while (end < len && (((c = mTransformed.charAt(end)) == '\'') || - (Character.isLetterOrDigit(c)))) { - end++; + for (; end < len; end++) { + char c = mTransformed.charAt(end); + int type = Character.getType(c); + + if (c != '\'' && + type != Character.UPPERCASE_LETTER && + type != Character.LOWERCASE_LETTER && + type != Character.TITLECASE_LETTER && + type != Character.MODIFIER_LETTER && + type != Character.DECIMAL_DIGIT_NUMBER) { + break; + } } if (start == end) { return null; } + if (end - start > 48) { + return null; + } + return TextUtils.substring(mTransformed, start, end); } diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java new file mode 100644 index 0000000..ec45e23 --- /dev/null +++ b/core/java/android/widget/ZoomButtonsController.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.view.View.OnClickListener; +import android.view.WindowManager.LayoutParams; + +// TODO: make sure no px values exist, only dip (scale if necessary from Viewconfiguration) + +/** + * TODO: Docs + * + * If you are using this with a custom View, please call + * {@link #setVisible(boolean) setVisible(false)} from the + * {@link View#onDetachedFromWindow}. + * + * @hide + */ +public class ZoomButtonsController implements View.OnTouchListener { + + private static final String TAG = "ZoomButtonsController"; + + private static final int ZOOM_CONTROLS_TIMEOUT = + (int) ViewConfiguration.getZoomControlsTimeout(); + + // TODO: scaled to density + private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20; + + private Context mContext; + private WindowManager mWindowManager; + + /** + * The view that is being zoomed by this zoom ring. + */ + private View mOwnerView; + + /** + * The bounds of the owner view in global coordinates. This is recalculated + * each time the zoom ring is shown. + */ + private Rect mOwnerViewBounds = new Rect(); + + /** + * The container that is added as a window. + */ + private FrameLayout mContainer; + private LayoutParams mContainerLayoutParams; + private int[] mContainerLocation = new int[2]; + + private ZoomControls mControls; + + /** + * The view (or null) that should receive touch events. This will get set if + * the touch down hits the container. It will be reset on the touch up. + */ + private View mTouchTargetView; + /** + * The {@link #mTouchTargetView}'s location in window, set on touch down. + */ + private int[] mTouchTargetLocationInWindow = new int[2]; + /** + * If the zoom ring is dismissed but the user is still in a touch + * interaction, we set this to true. This will ignore all touch events until + * up/cancel, and then set the owner's touch listener to null. + */ + private boolean mReleaseTouchListenerOnUp; + + private boolean mIsVisible; + + private Rect mTempRect = new Rect(); + + private OnZoomListener mCallback; + + /** + * When showing the zoom, we add the view as a new window. However, there is + * logic that needs to know the size of the zoom which is determined after + * it's laid out. Therefore, we must post this logic onto the UI thread so + * it will be exceuted AFTER the layout. This is the logic. + */ + private Runnable mPostedVisibleInitializer; + + private IntentFilter mConfigurationChangedFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!mIsVisible) return; + + mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); + mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); + } + }; + + /** When configuration changes, this is called after the UI thread is idle. */ + private static final int MSG_POST_CONFIGURATION_CHANGED = 2; + /** Used to delay the zoom ring dismissal. */ + private static final int MSG_DISMISS_ZOOM_RING = 3; + /** + * If setVisible(true) is called and the owner view's window token is null, + * we delay the setVisible(true) call until it is not null. + */ + private static final int MSG_POST_SET_VISIBLE = 4; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_POST_CONFIGURATION_CHANGED: + onPostConfigurationChanged(); + break; + + case MSG_DISMISS_ZOOM_RING: + setVisible(false); + break; + + case MSG_POST_SET_VISIBLE: + if (mOwnerView.getWindowToken() == null) { + // Doh, it is still null, throw an exception + throw new IllegalArgumentException( + "Cannot make the zoom ring visible if the owner view is " + + "not attached to a window."); + } + setVisible(true); + break; + } + + } + }; + + public ZoomButtonsController(Context context, View ownerView) { + mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mOwnerView = ownerView; + + mContainer = createContainer(); + } + + private FrameLayout createContainer() { + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.BOTTOM | Gravity.CENTER; + lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE | + LayoutParams.FLAG_LAYOUT_NO_LIMITS; + lp.height = LayoutParams.WRAP_CONTENT; + lp.width = LayoutParams.FILL_PARENT; + lp.type = LayoutParams.TYPE_APPLICATION_PANEL; + lp.format = PixelFormat.TRANSPARENT; + // TODO: make a new animation for this + lp.windowAnimations = com.android.internal.R.style.Animation_InputMethodFancy; + mContainerLayoutParams = lp; + + FrameLayout container = new FrameLayout(mContext); + container.setLayoutParams(lp); + container.setMeasureAllChildren(true); + + LayoutInflater inflater = (LayoutInflater) mContext + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(com.android.internal.R.layout.zoom_magnify, container); + + mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls); + mControls.setOnZoomInClickListener(new OnClickListener() { + public void onClick(View v) { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + if (mCallback != null) mCallback.onZoom(true); + } + }); + mControls.setOnZoomOutClickListener(new OnClickListener() { + public void onClick(View v) { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + if (mCallback != null) mCallback.onZoom(false); + } + }); + + View overview = container.findViewById(com.android.internal.R.id.zoomMagnify); + overview.setVisibility(View.GONE); + overview.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + if (mCallback != null) mCallback.onOverview(); + } + }); + + return container; + } + + public void setCallback(OnZoomListener callback) { + mCallback = callback; + } + + public void setFocusable(boolean focusable) { + if (focusable) { + mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE; + } + + if (mIsVisible) { + mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); + } + } + + public void setOverviewVisible(boolean visible) { + mContainer.findViewById(com.android.internal.R.id.zoomMagnify) + .setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public boolean isVisible() { + return mIsVisible; + } + + public void setVisible(boolean visible) { + + if (!useThisZoom(mContext)) return; + + if (visible) { + if (mOwnerView.getWindowToken() == null) { + /* + * We need a window token to show ourselves, maybe the owner's + * window hasn't been created yet but it will have been by the + * time the looper is idle, so post the setVisible(true) call. + */ + if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) { + mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE); + } + return; + } + + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + } + + if (mIsVisible == visible) { + return; + } + mIsVisible = visible; + + if (visible) { + if (mContainerLayoutParams.token == null) { + mContainerLayoutParams.token = mOwnerView.getWindowToken(); + } + + mWindowManager.addView(mContainer, mContainerLayoutParams); + + if (mPostedVisibleInitializer == null) { + mPostedVisibleInitializer = new Runnable() { + public void run() { + refreshPositioningVariables(); + + if (mCallback != null) { + mCallback.onVisibilityChanged(true); + } + } + }; + } + + mHandler.post(mPostedVisibleInitializer); + + // Handle configuration changes when visible + mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); + + // Steal touches events from the owner + mOwnerView.setOnTouchListener(this); + mReleaseTouchListenerOnUp = false; + + } else { + // Don't want to steal any more touches + if (mTouchTargetView != null) { + // We are still stealing the touch events for this touch + // sequence, so release the touch listener later + mReleaseTouchListenerOnUp = true; + } else { + mOwnerView.setOnTouchListener(null); + } + + // No longer care about configuration changes + mContext.unregisterReceiver(mConfigurationChangedReceiver); + + mWindowManager.removeView(mContainer); + mHandler.removeCallbacks(mPostedVisibleInitializer); + + if (mCallback != null) { + mCallback.onVisibilityChanged(false); + } + } + + } + + /** + * TODO: docs + * + * Notes: + * - Please ensure you set your View to INVISIBLE not GONE when hiding it. + * + * @return TODO + */ + public FrameLayout getContainer() { + return mContainer; + } + + public int getZoomRingId() { + return mControls.getId(); + } + + private void dismissControlsDelayed(int delay) { + mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_RING, delay); + } + + /** + * Should be called by the client for each event belonging to the second tap + * (the down, move, up, and cancel events). + * + * @param event The event belonging to the second tap. + * @return Whether the event was consumed. + */ + public boolean handleDoubleTapEvent(MotionEvent event) { + if (!useThisZoom(mContext)) return false; + + int action = event.getAction(); + + if (action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + setVisible(true); + centerPoint(x, y); + } + + return true; + } + + private void refreshPositioningVariables() { + // Calculate the owner view's bounds + mOwnerView.getGlobalVisibleRect(mOwnerViewBounds); + mContainer.getLocationOnScreen(mContainerLocation); + } + + /** + * Centers the point (in owner view's coordinates). + */ + private void centerPoint(int x, int y) { + if (mCallback != null) { + mCallback.onCenter(x, y); + } + } + + public boolean onTouch(View v, MotionEvent event) { + int action = event.getAction(); + + if (mReleaseTouchListenerOnUp) { + // The ring was dismissed but we need to throw away all events until the up + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mOwnerView.setOnTouchListener(null); + setTouchTargetView(null); + mReleaseTouchListenerOnUp = false; + } + + // Eat this event + return true; + } + + // TODO: optimize this (it ends up removing message and queuing another) + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + + View targetView = mTouchTargetView; + + switch (action) { + case MotionEvent.ACTION_DOWN: + targetView = getViewForTouch((int) event.getRawX(), (int) event.getRawY()); + setTouchTargetView(targetView); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + setTouchTargetView(null); + break; + } + + if (targetView != null) { + // The upperleft corner of the target view in raw coordinates + int targetViewRawX = mContainerLocation[0] + mTouchTargetLocationInWindow[0]; + int targetViewRawY = mContainerLocation[1] + mTouchTargetLocationInWindow[1]; + + MotionEvent containerEvent = MotionEvent.obtain(event); + // Convert the motion event into the target view's coordinates (from + // owner view's coordinates) + containerEvent.offsetLocation(mOwnerViewBounds.left - targetViewRawX, + mOwnerViewBounds.top - targetViewRawY); + boolean retValue = targetView.dispatchTouchEvent(containerEvent); + containerEvent.recycle(); + return retValue; + + } else { + return false; + } + } + + private void setTouchTargetView(View view) { + mTouchTargetView = view; + if (view != null) { + view.getLocationInWindow(mTouchTargetLocationInWindow); + } + } + + /** + * Returns the View that should receive a touch at the given coordinates. + * + * @param rawX The raw X. + * @param rawY The raw Y. + * @return The view that should receive the touches, or null if there is not one. + */ + private View getViewForTouch(int rawX, int rawY) { + // Reverse order so the child drawn on top gets first dibs. + int containerCoordsX = rawX - mContainerLocation[0]; + int containerCoordsY = rawY - mContainerLocation[1]; + Rect frame = mTempRect; + for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { + View child = mContainer.getChildAt(i); + if (child.getVisibility() != View.VISIBLE) { + continue; + } + + child.getHitRect(frame); + // Expand the touch region + frame.top -= ZOOM_CONTROLS_TOUCH_PADDING; + if (frame.contains(containerCoordsX, containerCoordsY)) { + return child; + } + } + + return null; + } + + private void onPostConfigurationChanged() { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + refreshPositioningVariables(); + } + + public static boolean useThisZoom(Context context) { + return ZoomRingController.getZoomType(context) == 2; + } + + public interface OnZoomListener { + void onCenter(int x, int y); + void onVisibilityChanged(boolean visible); + void onZoom(boolean zoomIn); + void onOverview(); + } +} diff --git a/core/java/android/widget/ZoomRing.java b/core/java/android/widget/ZoomRing.java index a29e1a0..a5a867b 100644 --- a/core/java/android/widget/ZoomRing.java +++ b/core/java/android/widget/ZoomRing.java @@ -77,7 +77,7 @@ public class ZoomRing extends View { private int mPreviousWidgetDragX; private int mPreviousWidgetDragY; - private boolean mDrawThumb = true; + private boolean mThumbVisible = true; private Drawable mThumbDrawable; /** Shown beneath the thumb if we can still zoom in. */ @@ -91,6 +91,13 @@ public class ZoomRing extends View { 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; @@ -99,7 +106,7 @@ public class ZoomRing extends View { * are waiting for him to move the slop amount before considering him in the * drag thumb state. */ - private static final int MODE_WAITING_FOR_DRAG_THUMB = 5; + private static final int MODE_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 @@ -109,11 +116,14 @@ public class ZoomRing extends View { private static final int MODE_WAITING_FOR_MOVE_ZOOM_RING = 4; private static final int MODE_MOVE_ZOOM_RING = 2; private static final int MODE_TAP_DRAG = 3; - /** Ignore the touch interaction. Reset to MODE_IDLE after up/cancel. */ - private static final int MODE_IGNORE_UNTIL_UP = 6; + /** Ignore the touch interaction until the user touches the thumb again. */ + private static final int MODE_IGNORE_UNTIL_TOUCHES_THUMB = 6; private int mMode; - - private long mPreviousUpTime; + + /** Records the last mode the user was in. */ + private int mPreviousMode; + + private long mPreviousCenterUpTime; private int mPreviousDownX; private int mPreviousDownY; @@ -122,7 +132,9 @@ public class ZoomRing extends View { 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; @@ -133,6 +145,8 @@ public class ZoomRing extends View { 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() { @@ -163,6 +177,8 @@ public class ZoomRing extends View { 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(); } @@ -175,7 +191,7 @@ public class ZoomRing extends View { mThumbHalfHeight = mThumbDrawable.getIntrinsicHeight() / 2; mThumbHalfWidth = mThumbDrawable.getIntrinsicWidth() / 2; - mCallbackThreshold = PI_INT_MULTIPLIED / 6; + setCallbackThreshold(PI_INT_MULTIPLIED / 6); } public ZoomRing(Context context, AttributeSet attrs) { @@ -193,8 +209,20 @@ public class ZoomRing extends View { // 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; @@ -306,15 +334,7 @@ public class ZoomRing extends View { public void setThumbAngleAnimated(int angle, int duration) { // The angle when going from the current angle to the new angle int deltaAngle = getDelta(mThumbAngle, angle); - // Counter clockwise if the new angle is more the current angle - boolean counterClockwise = deltaAngle > 0; - - if (deltaAngle > PI_INT_MULTIPLIED || deltaAngle < -PI_INT_MULTIPLIED) { - // It's quicker to go the other direction - counterClockwise = !counterClockwise; - } - - setThumbAngleAnimated(angle, duration, counterClockwise); + setThumbAngleAnimated(angle, duration, deltaAngle > 0); } public void setThumbAngleAnimated(int angle, int duration, boolean counterClockwise) { @@ -354,14 +374,10 @@ public class ZoomRing extends View { return mThumbScroller.getCurrX() % TWO_PI_INT_MULTIPLIED; } - public void resetThumbAngle(int angle) { - mPreviousCallbackAngle = angle; - setThumbAngleInt(angle); - } - public void resetThumbAngle() { if (mResetThumbAutomatically) { - resetThumbAngle(0); + mPreviousCallbackAngle = 0; + setThumbAngleInt(0); } } @@ -394,101 +410,119 @@ public class ZoomRing extends View { 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 resetState() { - mMode = MODE_IDLE; + private void resetToIdle() { + setMode(MODE_IDLE); mPreviousWidgetDragX = mPreviousWidgetDragY = Integer.MIN_VALUE; mAcculumalatedTrailAngle = 0.0; } public void setTapDragMode(boolean tapDragMode, int x, int y) { - resetState(); - mMode = tapDragMode ? MODE_TAP_DRAG : MODE_IDLE; - + 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) { - switch (action) { + // 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 (time - mPreviousUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT) { + 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); + onTouchUp(time, isTouchingRing); // Dismissing, so halt here return true; } + resetToIdle(); mCallback.onUserInteractionStarted(); mPreviousDownX = x; mPreviousDownY = y; - resetState(); // Fall through to code below switch (since the down is used for // jumping to the touched tick) break; case MotionEvent.ACTION_MOVE: - if (mMode == MODE_IGNORE_UNTIL_UP) return true; - // Fall through to code below switch break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: - onTouchUp(time); + onTouchUp(time, isTouchingRing); return true; default: return false; } - // local{X,Y} will be where the center of the widget is (0,0) - int localX = x - mCenterX; - int localY = y - mCenterY; - boolean isTouchingThumb = true; - boolean isInRingBounds = true; - - int touchAngle = getAngle(localX, localY); - int radiusSquared = localX * localX + localY * localY; - if (radiusSquared < mBoundInnerRadiusSquared || - radiusSquared > mBoundOuterRadiusSquared) { - // Out-of-bounds - isTouchingThumb = false; - isInRingBounds = false; - } - - int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle); - int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ? - deltaThumbAndTouch : -deltaThumbAndTouch; - if (isTouchingThumb && - absoluteDeltaThumbAndTouch > THUMB_GRAB_SLOP) { - // Didn't grab close enough to the thumb - isTouchingThumb = false; - } - if (mMode == MODE_IDLE) { if (isTouchingThumb) { // They grabbed the thumb - mMode = MODE_DRAG_THUMB; + setMode(MODE_DRAG_THUMB); onThumbDragStarted(touchAngle); - } else if (isInRingBounds) { + } else if (isTouchingRing) { // They tapped somewhere else on the ring int tickAngle = getClosestTickAngle(touchAngle); - int deltaThumbAndTick = getDelta(mThumbAngle, tickAngle); int boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); @@ -497,12 +531,12 @@ public class ZoomRing extends View { if (deltaThumbAndTick > MAX_ABS_JUMP_DELTA_ANGLE || deltaThumbAndTick < -MAX_ABS_JUMP_DELTA_ANGLE) { // Trying to jump too far, ignore this touch interaction - mMode = MODE_IGNORE_UNTIL_UP; + setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); return true; } - // Make sure we only let them jump within bounds if (boundAngle != Integer.MIN_VALUE) { + // Cap the user's jump to the bound tickAngle = boundAngle; } } else { @@ -515,47 +549,59 @@ public class ZoomRing extends View { deltaThumbAndTick = getDelta(mThumbAngle, tickAngle, !oldDirectionIsCcw); boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); if (boundAngle != Integer.MIN_VALUE) { - // Not allowed to be here, it is between two bounds - mMode = MODE_IGNORE_UNTIL_UP; + // Cannot get to the tapped location because it is out-of-bounds + setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); return true; } } } - mMode = MODE_WAITING_FOR_DRAG_THUMB; + 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 + /* + * 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 - mMode = MODE_WAITING_FOR_MOVE_ZOOM_RING; + // 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) { + } 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)) { - mMode = MODE_DRAG_THUMB; + 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. */ - mMode = MODE_MOVE_ZOOM_RING; + 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 (isInRingBounds) { + if (isTouchingRing) { onThumbDragged(touchAngle, false, false); } } else if (mMode == MODE_MOVE_ZOOM_RING) { @@ -565,24 +611,39 @@ public class ZoomRing extends View { return true; } - private void onTouchUp(long time) { - if (mMode == MODE_MOVE_ZOOM_RING || mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { + 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 (mMode == MODE_MOVE_ZOOM_RING) { + if (mode == MODE_MOVE_ZOOM_RING) { mCallback.onZoomRingMovingStopped(); } - } else if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG || - mMode == MODE_WAITING_FOR_DRAG_THUMB) { + } else if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG || + mode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) { onThumbDragStopped(); - if (mMode == MODE_DRAG_THUMB) { + if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG) { // Animate back to a tick setThumbAngleAnimated(mPreviousCallbackAngle, 0); } } - - mPreviousUpTime = time; 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) { @@ -681,9 +742,8 @@ public class ZoomRing extends View { int totalDeltaAngle; totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); - int fuzzyCallbackThreshold = (int) (mCallbackThreshold * 0.65f); - if (totalDeltaAngle >= fuzzyCallbackThreshold - || totalDeltaAngle <= -fuzzyCallbackThreshold) { + if (totalDeltaAngle >= mFuzzyCallbackThreshold + || totalDeltaAngle <= -mFuzzyCallbackThreshold) { if (!useDirection) { // Set ccw to match the direction found by getDelta @@ -737,7 +797,7 @@ public class ZoomRing extends View { // We bounded the touch angle totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); animateThumbToNewAngle = true; - mMode = MODE_IGNORE_UNTIL_UP; + setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); } @@ -764,11 +824,13 @@ public class ZoomRing extends View { boolean canStillZoom = mCallback.onZoomRingThumbDragged( deltaLevels, mThumbDragStartAngle, touchAngle); - // TODO: we're trying the haptics to see how it goes with - // users, so we're ignoring the settings (for now) - performHapticFeedback(HapticFeedbackConstants.ZOOM_RING_TICK, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + 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( @@ -791,6 +853,134 @@ public class ZoomRing extends View { 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) { @@ -818,16 +1008,16 @@ public class ZoomRing extends View { mCallback.onZoomRingThumbDraggingStopped(); } - private void onZoomRingMoved(int x, int y) { + private void onZoomRingMoved(int rawX, int rawY) { if (mPreviousWidgetDragX != Integer.MIN_VALUE) { - int deltaX = x - mPreviousWidgetDragX; - int deltaY = y - mPreviousWidgetDragY; + int deltaX = rawX - mPreviousWidgetDragX; + int deltaY = rawY - mPreviousWidgetDragY; - mCallback.onZoomRingMoved(deltaX, deltaY); + mCallback.onZoomRingMoved(deltaX, deltaY, rawX, rawY); } - mPreviousWidgetDragX = x; - mPreviousWidgetDragY = y; + mPreviousWidgetDragX = rawX; + mPreviousWidgetDragY = rawY; } @Override @@ -859,15 +1049,17 @@ public class ZoomRing extends View { protected void onDraw(Canvas canvas) { super.onDraw(canvas); - if (mDrawThumb) { + 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); } @@ -877,6 +1069,28 @@ public class ZoomRing extends View { 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) { @@ -886,6 +1100,7 @@ public class ZoomRing extends View { 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; @@ -893,6 +1108,7 @@ public class ZoomRing extends View { 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; @@ -917,10 +1133,14 @@ public class ZoomRing extends View { 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); } @@ -941,7 +1161,7 @@ public class ZoomRing extends View { void onZoomRingSetMovableHintVisible(boolean visible); void onZoomRingMovingStarted(); - boolean onZoomRingMoved(int deltaX, int deltaY); + boolean onZoomRingMoved(int deltaX, int deltaY, int rawX, int rawY); void onZoomRingMovingStopped(); void onZoomRingThumbDraggingStarted(); diff --git a/core/java/android/widget/ZoomRingController.java b/core/java/android/widget/ZoomRingController.java index 2e97fda..19f66a0 100644 --- a/core/java/android/widget/ZoomRingController.java +++ b/core/java/android/widget/ZoomRingController.java @@ -33,6 +33,7 @@ import android.provider.Settings; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; @@ -47,30 +48,36 @@ import android.view.animation.DecelerateInterpolator; /** * TODO: Docs - * + * * If you are using this with a custom View, please call * {@link #setVisible(boolean) setVisible(false)} from the * {@link View#onDetachedFromWindow}. - * + * * @hide */ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, View.OnTouchListener, View.OnKeyListener { - + private static final int ZOOM_RING_RADIUS_INSET = 24; private static final int ZOOM_RING_RECENTERING_DURATION = 500; private static final String TAG = "ZoomRing"; - public static final boolean USE_OLD_ZOOM = false; + public static final boolean USE_OLD_ZOOM = false; + static int getZoomType(Context context) { + return Settings.System.getInt(context.getContentResolver(), "zoom", 1); + } public static boolean useOldZoom(Context context) { - return Settings.System.getInt(context.getContentResolver(), "zoom", 1) == 0; + return getZoomType(context) == 0; } - + private static boolean useThisZoom(Context context) { + return getZoomType(context) == 1; + } + private static final int ZOOM_CONTROLS_TIMEOUT = (int) ViewConfiguration.getZoomControlsTimeout(); - + // TODO: move these to ViewConfiguration or re-use existing ones // TODO: scale px values based on latest from ViewConfiguration private static final int SECOND_TAP_TIMEOUT = 500; @@ -80,12 +87,12 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private static final int MAX_INITIATE_PAN_GAP = 10; // TODO view config private static final int INITIATE_PAN_DELAY = 300; - + private static final String SETTING_NAME_SHOWN_TOAST = "shown_zoom_ring_toast"; - + private Context mContext; private WindowManager mWindowManager; - + /** * The view that is being zoomed by this zoom ring. */ @@ -111,15 +118,15 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** * The {@link #mTouchTargetView}'s location in window, set on touch down. */ - private int[] mTouchTargetLocationInWindow = new int[2]; + private int[] mTouchTargetLocationInWindow = new int[2]; /** * If the zoom ring is dismissed but the user is still in a touch * interaction, we set this to true. This will ignore all touch events until * up/cancel, and then set the owner's touch listener to null. */ private boolean mReleaseTouchListenerOnUp; - - + + /* * Tap-drag is an interaction where the user first taps and then (quickly) * does the clockwise or counter-clockwise drag. In reality, this is: (down, @@ -132,30 +139,40 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, */ private int mTapDragStartX; private int mTapDragStartY; - + private static final int TOUCH_MODE_IDLE = 0; private static final int TOUCH_MODE_WAITING_FOR_SECOND_TAP = 1; private static final int TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT = 2; private static final int TOUCH_MODE_FORWARDING_FOR_TAP_DRAG = 3; private int mTouchMode; - + private boolean mIsZoomRingVisible; - + private ZoomRing mZoomRing; private int mZoomRingWidth; private int mZoomRingHeight; - + /** Invokes panning of owner view if the zoom ring is touching an edge. */ private Panner mPanner; private long mTouchingEdgeStartTime; private boolean mPanningEnabledForThisInteraction; - + + /** + * When the finger moves the zoom ring to an edge, this is the horizontal + * accumulator for how much the finger has moved off of its original touch + * point on the zoom ring (OOB = out-of-bounds). If < 0, the finger has + * moved that many px to the left of its original touch point on the ring. + */ + private int mMovingZoomRingOobX; + /** Vertical accumulator, see {@link #mMovingZoomRingOobX} */ + private int mMovingZoomRingOobY; + private ImageView mPanningArrows; private Animation mPanningArrowsEnterAnimation; private Animation mPanningArrowsExitAnimation; - + private Rect mTempRect = new Rect(); - + private OnZoomListener mCallback; private ViewConfiguration mViewConfig; @@ -171,7 +188,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * for the container's layout params. */ private int mCenteredContainerY = Integer.MIN_VALUE; - + /** * Scroller used to re-center the zoom ring if the user had dragged it to a * corner and then double-taps any point on the owner view (the owner view @@ -181,7 +198,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * The (x,y) of the scroller is the (x,y) of the container's layout params. */ private Scroller mScroller; - + /** * When showing the zoom ring, we add the view as a new window. However, * there is logic that needs to know the size of the zoom ring which is @@ -189,7 +206,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * the UI thread so it will be exceuted AFTER the layout. This is the logic. */ private Runnable mPostedVisibleInitializer; - + /** * Only touch from the main thread. */ @@ -199,23 +216,29 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - + private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!mIsZoomRingVisible) return; - + mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); } }; - + /** Keeps the scroller going (or starts it). */ private static final int MSG_SCROLLER_TICK = 1; /** When configuration changes, this is called after the UI thread is idle. */ private static final int MSG_POST_CONFIGURATION_CHANGED = 2; /** Used to delay the zoom ring dismissal. */ private static final int MSG_DISMISS_ZOOM_RING = 3; + + /** + * If setVisible(true) is called and the owner view's window token is null, + * we delay the setVisible(true) call until it is not null. + */ + private static final int MSG_POST_SET_VISIBLE = 4; private Handler mHandler = new Handler() { @Override @@ -224,26 +247,36 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, case MSG_SCROLLER_TICK: onScrollerTick(); break; - + case MSG_POST_CONFIGURATION_CHANGED: onPostConfigurationChanged(); break; - + case MSG_DISMISS_ZOOM_RING: setVisible(false); break; + + case MSG_POST_SET_VISIBLE: + if (mOwnerView.getWindowToken() == null) { + // Doh, it is still null, throw an exception + throw new IllegalArgumentException( + "Cannot make the zoom ring visible if the owner view is " + + "not attached to a window."); + } + setVisible(true); + break; } - - } + + } }; - + public ZoomRingController(Context context, View ownerView) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mPanner = new Panner(); mOwnerView = ownerView; - + mZoomRing = new ZoomRing(context); mZoomRing.setId(com.android.internal.R.id.zoomControls); mZoomRing.setLayoutParams(new FrameLayout.LayoutParams( @@ -251,7 +284,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); mZoomRing.setCallback(this); - + createPanningArrows(); mContainerLayoutParams = new LayoutParams(); @@ -269,12 +302,12 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mContainer = new FrameLayout(context); mContainer.setLayoutParams(mContainerLayoutParams); mContainer.setMeasureAllChildren(true); - + mContainer.addView(mZoomRing); mContainer.addView(mPanningArrows); - + mScroller = new Scroller(context, new DecelerateInterpolator()); - + mViewConfig = ViewConfiguration.get(context); } @@ -287,7 +320,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); mPanningArrows.setVisibility(View.INVISIBLE); - + mPanningArrowsEnterAnimation = AnimationUtils.loadAnimation(mContext, com.android.internal.R.anim.fade_in); mPanningArrowsExitAnimation = AnimationUtils.loadAnimation(mContext, @@ -299,7 +332,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * get a callback. Once there is a callback, the accumulator resets. For * example, if you set this to PI/6, it will give a callback every time the * user moves PI/6 amount on the ring. - * + * * @param callbackThreshold The angle for the callback threshold, in radians */ public void setZoomCallbackThreshold(float callbackThreshold) { @@ -308,7 +341,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** * Sets a drawable for the zoom ring track. - * + * * @param drawable The drawable to use for the track. * @hide Need a better way of doing this, but this one-off for browser so it * can have its final look for the usability study @@ -316,11 +349,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setZoomRingTrack(int drawable) { mZoomRing.setBackgroundResource(drawable); } - + public void setCallback(OnZoomListener callback) { mCallback = callback; } - + public void setThumbAngle(float angle) { mZoomRing.setThumbAngle((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER)); } @@ -328,13 +361,21 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setThumbAngleAnimated(float angle) { 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); + } public void setThumbClockwiseBound(float angle) { - mZoomRing.setThumbClockwiseBound(angle >= 0 ? + mZoomRing.setThumbClockwiseBound(angle >= 0 ? (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : Integer.MIN_VALUE); } @@ -351,14 +392,26 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setVisible(boolean visible) { - if (useOldZoom(mContext)) return; + if (!useThisZoom(mContext)) return; if (visible) { + if (mOwnerView.getWindowToken() == null) { + /* + * We need a window token to show ourselves, maybe the owner's + * window hasn't been created yet but it will have been by the + * time the looper is idle, so post the setVisible(true) call. + */ + if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) { + mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE); + } + return; + } + dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); } else { mPanner.stop(); } - + if (mIsZoomRingVisible == visible) { return; } @@ -368,40 +421,40 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, if (mContainerLayoutParams.token == null) { mContainerLayoutParams.token = mOwnerView.getWindowToken(); } - + mWindowManager.addView(mContainer, mContainerLayoutParams); - + if (mPostedVisibleInitializer == null) { mPostedVisibleInitializer = new Runnable() { public void run() { refreshPositioningVariables(); resetZoomRing(); - + // TODO: remove this 'update' and just center zoom ring before the // 'add', but need to make sure we have the width and height (which // probably can only be retrieved after it's measured, which happens // after it's added). mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); - + if (mCallback != null) { mCallback.onVisibilityChanged(true); } } - }; + }; } - + mPanningArrows.setAnimation(null); - + mHandler.post(mPostedVisibleInitializer); - + // Handle configuration changes when visible mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); - + // Steal key/touches events from the owner mOwnerView.setOnKeyListener(this); mOwnerView.setOnTouchListener(this); mReleaseTouchListenerOnUp = false; - + } else { // Don't want to steal any more keys/touches mOwnerView.setOnKeyListener(null); @@ -415,45 +468,45 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // No longer care about configuration changes mContext.unregisterReceiver(mConfigurationChangedReceiver); - + mWindowManager.removeView(mContainer); mHandler.removeCallbacks(mPostedVisibleInitializer); - + if (mCallback != null) { mCallback.onVisibilityChanged(false); } } - + } - + /** * TODO: docs - * + * * Notes: * - Touch dispatching is different. Only direct children who are clickable are eligble for touch events. * - Please ensure you set your View to INVISIBLE not GONE when hiding it. - * + * * @return */ public FrameLayout getContainer() { return mContainer; } - + public int getZoomRingId() { return mZoomRing.getId(); } - + private void dismissZoomRingDelayed(int delay) { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_RING, delay); } - + private void resetZoomRing() { mScroller.abortAnimation(); - + mContainerLayoutParams.x = mCenteredContainerX; mContainerLayoutParams.y = mCenteredContainerY; - + // Reset the thumb mZoomRing.resetThumbAngle(); } @@ -461,13 +514,15 @@ 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). - * + * * @param event The event belonging to the second tap. * @return Whether the event was consumed. */ public boolean handleDoubleTapEvent(MotionEvent event) { - int action = event.getAction(); + if (!useThisZoom(mContext)) return false; + int action = event.getAction(); + // TODO: make sure this works well with the // ownerView.setOnTouchListener(this) instead of window receiving // touches @@ -475,19 +530,19 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mTouchMode = TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT; int x = (int) event.getX(); int y = (int) event.getY(); - + refreshPositioningVariables(); setVisible(true); centerPoint(x, y); - ensureZoomRingIsCentered(); - + ensureZoomRingIsCentered(); + // Tap drag mode stuff mTapDragStartX = x; mTapDragStartY = y; } else if (action == MotionEvent.ACTION_CANCEL) { mTouchMode = TOUCH_MODE_IDLE; - + } else { // action is move or up switch (mTouchMode) { case TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT: { @@ -503,29 +558,29 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, setTouchTargetView(mZoomRing); } return true; - + case MotionEvent.ACTION_UP: mTouchMode = TOUCH_MODE_IDLE; break; } break; } - + case TOUCH_MODE_FORWARDING_FOR_TAP_DRAG: { switch (action) { case MotionEvent.ACTION_MOVE: giveTouchToZoomRing(event); return true; - + case MotionEvent.ACTION_UP: mTouchMode = TOUCH_MODE_IDLE; - + /* * This is a power-user feature that only shows the * zoom while the user is performing the tap-drag. * That means once it is released, the zoom ring * should disappear. - */ + */ mZoomRing.setTapDragMode(false, (int) event.getX(), (int) event.getY()); dismissZoomRingDelayed(0); break; @@ -534,13 +589,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } } } - + return true; } - + private void ensureZoomRingIsCentered() { LayoutParams lp = mContainerLayoutParams; - + if (lp.x != mCenteredContainerX || lp.y != mCenteredContainerY) { int width = mContainer.getWidth(); int height = mContainer.getHeight(); @@ -549,21 +604,21 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mHandler.sendEmptyMessage(MSG_SCROLLER_TICK); } } - + private void refreshPositioningVariables() { mZoomRingWidth = mZoomRing.getWidth(); mZoomRingHeight = mZoomRing.getHeight(); - + // Calculate the owner view's bounds mOwnerView.getGlobalVisibleRect(mOwnerViewBounds); - + // Get the center Gravity.apply(Gravity.CENTER, mContainer.getWidth(), mContainer.getHeight(), mOwnerViewBounds, mTempRect); mCenteredContainerX = mTempRect.left; mCenteredContainerY = mTempRect.top; } - + /** * Centers the point (in owner view's coordinates). */ @@ -572,7 +627,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mCallback.onCenter(x, y); } } - + private void giveTouchToZoomRing(MotionEvent event) { int rawX = (int) event.getRawX(); int rawY = (int) event.getRawY(); @@ -580,11 +635,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, int y = rawY - mContainerLayoutParams.y - mZoomRing.getTop(); mZoomRing.handleTouch(event.getAction(), event.getEventTime(), x, y, rawX, rawY); } - + public void onZoomRingSetMovableHintVisible(boolean visible) { - setPanningArrowsVisible(visible); + setPanningArrowsVisible(visible); } - + public void onUserInteractionStarted() { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); } @@ -596,24 +651,62 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void onZoomRingMovingStarted() { mScroller.abortAnimation(); mTouchingEdgeStartTime = 0; + mMovingZoomRingOobX = 0; + mMovingZoomRingOobY = 0; if (mCallback != null) { mCallback.onBeginPan(); } } - + private void setPanningArrowsVisible(boolean visible) { mPanningArrows.startAnimation(visible ? mPanningArrowsEnterAnimation : mPanningArrowsExitAnimation); mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } - - public boolean onZoomRingMoved(int deltaX, int deltaY) { + + public boolean onZoomRingMoved(int deltaX, int deltaY, int rawX, int rawY) { + + if (mMovingZoomRingOobX != 0) { + /* + * The finger has moved off the point where it originally touched + * the zidget. + */ + boolean wasOobLeft = mMovingZoomRingOobX < 0; + mMovingZoomRingOobX += deltaX; + if ((wasOobLeft && mMovingZoomRingOobX > 0) || + (!wasOobLeft && mMovingZoomRingOobX < 0)) { + /* + * Woot, the finger is back on the original point. Infact, it + * went PAST its original point, so take the amount it passed + * and use that as the delta to move the zoom ring. + */ + deltaX = mMovingZoomRingOobX; + // No longer out-of-bounds, reset + mMovingZoomRingOobX = 0; + } else { + // The finger is still not back, eat this movement + deltaX = 0; + } + } + + if (mMovingZoomRingOobY != 0) { + // See above for comments + boolean wasOobUp = mMovingZoomRingOobY < 0; + mMovingZoomRingOobY += deltaY; + if ((wasOobUp && mMovingZoomRingOobY > 0) || (!wasOobUp && mMovingZoomRingOobY < 0)) { + deltaY = mMovingZoomRingOobY; + mMovingZoomRingOobY = 0; + } else { + deltaY = 0; + } + } + WindowManager.LayoutParams lp = mContainerLayoutParams; Rect ownerBounds = mOwnerViewBounds; - + int zoomRingLeft = mZoomRing.getLeft(); int zoomRingTop = mZoomRing.getTop(); - + int newX = lp.x + deltaX; int newZoomRingX = newX + zoomRingLeft; newZoomRingX = (newZoomRingX <= ownerBounds.left) ? ownerBounds.left : @@ -627,19 +720,26 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, (newZoomRingY + mZoomRingHeight > ownerBounds.bottom) ? ownerBounds.bottom - mZoomRingHeight : newZoomRingY; lp.y = newZoomRingY - zoomRingTop; - + mWindowManager.updateViewLayout(mContainer, lp); - + // Check for pan boolean horizontalPanning = true; int leftGap = newZoomRingX - ownerBounds.left; if (leftGap < MAX_PAN_GAP) { + if (leftGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { + // Future moves in this direction should be accumulated in mMovingZoomRingOobX + mMovingZoomRingOobX = deltaX / Math.abs(deltaX); + } if (shouldPan(leftGap)) { mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); } } else { int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft); if (rightGap < MAX_PAN_GAP) { + if (rightGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { + mMovingZoomRingOobX = deltaX / Math.abs(deltaX); + } if (shouldPan(rightGap)) { mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); } @@ -648,15 +748,21 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, horizontalPanning = false; } } - + int topGap = newZoomRingY - ownerBounds.top; if (topGap < MAX_PAN_GAP) { + if (topGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { + mMovingZoomRingOobY = deltaY / Math.abs(deltaY); + } if (shouldPan(topGap)) { mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); } } else { int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop); if (bottomGap < MAX_PAN_GAP) { + if (bottomGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { + mMovingZoomRingOobY = deltaY / Math.abs(deltaY); + } if (shouldPan(bottomGap)) { mPanner.setVerticalStrength(getStrengthFromGap(bottomGap)); } @@ -670,13 +776,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } } } - + return true; } - + private boolean shouldPan(int gap) { if (mPanningEnabledForThisInteraction) return true; - + if (gap < MAX_INITIATE_PAN_GAP) { long time = SystemClock.elapsedRealtime(); if (mTouchingEdgeStartTime != 0 && @@ -693,7 +799,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } return false; } - + public void onZoomRingMovingStopped() { mPanner.stop(); setPanningArrowsVisible(false); @@ -701,18 +807,18 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mCallback.onEndPan(); } } - + private int getStrengthFromGap(int gap) { return gap > MAX_PAN_GAP ? 0 : (MAX_PAN_GAP - gap) * 100 / MAX_PAN_GAP; } - + public void onZoomRingThumbDraggingStarted() { if (mCallback != null) { mCallback.onBeginDrag(); } } - + public boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle) { if (mCallback != null) { int deltaZoomLevel = -numLevels; @@ -720,17 +826,17 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mZoomRingWidth / 2; int globalZoomCenterY = mContainerLayoutParams.y + mZoomRing.getTop() + mZoomRingHeight / 2; - + return mCallback.onDragZoom(deltaZoomLevel, globalZoomCenterX - mOwnerViewBounds.left, globalZoomCenterY - mOwnerViewBounds.top, (float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER, (float) curAngle / ZoomRing.RADIAN_INT_MULTIPLIER); } - + return false; } - + public void onZoomRingThumbDraggingStopped() { if (mCallback != null) { mCallback.onEndDrag(); @@ -740,35 +846,35 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void onZoomRingDismissed(boolean dismissImmediately) { if (dismissImmediately) { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); - setVisible(false); + setVisible(false); } else { dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); } } - + public void onRingDown(int tickAngle, int touchAngle) { } - + public boolean onTouch(View v, MotionEvent event) { if (sTutorialDialog != null && sTutorialDialog.isShowing() && SystemClock.elapsedRealtime() - sTutorialShowTime >= TUTORIAL_MIN_DISPLAY_TIME) { finishZoomTutorial(); } - + int action = event.getAction(); if (mReleaseTouchListenerOnUp) { - // The ring was dismissed but we need to throw away all events until the up + // The ring was dismissed but we need to throw away all events until the up if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { mOwnerView.setOnTouchListener(null); setTouchTargetView(null); mReleaseTouchListenerOnUp = false; } - + // Eat this event return true; } - + View targetView = mTouchTargetView; switch (action) { @@ -776,7 +882,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, targetView = getViewForTouch((int) event.getRawX(), (int) event.getRawY()); setTouchTargetView(targetView); break; - + case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: setTouchTargetView(null); @@ -787,7 +893,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // The upperleft corner of the target view in raw coordinates int targetViewRawX = mContainerLayoutParams.x + mTouchTargetLocationInWindow[0]; int targetViewRawY = mContainerLayoutParams.y + mTouchTargetLocationInWindow[1]; - + MotionEvent containerEvent = MotionEvent.obtain(event); // Convert the motion event into the target view's coordinates (from // owner view's coordinates) @@ -796,32 +902,32 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, boolean retValue = targetView.dispatchTouchEvent(containerEvent); containerEvent.recycle(); return retValue; - + } else { if (action == MotionEvent.ACTION_DOWN) { dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); } - + return false; } } - + private void setTouchTargetView(View view) { mTouchTargetView = view; if (view != null) { view.getLocationInWindow(mTouchTargetLocationInWindow); } } - + /** * Returns the View that should receive a touch at the given coordinates. - * + * * @param rawX The raw X. * @param rawY The raw Y. * @return The view that should receive the touches, or null if there is not one. */ private View getViewForTouch(int rawX, int rawY) { - // Check to see if it is touching the ring + // Check to see if it is touching the ring int containerCenterX = mContainerLayoutParams.x + mContainer.getWidth() / 2; int containerCenterY = mContainerLayoutParams.y + mContainer.getHeight() / 2; int distanceFromCenterX = rawX - containerCenterX; @@ -832,7 +938,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, zoomRingRadius * zoomRingRadius) { return mZoomRing; } - + // Check to see if it is touching any other clickable View. // Reverse order so the child drawn on top gets first dibs. int containerCoordsX = rawX - mContainerLayoutParams.x; @@ -844,13 +950,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, !child.isClickable()) { continue; } - + child.getHitRect(frame); if (frame.contains(containerCoordsX, containerCoordsY)) { return child; } } - + return null; } @@ -861,34 +967,34 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, case KeyEvent.KEYCODE_DPAD_RIGHT: // Eat these return true; - + case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: // Keep the zoom alive a little longer dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); // They started zooming, hide the thumb arrows mZoomRing.setThumbArrowsVisible(false); - + if (mCallback != null && event.getAction() == KeyEvent.ACTION_DOWN) { mCallback.onSimpleZoom(keyCode == KeyEvent.KEYCODE_DPAD_UP); } - + return true; } - + return false; } private void onScrollerTick() { if (!mScroller.computeScrollOffset() || !mIsZoomRingVisible) return; - + mContainerLayoutParams.x = mScroller.getCurrX(); mContainerLayoutParams.y = mScroller.getCurrY(); mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); mHandler.sendEmptyMessage(MSG_SCROLLER_TICK); } - + private void onPostConfigurationChanged() { dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); refreshPositioningVariables(); @@ -908,7 +1014,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * before. Furthermore, if the application does not have privilege to write * to the system settings, it will store this bit locally in a shared * preference. - * + * * @hide This should only be used by our main apps--browser, maps, and * gallery */ @@ -917,53 +1023,65 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, if (Settings.System.getInt(cr, SETTING_NAME_SHOWN_TOAST, 0) == 1) { return; } - + SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); if (sp.getInt(SETTING_NAME_SHOWN_TOAST, 0) == 1) { return; } - + if (sTutorialDialog != null && sTutorialDialog.isShowing()) { sTutorialDialog.dismiss(); } + + LayoutInflater layoutInflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + TextView textView = (TextView) layoutInflater.inflate( + com.android.internal.R.layout.alert_dialog_simple_text, null) + .findViewById(android.R.id.text1); + textView.setText(com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short); sTutorialDialog = new AlertDialog.Builder(context) - .setMessage( - com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short) + .setView(textView) .setIcon(0) .create(); - + Window window = sTutorialDialog.getWindow(); window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND | + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND | WindowManager.LayoutParams.FLAG_BLUR_BEHIND); window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); - + sTutorialDialog.show(); sTutorialShowTime = SystemClock.elapsedRealtime(); } - - public void finishZoomTutorial() { + + public static void finishZoomTutorial(Context context, boolean userNotified) { if (sTutorialDialog == null) return; - + sTutorialDialog.dismiss(); sTutorialDialog = null; - + // Record that they have seen the tutorial - try { - Settings.System.putInt(mContext.getContentResolver(), SETTING_NAME_SHOWN_TOAST, 1); - } catch (SecurityException e) { - /* - * The app does not have permission to clear this global flag, make - * sure the user does not see the message when he comes back to this - * same app at least. - */ - SharedPreferences sp = mContext.getSharedPreferences("_zoom", Context.MODE_PRIVATE); - sp.edit().putInt(SETTING_NAME_SHOWN_TOAST, 1).commit(); + if (userNotified) { + try { + Settings.System.putInt(context.getContentResolver(), SETTING_NAME_SHOWN_TOAST, 1); + } catch (SecurityException e) { + /* + * The app does not have permission to clear this global flag, make + * sure the user does not see the message when he comes back to this + * same app at least. + */ + SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); + sp.edit().putInt(SETTING_NAME_SHOWN_TOAST, 1).commit(); + } } } - + + public void finishZoomTutorial() { + finishZoomTutorial(mContext, true); + } + public void setPannerStartVelocity(float startVelocity) { mPanner.mStartVelocity = startVelocity; } @@ -983,27 +1101,27 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private class Panner implements Runnable { private static final int RUN_DELAY = 15; private static final float STOP_SLOWDOWN = 0.8f; - + private final Handler mUiHandler = new Handler(); - + private int mVerticalStrength; private int mHorizontalStrength; private boolean mStopping; - + /** The time this current pan started. */ private long mStartTime; - + /** The time of the last callback to pan the map/browser/etc. */ private long mPreviousCallbackTime; - + // TODO Adjust to be DPI safe private float mStartVelocity = 135; private float mAcceleration = 160; private float mMaxVelocity = 1000; private int mStartAcceleratingDuration = 700; private float mVelocity; - + /** -100 (full left) to 0 (none) to 100 (full right) */ public void setHorizontalStrength(int horizontalStrength) { if (mHorizontalStrength == 0 && mVerticalStrength == 0 && horizontalStrength != 0) { @@ -1011,7 +1129,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } else if (mVerticalStrength == 0 && horizontalStrength == 0) { stop(); } - + mHorizontalStrength = horizontalStrength; mStopping = false; } @@ -1023,11 +1141,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } else if (mHorizontalStrength == 0 && verticalStrength == 0) { stop(); } - - mVerticalStrength = verticalStrength; + + mVerticalStrength = verticalStrength; mStopping = false; } - + private void start() { mUiHandler.post(this); mPreviousCallbackTime = 0; @@ -1037,37 +1155,37 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void stop() { mStopping = true; } - + public void run() { if (mStopping) { mHorizontalStrength *= STOP_SLOWDOWN; mVerticalStrength *= STOP_SLOWDOWN; } - + if (mHorizontalStrength == 0 && mVerticalStrength == 0) { return; } - + boolean firstRun = mPreviousCallbackTime == 0; long curTime = SystemClock.elapsedRealtime(); int panAmount = getPanAmount(mPreviousCallbackTime, curTime); mPreviousCallbackTime = curTime; - + if (firstRun) { mStartTime = curTime; mVelocity = mStartVelocity; } else { int panX = panAmount * mHorizontalStrength / 100; int panY = panAmount * mVerticalStrength / 100; - + if (mCallback != null) { mCallback.onPan(panX, panY); } } - + mUiHandler.postDelayed(this, RUN_DELAY); } - + private int getPanAmount(long previousTime, long currentTime) { if (mVelocity > mMaxVelocity) { mVelocity = mMaxVelocity; @@ -1077,14 +1195,12 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mVelocity += (currentTime - previousTime) * mAcceleration / 1000; } } - + return (int) ((currentTime - previousTime) * mVelocity) / 1000; } } - - public interface OnZoomListener { void onBeginDrag(); boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, |