diff options
Diffstat (limited to 'core/java/android/widget')
-rw-r--r-- | core/java/android/widget/AbsListView.java | 73 | ||||
-rw-r--r-- | core/java/android/widget/Chronometer.java | 48 | ||||
-rw-r--r-- | core/java/android/widget/FrameLayout.java | 2 | ||||
-rw-r--r-- | core/java/android/widget/ImageView.java | 7 | ||||
-rw-r--r-- | core/java/android/widget/LinearLayout.java | 6 | ||||
-rw-r--r-- | core/java/android/widget/ListView.java | 4 | ||||
-rw-r--r-- | core/java/android/widget/PopupWindow.java | 91 | ||||
-rw-r--r-- | core/java/android/widget/ProgressBar.java | 4 | ||||
-rw-r--r-- | core/java/android/widget/RelativeLayout.java | 4 | ||||
-rw-r--r-- | core/java/android/widget/RemoteViews.java | 664 | ||||
-rw-r--r-- | core/java/android/widget/TabHost.java | 6 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 65 | ||||
-rw-r--r-- | core/java/android/widget/ViewFlipper.java | 2 | ||||
-rw-r--r-- | core/java/android/widget/ZoomRing.java | 687 | ||||
-rw-r--r-- | core/java/android/widget/ZoomRingController.java | 471 |
15 files changed, 1437 insertions, 697 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index c012e25..f362e22 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -41,7 +41,7 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.view.WindowManagerImpl; +import android.view.inputmethod.InputMethodManager; import android.view.ContextMenu.ContextMenuInfo; import com.android.internal.R; @@ -425,6 +425,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private int mTouchSlop; + private float mDensityScale; + /** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. @@ -567,7 +569,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ @Override protected boolean isVerticalScrollBarHidden() { - return mFastScroller != null ? mFastScroller.isVisible() : false; + return mFastScroller != null && mFastScroller.isVisible(); } /** @@ -709,6 +711,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te setScrollingCacheEnabled(true); mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + mDensityScale = getContext().getResources().getDisplayMetrics().density; } private void useDefaultSelector() { @@ -891,14 +894,26 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } // Don't restore the type filter window when there is no keyboard - int keyboardHidden = getContext().getResources().getConfiguration().keyboardHidden; - if (keyboardHidden != Configuration.KEYBOARDHIDDEN_YES) { + if (acceptFilter()) { String filterText = ss.filter; setFilterText(filterText); } + 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()); + } + /** * Sets the initial value for the text filter. * @param filterText The text to use for the filter. @@ -906,6 +921,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setTextFilterEnabled */ public void setFilterText(String filterText) { + // TODO: Should we check for acceptFilter()? if (mTextFilterEnabled && filterText != null && filterText.length() > 0) { createTextFilter(false); // This is going to call our listener onTextChanged, but we might not @@ -1076,6 +1092,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mInLayout = false; } + /** + * @hide + */ + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + final boolean changed = super.setFrame(left, top, right, bottom); + + // Reposition the popup when the frame has changed. This includes + // translating the widget, not just changing its dimension. The + // filter popup needs to follow the widget. + if (mFiltered && changed && getWindowVisibility() == View.VISIBLE && mPopup != null && + mPopup.isShowing()) { + positionPopup(true); + } + + return changed; + } + protected void layoutChildren() { } @@ -2587,10 +2621,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te clearScrollingCache(); mSpecificTop = selectedTop; selectedPos = lookForSelectablePosition(selectedPos, down); - if (selectedPos >= 0) { + if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { mLayoutMode = LAYOUT_SPECIFIC; setSelectionInt(selectedPos); invokeOnItemScrollListener(); + } else { + selectedPos = INVALID_POSITION; } reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); @@ -2727,19 +2763,27 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private void showPopup() { // Make sure we have a window before showing the popup if (getWindowVisibility() == View.VISIBLE) { - int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight(); - final int[] xy = new int[2]; - getLocationOnScreen(xy); - // TODO: The 20 below should come from the theme and be expressed in dip - final float scale = getContext().getResources().getDisplayMetrics().density; - int bottomGap = screenHeight - xy[1] - getHeight() + (int) (scale * 20); - mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, - xy[0], bottomGap); + positionPopup(false); // Make sure we get focus if we are showing the popup checkFocus(); } } + private void positionPopup(boolean update) { + int screenHeight = getResources().getDisplayMetrics().heightPixels; + final int[] xy = new int[2]; + getLocationOnScreen(xy); + // TODO: The 20 below should come from the theme and be expressed in dip + // TODO: And the gravity should be defined in the theme as well + final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); + if (!update) { + mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, + xy[0], bottomGap); + } else { + mPopup.update(xy[0], bottomGap, -1, -1); + } + } + /** * What is the distance between the source and destination rectangles given the direction of * focus navigation between them? The direction basically helps figure out more quickly what is @@ -2831,7 +2875,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; } - if (okToSend) { + if (okToSend && acceptFilter()) { createTextFilter(true); KeyEvent forwardEvent = event; @@ -2873,6 +2917,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mTextFilter.addTextChangedListener(this); p.setFocusable(false); p.setTouchable(false); + p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); p.setContentView(mTextFilter); p.setWidth(LayoutParams.WRAP_CONTENT); p.setHeight(LayoutParams.WRAP_CONTENT); diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index 7086ae2..369221e 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -46,6 +46,18 @@ import java.util.Locale; public class Chronometer extends TextView { private static final String TAG = "Chronometer"; + /** + * A callback that notifies when the chronometer has incremented on its own. + */ + public interface OnChronometerTickListener { + + /** + * Notification that the chronometer has changed. + */ + void onChronometerTick(Chronometer chronometer); + + } + private long mBase; private boolean mVisible; private boolean mStarted; @@ -56,6 +68,7 @@ public class Chronometer extends TextView { private Locale mFormatterLocale; private Object[] mFormatterArgs = new Object[1]; private StringBuilder mFormatBuilder; + private OnChronometerTickListener mOnChronometerTickListener; /** * Initialize this Chronometer object. @@ -99,6 +112,7 @@ public class Chronometer extends TextView { * * @param base Use the {@link SystemClock#elapsedRealtime} time base. */ + @android.view.RemotableViewMethod public void setBase(long base) { mBase = base; updateText(SystemClock.elapsedRealtime()); @@ -122,6 +136,7 @@ public class Chronometer extends TextView { * * @param format the format string. */ + @android.view.RemotableViewMethod public void setFormat(String format) { mFormat = format; if (format != null && mFormatBuilder == null) { @@ -137,6 +152,23 @@ public class Chronometer extends TextView { } /** + * Sets the listener to be called when the chronometer changes. + * + * @param listener The listener. + */ + public void setOnChronometerTickListener(OnChronometerTickListener listener) { + mOnChronometerTickListener = listener; + } + + /** + * @return The listener (may be null) that is listening for chronometer change + * events. + */ + public OnChronometerTickListener getOnChronometerTickListener() { + return mOnChronometerTickListener; + } + + /** * Start counting up. This does not affect the base as set from {@link #setBase}, just * the view display. * @@ -161,6 +193,15 @@ public class Chronometer extends TextView { updateRunning(); } + /** + * The same as calling {@link #start} or {@link #stop}. + */ + @android.view.RemotableViewMethod + public void setStarted(boolean started) { + mStarted = started; + updateRunning(); + } + @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -216,8 +257,15 @@ public class Chronometer extends TextView { public void handleMessage(Message m) { if (mStarted) { updateText(SystemClock.elapsedRealtime()); + dispatchChronometerTick(); sendMessageDelayed(Message.obtain(), 1000); } } }; + + void dispatchChronometerTick() { + if (mOnChronometerTickListener != null) { + mOnChronometerTickListener.onChronometerTick(this); + } + } } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index b4ed3ba..8aafee2 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -93,6 +93,7 @@ public class FrameLayout extends ViewGroup { * * @attr ref android.R.styleable#FrameLayout_foregroundGravity */ + @android.view.RemotableViewMethod public void setForegroundGravity(int foregroundGravity) { if (mForegroundGravity != foregroundGravity) { if ((foregroundGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { @@ -348,6 +349,7 @@ public class FrameLayout extends ViewGroup { * * @attr ref android.R.styleable#FrameLayout_measureAllChildren */ + @android.view.RemotableViewMethod public void setMeasureAllChildren(boolean measureAll) { mMeasureAllChildren = measureAll; } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 4ae322e..94d1bd1 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -193,6 +193,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_adjustViewBounds */ + @android.view.RemotableViewMethod public void setAdjustViewBounds(boolean adjustViewBounds) { mAdjustViewBounds = adjustViewBounds; if (adjustViewBounds) { @@ -217,6 +218,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_maxWidth */ + @android.view.RemotableViewMethod public void setMaxWidth(int maxWidth) { mMaxWidth = maxWidth; } @@ -238,6 +240,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_maxHeight */ + @android.view.RemotableViewMethod public void setMaxHeight(int maxHeight) { mMaxHeight = maxHeight; } @@ -256,6 +259,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_src */ + @android.view.RemotableViewMethod public void setImageResource(int resId) { if (mUri != null || mResource != resId) { updateDrawable(null); @@ -272,6 +276,7 @@ public class ImageView extends View { * * @param uri The Uri of an image */ + @android.view.RemotableViewMethod public void setImageURI(Uri uri) { if (mResource != 0 || (mUri != uri && @@ -306,6 +311,7 @@ public class ImageView extends View { * * @param bm The bitmap to set */ + @android.view.RemotableViewMethod public void setImageBitmap(Bitmap bm) { // if this is used frequently, may handle bitmaps explicitly // to reduce the intermediate drawable object @@ -327,6 +333,7 @@ public class ImageView extends View { resizeFromDrawable(); } + @android.view.RemotableViewMethod public void setImageLevel(int level) { mLevel = level; if (mDrawable != null) { diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index 85a7339..a9822f8 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -136,6 +136,7 @@ public class LinearLayout extends ViewGroup { * * @attr ref android.R.styleable#LinearLayout_baselineAligned */ + @android.view.RemotableViewMethod public void setBaselineAligned(boolean baselineAligned) { mBaselineAligned = baselineAligned; } @@ -208,6 +209,7 @@ public class LinearLayout extends ViewGroup { * * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex */ + @android.view.RemotableViewMethod public void setBaselineAlignedChildIndex(int i) { if ((i < 0) || (i >= getChildCount())) { throw new IllegalArgumentException("base aligned child index out " @@ -265,6 +267,7 @@ public class LinearLayout extends ViewGroup { * to 0.0f if the weight sum should be computed from the children's * layout_weight */ + @android.view.RemotableViewMethod public void setWeightSum(float weightSum) { mWeightSum = Math.max(0.0f, weightSum); } @@ -1149,6 +1152,7 @@ public class LinearLayout extends ViewGroup { * * @attr ref android.R.styleable#LinearLayout_gravity */ + @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { @@ -1164,6 +1168,7 @@ public class LinearLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { @@ -1172,6 +1177,7 @@ public class LinearLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setVerticalGravity(int verticalGravity) { final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) { diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 9c7f600..4e5989c 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -2179,6 +2179,10 @@ public class ListView extends AbsListView { && !isViewAncestorOf(selectedView, this)) { selectedView = null; hideSelector(); + + // but we don't want to set the ressurect position (that would make subsequent + // unhandled key events bring back the item we just scrolled off!) + mResurrectToPosition = INVALID_POSITION; } if (needToRedraw) { diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index dada105..4a5cea1 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -24,6 +24,8 @@ import android.view.View; import android.view.WindowManager; import android.view.Gravity; import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.View.OnTouchListener; import android.graphics.PixelFormat; import android.graphics.Rect; @@ -33,6 +35,8 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import java.lang.ref.WeakReference; + /** * <p>A popup window that can be used to display an arbitrary view. The popup * windows is a floating container that appears on top of the current @@ -109,7 +113,23 @@ public class PopupWindow { private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] { com.android.internal.R.attr.state_above_anchor }; - + + private WeakReference<View> mAnchor; + private OnScrollChangedListener mOnScrollChangedListener = + new OnScrollChangedListener() { + public void onScrollChanged() { + View anchor = mAnchor.get(); + if (anchor != null && mPopupView != null) { + WindowManager.LayoutParams p = (WindowManager.LayoutParams) + mPopupView.getLayoutParams(); + + mAboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff); + update(p.x, p.y, -1, -1, true); + } + } + }; + private int mAnchorXoff, mAnchorYoff; + /** * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> * @@ -579,6 +599,8 @@ public class PopupWindow { return; } + unregisterForScrollChanged(); + mIsShowing = true; mIsDropdown = false; @@ -617,6 +639,8 @@ public class PopupWindow { * the popup in its entirety, this method tries to find a parent scroll * view to scroll. If no parent scroll view can be scrolled, the bottom-left * corner of the popup is pinned at the top left corner of the anchor view.</p> + * <p>If the view later scrolls to move <code>anchor</code> to a different + * location, the popup will be moved correspondingly.</p> * * @param anchor the view on which to pin the popup window * @@ -627,6 +651,8 @@ public class PopupWindow { return; } + registerForScrollChanged(anchor, xoff, yoff); + mIsShowing = true; mIsDropdown = true; @@ -894,6 +920,8 @@ public class PopupWindow { */ public void dismiss() { if (isShowing() && mPopupView != null) { + unregisterForScrollChanged(); + mWindowManager.removeView(mPopupView); if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { ((ViewGroup) mPopupView).removeView(mContentView); @@ -962,6 +990,25 @@ public class PopupWindow { * @param height the new height, can be -1 to ignore */ public void update(int x, int y, int width, int height) { + update(x, y, width, height, false); + } + + /** + * <p>Updates the position and the dimension of the popup window. Width and + * height can be set to -1 to update location only. Calling this function + * also updates the window with the current popup state as + * described for {@link #update()}.</p> + * + * @param x the new x location + * @param y the new y location + * @param width the new width, can be -1 to ignore + * @param height the new height, can be -1 to ignore + * @param force reposition the window even if the specified position + * already seems to correspond to the LayoutParams + * + * @hide pending API council approval + */ + public void update(int x, int y, int width, int height, boolean force) { if (width != -1) { mLastWidth = width; setWidth(width); @@ -979,7 +1026,7 @@ public class PopupWindow { WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); - boolean update = false; + boolean update = force; final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth; if (width != -1 && p.width != finalWidth) { @@ -1039,6 +1086,8 @@ public class PopupWindow { * height can be set to -1 to update location only. Calling this function * also updates the window with the current popup state as * described for {@link #update()}.</p> + * <p>If the view later scrolls to move <code>anchor</code> to a different + * location, the popup will be moved correspondingly.</p> * * @param anchor the popup's anchor view * @param xoff x offset from the view's left edge @@ -1051,6 +1100,12 @@ public class PopupWindow { return; } + WeakReference<View> oldAnchor = mAnchor; + if (oldAnchor == null || oldAnchor.get() != anchor || + mAnchorXoff != xoff || mAnchorYoff != yoff) { + registerForScrollChanged(anchor, xoff, yoff); + } + WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); @@ -1065,10 +1120,10 @@ public class PopupWindow { mPopupHeight = height; } - findDropDownPosition(anchor, p, xoff, yoff); + mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); update(p.x, p.y, width, height); } - + /** * Listener that is called when this popup window is dismissed. */ @@ -1078,7 +1133,33 @@ public class PopupWindow { */ public void onDismiss(); } - + + private void unregisterForScrollChanged() { + WeakReference<View> anchorRef = mAnchor; + View anchor = null; + if (anchorRef != null) { + anchor = anchorRef.get(); + } + if (anchor != null) { + ViewTreeObserver vto = anchor.getViewTreeObserver(); + vto.removeOnScrollChangedListener(mOnScrollChangedListener); + } + mAnchor = null; + } + + private void registerForScrollChanged(View anchor, int xoff, int yoff) { + unregisterForScrollChanged(); + + mAnchor = new WeakReference<View>(anchor); + ViewTreeObserver vto = anchor.getViewTreeObserver(); + if (vto != null) { + vto.addOnScrollChangedListener(mOnScrollChangedListener); + } + + mAnchorXoff = xoff; + mAnchorYoff = yoff; + } + private class PopupViewContainer extends FrameLayout { public PopupViewContainer(Context context) { diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 434e9f3..dd2570a 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -344,6 +344,7 @@ public class ProgressBar extends View { * * @param indeterminate true to enable the indeterminate mode */ + @android.view.RemotableViewMethod public synchronized void setIndeterminate(boolean indeterminate) { if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) { mIndeterminate = indeterminate; @@ -529,6 +530,7 @@ public class ProgressBar extends View { setProgress(progress, false); } + @android.view.RemotableViewMethod synchronized void setProgress(int progress, boolean fromUser) { if (mIndeterminate) { return; @@ -560,6 +562,7 @@ public class ProgressBar extends View { * @see #getSecondaryProgress() * @see #incrementSecondaryProgressBy(int) */ + @android.view.RemotableViewMethod public synchronized void setSecondaryProgress(int secondaryProgress) { if (mIndeterminate) { return; @@ -633,6 +636,7 @@ public class ProgressBar extends View { * @see #setProgress(int) * @see #setSecondaryProgress(int) */ + @android.view.RemotableViewMethod public synchronized void setMax(int max) { if (max < 0) { max = 0; diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index 9ded52b..ba63ec3 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -168,6 +168,7 @@ public class RelativeLayout extends ViewGroup { * * @attr ref android.R.styleable#RelativeLayout_ignoreGravity */ + @android.view.RemotableViewMethod public void setIgnoreGravity(int viewId) { mIgnoreGravity = viewId; } @@ -183,6 +184,7 @@ public class RelativeLayout extends ViewGroup { * * @attr ref android.R.styleable#RelativeLayout_gravity */ + @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { @@ -198,6 +200,7 @@ public class RelativeLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { @@ -206,6 +209,7 @@ public class RelativeLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setVerticalGravity(int verticalGravity) { final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 25afee8..e000d2e 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -31,6 +31,7 @@ import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; +import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater.Filter; @@ -38,10 +39,13 @@ import android.view.View.OnClickListener; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import java.lang.Class; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; @@ -80,19 +84,22 @@ public class RemoteViews implements Parcelable, Filter { /** - * This annotation indicates that a subclass of View is alllowed to be used with the - * {@link android.widget.RemoteViews} mechanism. + * This annotation indicates that a subclass of View is alllowed to be used + * with the {@link android.widget.RemoteViews} mechanism. */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface RemoteView { } - + /** * Exception to send when something goes wrong executing an action * */ public static class ActionException extends RuntimeException { + public ActionException(Exception ex) { + super(ex); + } public ActionException(String message) { super(message); } @@ -110,274 +117,7 @@ public class RemoteViews implements Parcelable, Filter { return 0; } }; - - /** - * Equivalent to calling View.setVisibility - */ - private class SetViewVisibility extends Action { - public SetViewVisibility(int id, int vis) { - viewId = id; - visibility = vis; - } - - public SetViewVisibility(Parcel parcel) { - viewId = parcel.readInt(); - visibility = parcel.readInt(); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(visibility); - } - - @Override - public void apply(View root) { - View target = root.findViewById(viewId); - if (target != null) { - target.setVisibility(visibility); - } - } - - private int viewId; - private int visibility; - public final static int TAG = 0; - } - - /** - * Equivalent to calling TextView.setText - */ - private class SetTextViewText extends Action { - public SetTextViewText(int id, CharSequence t) { - viewId = id; - text = t; - } - - public SetTextViewText(Parcel parcel) { - viewId = parcel.readInt(); - text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - TextUtils.writeToParcel(text, dest, flags); - } - - @Override - public void apply(View root) { - TextView target = (TextView) root.findViewById(viewId); - if (target != null) { - target.setText(text); - } - } - - int viewId; - CharSequence text; - public final static int TAG = 1; - } - - /** - * Equivalent to calling ImageView.setResource - */ - private class SetImageViewResource extends Action { - public SetImageViewResource(int id, int src) { - viewId = id; - srcId = src; - } - - public SetImageViewResource(Parcel parcel) { - viewId = parcel.readInt(); - srcId = parcel.readInt(); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(srcId); - } - - @Override - public void apply(View root) { - ImageView target = (ImageView) root.findViewById(viewId); - Drawable d = mContext.getResources().getDrawable(srcId); - if (target != null) { - target.setImageDrawable(d); - } - } - - int viewId; - int srcId; - public final static int TAG = 2; - } - - /** - * Equivalent to calling ImageView.setImageURI - */ - private class SetImageViewUri extends Action { - public SetImageViewUri(int id, Uri u) { - viewId = id; - uri = u; - } - - public SetImageViewUri(Parcel parcel) { - viewId = parcel.readInt(); - uri = Uri.CREATOR.createFromParcel(parcel); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - Uri.writeToParcel(dest, uri); - } - - @Override - public void apply(View root) { - ImageView target = (ImageView) root.findViewById(viewId); - if (target != null) { - target.setImageURI(uri); - } - } - - int viewId; - Uri uri; - public final static int TAG = 3; - } - - /** - * Equivalent to calling ImageView.setImageBitmap - */ - private class SetImageViewBitmap extends Action { - public SetImageViewBitmap(int id, Bitmap src) { - viewId = id; - bitmap = src; - } - - public SetImageViewBitmap(Parcel parcel) { - viewId = parcel.readInt(); - bitmap = Bitmap.CREATOR.createFromParcel(parcel); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - if (bitmap != null) { - bitmap.writeToParcel(dest, flags); - } - } - - @Override - public void apply(View root) { - if (bitmap != null) { - ImageView target = (ImageView) root.findViewById(viewId); - Drawable d = new BitmapDrawable(bitmap); - if (target != null) { - target.setImageDrawable(d); - } - } - } - int viewId; - Bitmap bitmap; - public final static int TAG = 4; - } - - /** - * Equivalent to calling Chronometer.setBase, Chronometer.setFormat, - * and Chronometer.start/stop. - */ - private class SetChronometer extends Action { - public SetChronometer(int id, long base, String format, boolean running) { - this.viewId = id; - this.base = base; - this.format = format; - this.running = running; - } - - public SetChronometer(Parcel parcel) { - viewId = parcel.readInt(); - base = parcel.readLong(); - format = parcel.readString(); - running = parcel.readInt() != 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeLong(base); - dest.writeString(format); - dest.writeInt(running ? 1 : 0); - } - - @Override - public void apply(View root) { - Chronometer target = (Chronometer) root.findViewById(viewId); - if (target != null) { - target.setBase(base); - target.setFormat(format); - if (running) { - target.start(); - } else { - target.stop(); - } - } - } - - int viewId; - boolean running; - long base; - String format; - - public final static int TAG = 5; - } - - /** - * Equivalent to calling ProgressBar.setMax, ProgressBar.setProgress and - * ProgressBar.setIndeterminate - */ - private class SetProgressBar extends Action { - public SetProgressBar(int id, int max, int progress, boolean indeterminate) { - this.viewId = id; - this.progress = progress; - this.max = max; - this.indeterminate = indeterminate; - } - - public SetProgressBar(Parcel parcel) { - viewId = parcel.readInt(); - progress = parcel.readInt(); - max = parcel.readInt(); - indeterminate = parcel.readInt() != 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(progress); - dest.writeInt(max); - dest.writeInt(indeterminate ? 1 : 0); - } - - @Override - public void apply(View root) { - ProgressBar target = (ProgressBar) root.findViewById(viewId); - if (target != null) { - target.setIndeterminate(indeterminate); - if (!indeterminate) { - target.setMax(max); - target.setProgress(progress); - } - } - } - - int viewId; - boolean indeterminate; - int progress; - int max; - - public final static int TAG = 6; - } - /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} @@ -421,7 +161,7 @@ public class RemoteViews implements Parcelable, Filter { int viewId; PendingIntent pendingIntent; - public final static int TAG = 7; + public final static int TAG = 1; } /** @@ -511,92 +251,215 @@ public class RemoteViews implements Parcelable, Filter { PorterDuff.Mode filterMode; int level; - public final static int TAG = 8; + public final static int TAG = 3; } /** - * Equivalent to calling {@link android.widget.TextView#setTextColor(int)}. + * Base class for the reflection actions. */ - private class SetTextColor extends Action { - public SetTextColor(int id, int color) { - this.viewId = id; - this.color = color; - } - - public SetTextColor(Parcel parcel) { - viewId = parcel.readInt(); - color = parcel.readInt(); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(color); - } - - @Override - public void apply(View root) { - final View target = root.findViewById(viewId); - if (target instanceof TextView) { - final TextView textView = (TextView) target; - textView.setTextColor(color); + private class ReflectionAction extends Action { + static final int TAG = 2; + + static final int BOOLEAN = 1; + static final int BYTE = 2; + static final int SHORT = 3; + static final int INT = 4; + static final int LONG = 5; + static final int FLOAT = 6; + static final int DOUBLE = 7; + static final int CHAR = 8; + static final int STRING = 9; + static final int CHAR_SEQUENCE = 10; + static final int URI = 11; + static final int BITMAP = 12; + + int viewId; + String methodName; + int type; + Object value; + + ReflectionAction(int viewId, String methodName, int type, Object value) { + this.viewId = viewId; + this.methodName = methodName; + this.type = type; + this.value = value; + } + + ReflectionAction(Parcel in) { + this.viewId = in.readInt(); + this.methodName = in.readString(); + this.type = in.readInt(); + if (false) { + Log.d("RemoteViews", "read viewId=0x" + Integer.toHexString(this.viewId) + + " methodName=" + this.methodName + " type=" + this.type); + } + switch (this.type) { + case BOOLEAN: + this.value = in.readInt() != 0; + break; + case BYTE: + this.value = in.readByte(); + break; + case SHORT: + this.value = (short)in.readInt(); + break; + case INT: + this.value = in.readInt(); + break; + case LONG: + this.value = in.readLong(); + break; + case FLOAT: + this.value = in.readFloat(); + break; + case DOUBLE: + this.value = in.readDouble(); + break; + case CHAR: + this.value = (char)in.readInt(); + break; + case STRING: + this.value = in.readString(); + break; + case CHAR_SEQUENCE: + this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + break; + case URI: + this.value = Uri.CREATOR.createFromParcel(in); + break; + case BITMAP: + this.value = Bitmap.CREATOR.createFromParcel(in); + break; + default: + break; } } - - int viewId; - int color; - public final static int TAG = 9; - } - - /** - * Equivalent to calling {@link android.widget.ViewFlipper#startFlipping()} - * or {@link android.widget.ViewFlipper#stopFlipping()} along with - * {@link android.widget.ViewFlipper#setFlipInterval(int)}. - */ - private class SetFlipping extends Action { - public SetFlipping(int id, boolean flipping, int milliseconds) { - this.viewId = id; - this.flipping = flipping; - this.milliseconds = milliseconds; - } - - public SetFlipping(Parcel parcel) { - viewId = parcel.readInt(); - flipping = parcel.readInt() != 0; - milliseconds = parcel.readInt(); + public void writeToParcel(Parcel out, int flags) { + out.writeInt(TAG); + out.writeInt(this.viewId); + out.writeString(this.methodName); + out.writeInt(this.type); + if (false) { + Log.d("RemoteViews", "write viewId=0x" + Integer.toHexString(this.viewId) + + " methodName=" + this.methodName + " type=" + this.type); + } + switch (this.type) { + case BOOLEAN: + out.writeInt(((Boolean)this.value).booleanValue() ? 1 : 0); + break; + case BYTE: + out.writeByte(((Byte)this.value).byteValue()); + break; + case SHORT: + out.writeInt(((Short)this.value).shortValue()); + break; + case INT: + out.writeInt(((Integer)this.value).intValue()); + break; + case LONG: + out.writeLong(((Long)this.value).longValue()); + break; + case FLOAT: + out.writeFloat(((Float)this.value).floatValue()); + break; + case DOUBLE: + out.writeDouble(((Double)this.value).doubleValue()); + break; + case CHAR: + out.writeInt((int)((Character)this.value).charValue()); + break; + case STRING: + out.writeString((String)this.value); + break; + case CHAR_SEQUENCE: + TextUtils.writeToParcel((CharSequence)this.value, out, flags); + break; + case URI: + ((Uri)this.value).writeToParcel(out, flags); + break; + case BITMAP: + ((Bitmap)this.value).writeToParcel(out, flags); + break; + default: + break; + } } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(flipping ? 1 : 0); - dest.writeInt(milliseconds); + + private Class getParameterType() { + switch (this.type) { + case BOOLEAN: + return boolean.class; + case BYTE: + return byte.class; + case SHORT: + return short.class; + case INT: + return int.class; + case LONG: + return long.class; + case FLOAT: + return float.class; + case DOUBLE: + return double.class; + case CHAR: + return char.class; + case STRING: + return String.class; + case CHAR_SEQUENCE: + return CharSequence.class; + case URI: + return Uri.class; + case BITMAP: + return Bitmap.class; + default: + return null; + } } - + @Override public void apply(View root) { - final View target = root.findViewById(viewId); - if (target instanceof ViewFlipper) { - final ViewFlipper flipper = (ViewFlipper) target; - if (milliseconds != -1) { - flipper.setFlipInterval(milliseconds); - } - if (flipping) { - flipper.startFlipping(); - } else { - flipper.stopFlipping(); + final View view = root.findViewById(viewId); + if (view == null) { + throw new ActionException("can't find view: 0x" + Integer.toHexString(viewId)); + } + + Class param = getParameterType(); + if (param == null) { + throw new ActionException("bad type: " + this.type); + } + + Class klass = view.getClass(); + Method method = null; + try { + method = klass.getMethod(this.methodName, getParameterType()); + } + catch (NoSuchMethodException ex) { + throw new ActionException("view: " + klass.getName() + " doesn't have method: " + + this.methodName + "(" + param.getName() + ")"); + } + + if (!method.isAnnotationPresent(RemotableViewMethod.class)) { + throw new ActionException("view: " + klass.getName() + + " can't use method with RemoteViews: " + + this.methodName + "(" + param.getName() + ")"); + } + + try { + if (false) { + Log.d("RemoteViews", "view: " + klass.getName() + " calling method: " + + this.methodName + "(" + param.getName() + ") with " + + (this.value == null ? "null" : this.value.getClass().getName())); } + method.invoke(view, this.value); + } + catch (Exception ex) { + throw new ActionException(ex); } } - - int viewId; - boolean flipping; - int milliseconds; - - public final static int TAG = 10; } + /** * Create a new RemoteViews object that will display the views contained * in the specified layout file. @@ -623,41 +486,17 @@ public class RemoteViews implements Parcelable, Filter { for (int i=0; i<count; i++) { int tag = parcel.readInt(); switch (tag) { - case SetViewVisibility.TAG: - mActions.add(new SetViewVisibility(parcel)); - break; - case SetTextViewText.TAG: - mActions.add(new SetTextViewText(parcel)); - break; - case SetImageViewResource.TAG: - mActions.add(new SetImageViewResource(parcel)); - break; - case SetImageViewUri.TAG: - mActions.add(new SetImageViewUri(parcel)); - break; - case SetImageViewBitmap.TAG: - mActions.add(new SetImageViewBitmap(parcel)); - break; - case SetChronometer.TAG: - mActions.add(new SetChronometer(parcel)); - break; - case SetProgressBar.TAG: - mActions.add(new SetProgressBar(parcel)); - break; case SetOnClickPendingIntent.TAG: mActions.add(new SetOnClickPendingIntent(parcel)); break; case SetDrawableParameters.TAG: mActions.add(new SetDrawableParameters(parcel)); break; - case SetTextColor.TAG: - mActions.add(new SetTextColor(parcel)); - break; - case SetFlipping.TAG: - mActions.add(new SetFlipping(parcel)); + case ReflectionAction.TAG: + mActions.add(new ReflectionAction(parcel)); break; default: - throw new ActionException("Tag " + tag + "not found"); + throw new ActionException("Tag " + tag + " not found"); } } } @@ -690,7 +529,7 @@ public class RemoteViews implements Parcelable, Filter { * @param visibility The new visibility for the view */ public void setViewVisibility(int viewId, int visibility) { - addAction(new SetViewVisibility(viewId, visibility)); + setInt(viewId, "setVisibility", visibility); } /** @@ -700,7 +539,7 @@ public class RemoteViews implements Parcelable, Filter { * @param text The new text for the view */ public void setTextViewText(int viewId, CharSequence text) { - addAction(new SetTextViewText(viewId, text)); + setCharSequence(viewId, "setText", text); } /** @@ -710,7 +549,7 @@ public class RemoteViews implements Parcelable, Filter { * @param srcId The new resource id for the drawable */ public void setImageViewResource(int viewId, int srcId) { - addAction(new SetImageViewResource(viewId, srcId)); + setInt(viewId, "setImageResource", srcId); } /** @@ -720,7 +559,7 @@ public class RemoteViews implements Parcelable, Filter { * @param uri The Uri for the image */ public void setImageViewUri(int viewId, Uri uri) { - addAction(new SetImageViewUri(viewId, uri)); + setUri(viewId, "setImageURI", uri); } /** @@ -730,7 +569,7 @@ public class RemoteViews implements Parcelable, Filter { * @param bitmap The new Bitmap for the drawable */ public void setImageViewBitmap(int viewId, Bitmap bitmap) { - addAction(new SetImageViewBitmap(viewId, bitmap)); + setBitmap(viewId, "setImageBitmap", bitmap); } /** @@ -745,16 +584,20 @@ public class RemoteViews implements Parcelable, Filter { * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}. * @param format The Chronometer format string, or null to * simply display the timer value. - * @param running True if you want the clock to be running, false if not. + * @param started True if you want the clock to be started, false if not. */ - public void setChronometer(int viewId, long base, String format, boolean running) { - addAction(new SetChronometer(viewId, base, format, running)); + public void setChronometer(int viewId, long base, String format, boolean started) { + setLong(viewId, "setBase", base); + setString(viewId, "setFormat", format); + setBoolean(viewId, "setStarted", started); } /** * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax}, * {@link ProgressBar#setProgress ProgressBar.setProgress}, and * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate} + * + * If indeterminate is true, then the values for max and progress are ignored. * * @param viewId The id of the view whose text should change * @param max The 100% value for the progress bar @@ -764,7 +607,11 @@ public class RemoteViews implements Parcelable, Filter { */ public void setProgressBar(int viewId, int max, int progress, boolean indeterminate) { - addAction(new SetProgressBar(viewId, max, progress, indeterminate)); + setBoolean(viewId, "setIndeterminate", indeterminate); + if (!indeterminate) { + setInt(viewId, "setMax", max); + setInt(viewId, "setProgress", progress); + } } /** @@ -780,6 +627,7 @@ public class RemoteViews implements Parcelable, Filter { } /** + * @hide * Equivalent to calling a combination of {@link Drawable#setAlpha(int)}, * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given @@ -818,23 +666,55 @@ public class RemoteViews implements Parcelable, Filter { * focused) to be this color. */ public void setTextColor(int viewId, int color) { - addAction(new SetTextColor(viewId, color)); + setInt(viewId, "setTextColor", color); } - /** - * Equivalent to calling {@link android.widget.ViewFlipper#startFlipping()} - * or {@link android.widget.ViewFlipper#stopFlipping()} along with - * {@link android.widget.ViewFlipper#setFlipInterval(int)}. - * - * @param viewId The id of the view to apply changes to - * @param flipping True means we should - * {@link android.widget.ViewFlipper#startFlipping()}, otherwise - * {@link android.widget.ViewFlipper#stopFlipping()}. - * @param milliseconds How long to wait before flipping to the next view, or - * -1 to leave unchanged. - */ - public void setFlipping(int viewId, boolean flipping, int milliseconds) { - addAction(new SetFlipping(viewId, flipping, milliseconds)); + public void setBoolean(int viewId, String methodName, boolean value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BOOLEAN, value)); + } + + public void setByte(int viewId, String methodName, byte value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BYTE, value)); + } + + public void setShort(int viewId, String methodName, short value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.SHORT, value)); + } + + public void setInt(int viewId, String methodName, int value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value)); + } + + public void setLong(int viewId, String methodName, long value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.LONG, value)); + } + + public void setFloat(int viewId, String methodName, float value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.FLOAT, value)); + } + + public void setDouble(int viewId, String methodName, double value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.DOUBLE, value)); + } + + public void setChar(int viewId, String methodName, char value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR, value)); + } + + public void setString(int viewId, String methodName, String value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.STRING, value)); + } + + public void setCharSequence(int viewId, String methodName, CharSequence value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value)); + } + + public void setUri(int viewId, String methodName, Uri value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.URI, value)); + } + + public void setBitmap(int viewId, String methodName, Bitmap value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BITMAP, value)); } /** diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index da4a077..dc2c70d 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -405,7 +405,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); * Specify a label and icon as the tab indicator. */ public TabSpec setIndicator(CharSequence label, Drawable icon) { - mIndicatorStrategy = new LabelAndIconIndicatorStategy(label, icon); + mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon); return this; } @@ -497,12 +497,12 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); /** * How we create a tab indicator that has a label and an icon */ - private class LabelAndIconIndicatorStategy implements IndicatorStrategy { + private class LabelAndIconIndicatorStrategy implements IndicatorStrategy { private final CharSequence mLabel; private final Drawable mIcon; - private LabelAndIconIndicatorStategy(CharSequence label, Drawable icon) { + private LabelAndIconIndicatorStrategy(CharSequence label, Drawable icon) { mLabel = label; mIcon = icon; } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 2ae5d4e..bdc54ff 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -87,6 +87,7 @@ import android.view.ViewDebug; import android.view.ViewTreeObserver; import android.view.ViewGroup.LayoutParams; import android.view.animation.AnimationUtils; +import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -1521,6 +1522,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textSize */ + @android.view.RemotableViewMethod public void setTextSize(float size) { setTextSize(TypedValue.COMPLEX_UNIT_SP, size); } @@ -1572,6 +1574,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textScaleX */ + @android.view.RemotableViewMethod public void setTextScaleX(float size) { if (size != mTextPaint.getTextScaleX()) { mTextPaint.setTextScaleX(size); @@ -1620,6 +1623,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColor */ + @android.view.RemotableViewMethod public void setTextColor(int color) { mTextColor = ColorStateList.valueOf(color); updateTextColors(); @@ -1662,6 +1666,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColorHighlight */ + @android.view.RemotableViewMethod public void setHighlightColor(int color) { if (mHighlightColor != color) { mHighlightColor = color; @@ -1703,6 +1708,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_autoLink */ + @android.view.RemotableViewMethod public final void setAutoLinkMask(int mask) { mAutoLinkMask = mask; } @@ -1715,6 +1721,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_linksClickable */ + @android.view.RemotableViewMethod public final void setLinksClickable(boolean whether) { mLinksClickable = whether; } @@ -1751,6 +1758,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColorHint */ + @android.view.RemotableViewMethod public final void setHintTextColor(int color) { mHintTextColor = ColorStateList.valueOf(color); updateTextColors(); @@ -1789,6 +1797,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColorLink */ + @android.view.RemotableViewMethod public final void setLinkTextColor(int color) { mLinkTextColor = ColorStateList.valueOf(color); updateTextColors(); @@ -1876,6 +1885,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * reflows the text if they are different from the old flags. * @see Paint#setFlags */ + @android.view.RemotableViewMethod public void setPaintFlags(int flags) { if (mTextPaint.getFlags() != flags) { mTextPaint.setFlags(flags); @@ -1909,6 +1919,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minLines */ + @android.view.RemotableViewMethod public void setMinLines(int minlines) { mMinimum = minlines; mMinMode = LINES; @@ -1922,6 +1933,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minHeight */ + @android.view.RemotableViewMethod public void setMinHeight(int minHeight) { mMinimum = minHeight; mMinMode = PIXELS; @@ -1935,6 +1947,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxLines */ + @android.view.RemotableViewMethod public void setMaxLines(int maxlines) { mMaximum = maxlines; mMaxMode = LINES; @@ -1948,6 +1961,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxHeight */ + @android.view.RemotableViewMethod public void setMaxHeight(int maxHeight) { mMaximum = maxHeight; mMaxMode = PIXELS; @@ -1961,6 +1975,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_lines */ + @android.view.RemotableViewMethod public void setLines(int lines) { mMaximum = mMinimum = lines; mMaxMode = mMinMode = LINES; @@ -1976,6 +1991,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_height */ + @android.view.RemotableViewMethod public void setHeight(int pixels) { mMaximum = mMinimum = pixels; mMaxMode = mMinMode = PIXELS; @@ -1989,6 +2005,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minEms */ + @android.view.RemotableViewMethod public void setMinEms(int minems) { mMinWidth = minems; mMinWidthMode = EMS; @@ -2002,6 +2019,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minWidth */ + @android.view.RemotableViewMethod public void setMinWidth(int minpixels) { mMinWidth = minpixels; mMinWidthMode = PIXELS; @@ -2015,6 +2033,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxEms */ + @android.view.RemotableViewMethod public void setMaxEms(int maxems) { mMaxWidth = maxems; mMaxWidthMode = EMS; @@ -2028,6 +2047,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxWidth */ + @android.view.RemotableViewMethod public void setMaxWidth(int maxpixels) { mMaxWidth = maxpixels; mMaxWidthMode = PIXELS; @@ -2041,6 +2061,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_ems */ + @android.view.RemotableViewMethod public void setEms(int ems) { mMaxWidth = mMinWidth = ems; mMaxWidthMode = mMinWidthMode = EMS; @@ -2056,6 +2077,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_width */ + @android.view.RemotableViewMethod public void setWidth(int pixels) { mMaxWidth = mMinWidth = pixels; mMaxWidthMode = mMinWidthMode = PIXELS; @@ -2321,6 +2343,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_freezesText */ + @android.view.RemotableViewMethod public void setFreezesText(boolean freezesText) { mFreezesText = freezesText; } @@ -2366,6 +2389,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_text */ + @android.view.RemotableViewMethod public final void setText(CharSequence text) { setText(text, mBufferType); } @@ -2378,6 +2402,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @see #setText(CharSequence) */ + @android.view.RemotableViewMethod public final void setTextKeepState(CharSequence text) { setTextKeepState(text, mBufferType); } @@ -2648,6 +2673,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + @android.view.RemotableViewMethod public final void setText(int resid) { setText(getContext().getResources().getText(resid)); } @@ -2666,6 +2692,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_hint */ + @android.view.RemotableViewMethod public final void setHint(CharSequence hint) { mHint = TextUtils.stringOrSpannedString(hint); @@ -2686,6 +2713,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_hint */ + @android.view.RemotableViewMethod public final void setHint(int resid) { setHint(getContext().getResources().getText(resid)); } @@ -2896,6 +2924,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * <code>error</code> is <code>null</code>, the error message and icon * will be cleared. */ + @android.view.RemotableViewMethod public void setError(CharSequence error) { if (error == null) { setError(null, null); @@ -2954,10 +2983,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mPopup == null) { LayoutInflater inflater = LayoutInflater.from(getContext()); - TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint, + final TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint, null); - mPopup = new PopupWindow(err, 200, 50); + mPopup = new PopupWindow(err, 200, 50) { + private boolean mAbove = false; + + @Override + public void update(int x, int y, int w, int h, boolean force) { + super.update(x, y, w, h, force); + + boolean above = isAboveAnchor(); + if (above != mAbove) { + mAbove = above; + + if (above) { + err.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error_above); + } else { + err.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error); + } + } + } + }; mPopup.setFocusable(false); } @@ -5094,6 +5141,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_singleLine */ + @android.view.RemotableViewMethod public void setSingleLine(boolean singleLine) { if ((mInputType&EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { @@ -5168,6 +5216,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_selectAllOnFocus */ + @android.view.RemotableViewMethod public void setSelectAllOnFocus(boolean selectAllOnFocus) { mSelectAllOnFocus = selectAllOnFocus; @@ -5181,6 +5230,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_cursorVisible */ + @android.view.RemotableViewMethod public void setCursorVisible(boolean visible) { mCursorVisible = visible; invalidate(); @@ -5730,6 +5780,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener startStopMarquee(hasWindowFocus); } + /** + * Use {@link BaseInputConnection#removeComposingSpans + * BaseInputConnection.removeComposingSpans()} to remove any IME composing + * state from this text view. + */ + public void clearComposingText() { + if (mText instanceof Spannable) { + BaseInputConnection.removeComposingSpans((Spannable)mText); + } + } + @Override public void setSelected(boolean selected) { boolean wasSelected = isSelected(); diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index e20bfdf..8a7946b 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -31,7 +31,6 @@ import android.widget.RemoteViews.RemoteView; * * @attr ref android.R.styleable#ViewFlipper_flipInterval */ -@RemoteView public class ViewFlipper extends ViewAnimator { private int mFlipInterval = 3000; private boolean mKeepFlipping = false; @@ -56,6 +55,7 @@ public class ViewFlipper extends ViewAnimator { * @param milliseconds * time in milliseconds */ + @android.view.RemotableViewMethod public void setFlipInterval(int milliseconds) { mFlipInterval = milliseconds; } diff --git a/core/java/android/widget/ZoomRing.java b/core/java/android/widget/ZoomRing.java index be3b1fb..22881b3 100644 --- a/core/java/android/widget/ZoomRing.java +++ b/core/java/android/widget/ZoomRing.java @@ -7,7 +7,11 @@ import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.graphics.drawable.RotateDrawable; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; import android.util.AttributeSet; +import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; @@ -19,10 +23,8 @@ import android.view.ViewConfiguration; public class ZoomRing extends View { // TODO: move to ViewConfiguration? - static final int DOUBLE_TAP_DISMISS_TIMEOUT = ViewConfiguration.getJumpTapTimeout(); + static final int DOUBLE_TAP_DISMISS_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); // TODO: get from theme - private static final int DISABLED_ALPHA = 160; - private static final String TAG = "ZoomRing"; // TODO: Temporary until the trail is done @@ -32,15 +34,26 @@ public class ZoomRing extends View { private static final int THUMB_DISTANCE = 63; /** To avoid floating point calculations, we multiply radians by this value. */ - public static final int RADIAN_INT_MULTIPLIER = 100000000; + public static final int RADIAN_INT_MULTIPLIER = 10000; + public static final int RADIAN_INT_ERROR = 100; /** PI using our multiplier. */ public static final int PI_INT_MULTIPLIED = (int) (Math.PI * RADIAN_INT_MULTIPLIER); + public static final int TWO_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED * 2; /** PI/2 using our multiplier. */ private static final int HALF_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED / 2; private int mZeroAngle = HALF_PI_INT_MULTIPLIED * 3; + + private static final int THUMB_GRAB_SLOP = PI_INT_MULTIPLIED / 8; + private static final int THUMB_DRAG_SLOP = PI_INT_MULTIPLIED / 12; - private static final int THUMB_GRAB_SLOP = PI_INT_MULTIPLIED / 4; + /** + * Includes error because we compare this to the result of + * getDelta(getClosestTickeAngle(..), oldAngle) which ends up having some + * rounding error. + */ + private static final int MAX_ABS_JUMP_DELTA_ANGLE = (2 * PI_INT_MULTIPLIED / 3) + + RADIAN_INT_ERROR; /** The cached X of our center. */ private int mCenterX; @@ -49,10 +62,13 @@ public class ZoomRing extends View { /** The angle of the thumb (in int radians) */ private int mThumbAngle; - private boolean mIsThumbAngleValid; private int mThumbHalfWidth; private int mThumbHalfHeight; - + + private int mThumbCwBound = Integer.MIN_VALUE; + private int mThumbCcwBound = Integer.MIN_VALUE; + private boolean mEnforceMaxAbsJump = true; + /** The inner radius of the track. */ private int mBoundInnerRadiusSquared = 0; /** The outer radius of the track. */ @@ -63,8 +79,23 @@ public class ZoomRing extends View { private boolean mDrawThumb = true; private Drawable mThumbDrawable; - + + /** Shown beneath the thumb if we can still zoom in. */ + private Drawable mThumbPlusArrowDrawable; + /** Shown beneath the thumb if we can still zoom out. */ + private Drawable mThumbMinusArrowDrawable; + private static final int THUMB_ARROWS_FADE_DURATION = 300; + private long mThumbArrowsFadeStartTime; + private int mThumbArrowsAlpha = 255; + private static final int MODE_IDLE = 0; + + /** + * User has his finger down somewhere on the ring (besides the thumb) and we + * are waiting for him to move the slop amount before considering him in the + * drag thumb state. + */ + private static final int MODE_WAITING_FOR_DRAG_THUMB = 5; private static final int MODE_DRAG_THUMB = 1; /** * User has his finger down, but we are waiting for him to pass the touch @@ -74,24 +105,47 @@ 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; private int mMode; - private long mPreviousDownTime; + private long mPreviousUpTime; private int mPreviousDownX; private int mPreviousDownY; - private Disabler mDisabler = new Disabler(); - + private int mWaitingForDragThumbDownAngle; + private OnZoomRingCallback mCallback; private int mPreviousCallbackAngle; private int mCallbackThreshold = Integer.MAX_VALUE; private boolean mResetThumbAutomatically = true; private int mThumbDragStartAngle; + private final int mTouchSlop; + private Drawable mTrail; private double mAcculumalatedTrailAngle; - + + private Scroller mThumbScroller; + + private static final int MSG_THUMB_SCROLLER_TICK = 1; + private static final int MSG_THUMB_ARROWS_FADE_TICK = 2; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_THUMB_SCROLLER_TICK: + onThumbScrollerTick(); + break; + + case MSG_THUMB_ARROWS_FADE_TICK: + onThumbArrowsFadeTick(); + break; + } + } + }; + public ZoomRing(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); @@ -101,6 +155,10 @@ public class ZoomRing extends View { // TODO get drawables from style instead Resources res = context.getResources(); mThumbDrawable = res.getDrawable(R.drawable.zoom_ring_thumb); + mThumbPlusArrowDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_plus_arrow_rotatable). + mutate(); + mThumbMinusArrowDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_minus_arrow_rotatable). + mutate(); if (DRAW_TRAIL) { mTrail = res.getDrawable(R.drawable.zoom_ring_trail).mutate(); } @@ -108,7 +166,7 @@ public class ZoomRing extends View { // TODO: add padding to drawable setBackgroundResource(R.drawable.zoom_ring_track); // TODO get from style - setBounds(30, Integer.MAX_VALUE); + setRingBounds(30, Integer.MAX_VALUE); mThumbHalfHeight = mThumbDrawable.getIntrinsicHeight() / 2; mThumbHalfWidth = mThumbDrawable.getIntrinsicWidth() / 2; @@ -134,7 +192,7 @@ public class ZoomRing extends View { } // TODO: from XML too - public void setBounds(int innerRadius, int outerRadius) { + public void setRingBounds(int innerRadius, int outerRadius) { mBoundInnerRadiusSquared = innerRadius * innerRadius; if (mBoundInnerRadiusSquared < innerRadius) { // Prevent overflow @@ -148,7 +206,64 @@ public class ZoomRing extends View { } } + public void setThumbClockwiseBound(int angle) { + if (angle < 0) { + mThumbCwBound = Integer.MIN_VALUE; + } else { + mThumbCwBound = getClosestTickAngle(angle); + } + setEnforceMaxAbsJump(); + } + + public void setThumbCounterclockwiseBound(int angle) { + if (angle < 0) { + mThumbCcwBound = Integer.MIN_VALUE; + } else { + mThumbCcwBound = getClosestTickAngle(angle); + } + setEnforceMaxAbsJump(); + } + + private void setEnforceMaxAbsJump() { + // If there are bounds in both direction, there is no reason to restrict + // the amount that a user can absolute jump to + mEnforceMaxAbsJump = + mThumbCcwBound == Integer.MIN_VALUE || mThumbCwBound == Integer.MIN_VALUE; + } + + public int getThumbAngle() { + return mThumbAngle; + } + public void setThumbAngle(int angle) { + angle = getValidAngle(angle); + mPreviousCallbackAngle = getClosestTickAngle(angle); + setThumbAngleAuto(angle, false, false); + } + + /** + * Sets the thumb angle. If already animating, will continue the animation, + * otherwise it will do a direct jump. + * + * @param angle + * @param useDirection Whether to use the ccw parameter + * @param ccw Whether going counterclockwise (only used if useDirection is true) + */ + private void setThumbAngleAuto(int angle, boolean useDirection, boolean ccw) { + if (mThumbScroller == null + || mThumbScroller.isFinished() + || Math.abs(getDelta(angle, getThumbScrollerAngle())) < THUMB_GRAB_SLOP) { + setThumbAngleInt(angle); + } else { + if (useDirection) { + setThumbAngleAnimated(angle, 0, ccw); + } else { + setThumbAngleAnimated(angle, 0); + } + } + } + + private void setThumbAngleInt(int angle) { mThumbAngle = angle; int unoffsetAngle = angle + mZeroAngle; int thumbCenterX = (int) (Math.cos(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) * @@ -161,6 +276,10 @@ public class ZoomRing extends View { thumbCenterX + mThumbHalfWidth, thumbCenterY + mThumbHalfHeight); + if (mThumbArrowsAlpha > 0) { + setThumbArrowsAngle(angle); + } + if (DRAW_TRAIL) { double degrees; degrees = Math.min(359.0, Math.abs(mAcculumalatedTrailAngle)); @@ -174,10 +293,66 @@ public class ZoomRing extends View { invalidate(); } + + /** + * + * @param angle + * @param duration The animation duration, or 0 for the default duration. + */ + public void setThumbAngleAnimated(int angle, int duration) { + // The angle when going from the current angle to the new angle + int deltaAngle = getDelta(mThumbAngle, angle); + // Counter clockwise if the new angle is more the current angle + boolean counterClockwise = deltaAngle > 0; + + if (deltaAngle > PI_INT_MULTIPLIED || deltaAngle < -PI_INT_MULTIPLIED) { + // It's quicker to go the other direction + counterClockwise = !counterClockwise; + } + + setThumbAngleAnimated(angle, duration, counterClockwise); + } + + public void setThumbAngleAnimated(int angle, int duration, boolean counterClockwise) { + if (mThumbScroller == null) { + mThumbScroller = new Scroller(mContext); + } + + int startAngle = mThumbAngle; + int endAngle = getValidAngle(angle); + int deltaAngle = getDelta(startAngle, endAngle, counterClockwise); + if (startAngle + deltaAngle < 0) { + // Keep our angles positive + startAngle += TWO_PI_INT_MULTIPLIED; + } + + if (!mThumbScroller.isFinished()) { + duration = mThumbScroller.getDuration() - mThumbScroller.timePassed(); + } else if (duration == 0) { + duration = getAnimationDuration(deltaAngle); + } + mThumbScroller.startScroll(startAngle, 0, deltaAngle, 0, duration); + onThumbScrollerTick(); + } + + private int getAnimationDuration(int deltaAngle) { + if (deltaAngle < 0) deltaAngle *= -1; + return 300 + deltaAngle * 300 / RADIAN_INT_MULTIPLIER; + } + + private void onThumbScrollerTick() { + if (!mThumbScroller.computeScrollOffset()) return; + setThumbAngleInt(getThumbScrollerAngle()); + mHandler.sendEmptyMessage(MSG_THUMB_SCROLLER_TICK); + } + private int getThumbScrollerAngle() { + return mThumbScroller.getCurrX() % TWO_PI_INT_MULTIPLIED; + } + public void resetThumbAngle(int angle) { mPreviousCallbackAngle = angle; - setThumbAngle(angle); + setThumbAngleInt(angle); } public void resetThumbAngle() { @@ -185,7 +360,7 @@ public class ZoomRing extends View { resetThumbAngle(0); } } - + public void setResetThumbAutomatically(boolean resetThumbAutomatically) { mResetThumbAutomatically = resetThumbAutomatically; } @@ -214,6 +389,9 @@ public class ZoomRing extends View { if (DRAW_TRAIL) { mTrail.setBounds(0, 0, right - left, bottom - top); } + + mThumbPlusArrowDrawable.setBounds(0, 0, right - left, bottom - top); + mThumbMinusArrowDrawable.setBounds(0, 0, right - left, bottom - top); } @Override @@ -227,15 +405,13 @@ public class ZoomRing extends View { mMode = MODE_IDLE; mPreviousWidgetDragX = mPreviousWidgetDragY = Integer.MIN_VALUE; mAcculumalatedTrailAngle = 0.0; - mIsThumbAngleValid = false; } public void setTapDragMode(boolean tapDragMode, int x, int y) { resetState(); mMode = tapDragMode ? MODE_TAP_DRAG : MODE_IDLE; - mIsThumbAngleValid = false; - if (tapDragMode && mCallback != null) { + if (tapDragMode) { onThumbDragStarted(getAngle(x - mCenterX, y - mCenterY)); } } @@ -244,35 +420,44 @@ public class ZoomRing extends View { switch (action) { case MotionEvent.ACTION_DOWN: - if (mPreviousDownTime + DOUBLE_TAP_DISMISS_TIMEOUT >= time) { - if (mCallback != null) { - mCallback.onZoomRingDismissed(); - } - } else { - mPreviousDownTime = time; - mPreviousDownX = x; - mPreviousDownY = y; + mCallback.onUserInteractionStarted(); + + if (time - mPreviousUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT) { + mCallback.onZoomRingDismissed(true); } + + mPreviousDownX = x; + mPreviousDownY = y; resetState(); - return true; + // 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: - if (mCallback != null) { - if (mMode == MODE_MOVE_ZOOM_RING || mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { - mCallback.onZoomRingSetMovableHintVisible(false); - if (mMode == MODE_MOVE_ZOOM_RING) { - mCallback.onZoomRingMovingStopped(); - } - } else if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) { - onThumbDragStopped(getAngle(x - mCenterX, y - mCenterY)); + if (mMode == MODE_MOVE_ZOOM_RING || mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { + mCallback.onZoomRingSetMovableHintVisible(false); + if (mMode == MODE_MOVE_ZOOM_RING) { + mCallback.onZoomRingMovingStopped(); + } + } else if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG || + mMode == MODE_WAITING_FOR_DRAG_THUMB) { + onThumbDragStopped(); + + if (mMode == MODE_DRAG_THUMB) { + // Animate back to a tick + setThumbAngleAnimated(mPreviousCallbackAngle, 0); } } - mDisabler.setEnabling(true); + + mPreviousUpTime = time; + mCallback.onUserInteractionStopped(); return true; default: @@ -283,18 +468,18 @@ public class ZoomRing extends View { int localX = x - mCenterX; int localY = y - mCenterY; boolean isTouchingThumb = true; - boolean isInBounds = 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; - isInBounds = false; + isInRingBounds = false; } - int deltaThumbAndTouch = getDelta(touchAngle, mThumbAngle); + int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle); int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ? deltaThumbAndTouch : -deltaThumbAndTouch; if (isTouchingThumb && @@ -305,17 +490,68 @@ public class ZoomRing extends View { if (mMode == MODE_IDLE) { if (isTouchingThumb) { + // They grabbed the thumb mMode = MODE_DRAG_THUMB; + onThumbDragStarted(touchAngle); + + } else if (isInRingBounds) { + // They tapped somewhere else on the ring + int tickAngle = getClosestTickAngle(touchAngle); + + int deltaThumbAndTick = getDelta(mThumbAngle, tickAngle); + int boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); + + if (mEnforceMaxAbsJump) { + // Enforcing the max jump + if (deltaThumbAndTick > MAX_ABS_JUMP_DELTA_ANGLE || + deltaThumbAndTick < -MAX_ABS_JUMP_DELTA_ANGLE) { + // Trying to jump too far, ignore this touch interaction + mMode = MODE_IGNORE_UNTIL_UP; + return true; + } + + // Make sure we only let them jump within bounds + if (boundAngle != Integer.MIN_VALUE) { + tickAngle = boundAngle; + } + } else { + // Not enforcing the max jump, but we have to make sure + // we're getting to the tapped angle by going through the + // in-bounds region + if (boundAngle != Integer.MIN_VALUE) { + // Going this direction hits a bound, let's go the opposite direction + boolean oldDirectionIsCcw = deltaThumbAndTick > 0; + deltaThumbAndTick = getDelta(mThumbAngle, tickAngle, !oldDirectionIsCcw); + boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); + if (boundAngle != Integer.MIN_VALUE) { + Log + .d( + TAG, + "Tapped somewhere where the shortest distance goes through a bound, but then the opposite direction also went through a bound!"); + } + } + } + + mMode = MODE_WAITING_FOR_DRAG_THUMB; + mWaitingForDragThumbDownAngle = touchAngle; + boolean ccw = deltaThumbAndTick > 0; + setThumbAngleAnimated(tickAngle, 0, ccw); + + // Our thumb scrolling animation takes us from mThumbAngle to tickAngle + onThumbDragStarted(mThumbAngle); + onThumbDragged(tickAngle, true, ccw); + } else { + // They tapped somewhere else mMode = MODE_WAITING_FOR_MOVE_ZOOM_RING; + mCallback.onZoomRingSetMovableHintVisible(true); } - if (mCallback != null) { - if (mMode == MODE_DRAG_THUMB) { - onThumbDragStarted(touchAngle); - } else if (mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { - mCallback.onZoomRingSetMovableHintVisible(true); - } + } else if (mMode == MODE_WAITING_FOR_DRAG_THUMB) { + int deltaDownAngle = getDelta(mWaitingForDragThumbDownAngle, touchAngle); + if ((deltaDownAngle < -THUMB_DRAG_SLOP || deltaDownAngle > THUMB_DRAG_SLOP) && + isDeltaInBounds(mWaitingForDragThumbDownAngle, deltaDownAngle)) { + mMode = MODE_DRAG_THUMB; } } else if (mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { @@ -323,19 +559,14 @@ public class ZoomRing extends View { Math.abs(y - mPreviousDownY) > mTouchSlop) { /* Make sure the user has moved the slop amount before going into that mode. */ mMode = MODE_MOVE_ZOOM_RING; - - if (mCallback != null) { - mCallback.onZoomRingMovingStarted(); - } + mCallback.onZoomRingMovingStarted(); } } // Purposefully not an "else if" if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) { - if (isInBounds) { - onThumbDragged(touchAngle, mIsThumbAngleValid ? deltaThumbAndTouch : 0); - } else { - mIsThumbAngleValid = false; + if (isInRingBounds) { + onThumbDragged(touchAngle, false, false); } } else if (mMode == MODE_MOVE_ZOOM_RING) { onZoomRingMoved(rawX, rawY); @@ -344,55 +575,219 @@ public class ZoomRing extends View { return true; } - private int getDelta(int angle1, int angle2) { - int delta = angle1 - angle2; - - // Assume this is a result of crossing over the discontinuous 0 -> 2pi - if (delta > PI_INT_MULTIPLIED || delta < -PI_INT_MULTIPLIED) { - // Bring both the radians and previous angle onto a continuous range - if (angle1 < HALF_PI_INT_MULTIPLIED) { - // Same as deltaRadians = (radians + 2PI) - previousAngle - delta += PI_INT_MULTIPLIED * 2; - } else if (angle2 < HALF_PI_INT_MULTIPLIED) { - // Same as deltaRadians = radians - (previousAngle + 2PI) - delta -= PI_INT_MULTIPLIED * 2; + private boolean isDeltaInBounds(int startAngle, int deltaAngle) { + return getBoundIfExceeds(startAngle, deltaAngle) == Integer.MIN_VALUE; + } + + private int getBoundIfExceeds(int startAngle, int deltaAngle) { + if (deltaAngle > 0) { + // Counterclockwise movement + if (mThumbCcwBound != Integer.MIN_VALUE && + getDelta(startAngle, mThumbCcwBound, true) < deltaAngle) { + return mThumbCcwBound; + } + } else if (deltaAngle < 0) { + // Clockwise movement, both of these will be negative + int deltaThumbAndBound = getDelta(startAngle, mThumbCwBound, false); + if (mThumbCwBound != Integer.MIN_VALUE && + deltaThumbAndBound > deltaAngle) { + // Tapped outside of the bound in that direction + return mThumbCwBound; } } + + return Integer.MIN_VALUE; + } + + private int getDelta(int startAngle, int endAngle, boolean useDirection, boolean ccw) { + return useDirection ? getDelta(startAngle, endAngle, ccw) : getDelta(startAngle, endAngle); + } + + /** + * Gets the smallest delta between two angles, and infers the direction + * based on the shortest path between the two angles. If going from + * startAngle to endAngle is counterclockwise, the result will be positive. + * If it is clockwise, the result will be negative. + * + * @param startAngle The start angle. + * @param endAngle The end angle. + * @return The difference in angles. + */ + private int getDelta(int startAngle, int endAngle) { + int largerAngle, smallerAngle; + if (endAngle > startAngle) { + largerAngle = endAngle; + smallerAngle = startAngle; + } else { + largerAngle = startAngle; + smallerAngle = endAngle; + } - return delta; + int delta = largerAngle - smallerAngle; + if (delta <= PI_INT_MULTIPLIED) { + // If going clockwise, negate the delta + return startAngle == largerAngle ? -delta : delta; + } else { + // The other direction is the delta we want (it includes the + // discontinuous 0-2PI angle) + delta = TWO_PI_INT_MULTIPLIED - delta; + // If going clockwise, negate the delta + return startAngle == smallerAngle ? -delta : delta; + } } + /** + * Gets the delta between two angles in the direction specified. + * + * @param startAngle The start angle. + * @param endAngle The end angle. + * @param counterClockwise The direction to take when computing the delta. + * @return The difference in angles in the given direction. + */ + private int getDelta(int startAngle, int endAngle, boolean counterClockwise) { + int delta = endAngle - startAngle; + + if (!counterClockwise && delta > 0) { + // Crossed the discontinuous 0/2PI angle, take the leftover slice of + // the pie and negate it + return -TWO_PI_INT_MULTIPLIED + delta; + } else if (counterClockwise && delta < 0) { + // Crossed the discontinuous 0/2PI angle, take the leftover slice of + // the pie (and ensure it is positive) + return TWO_PI_INT_MULTIPLIED + delta; + } else { + return delta; + } + } + private void onThumbDragStarted(int startAngle) { + setThumbArrowsVisible(false); mThumbDragStartAngle = startAngle; - mCallback.onZoomRingThumbDraggingStarted(startAngle); + mCallback.onZoomRingThumbDraggingStarted(); } - - private void onThumbDragged(int touchAngle, int deltaAngle) { - mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER); - int totalDeltaAngle = getDelta(touchAngle, mPreviousCallbackAngle); - if (totalDeltaAngle > mCallbackThreshold - || totalDeltaAngle < -mCallbackThreshold) { - if (mCallback != null) { - boolean canStillZoom = mCallback.onZoomRingThumbDragged( - totalDeltaAngle / mCallbackThreshold, - mThumbDragStartAngle, touchAngle); - mDisabler.setEnabling(canStillZoom); - - if (canStillZoom) { - // 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); + + private void onThumbDragged(int touchAngle, boolean useDirection, boolean ccw) { + boolean animateThumbToNewAngle = false; + + int totalDeltaAngle; + totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); + int fuzzyCallbackThreshold = (int) (mCallbackThreshold * 0.65f); + if (totalDeltaAngle >= fuzzyCallbackThreshold + || totalDeltaAngle <= -fuzzyCallbackThreshold) { + + if (!useDirection) { + // Set ccw to match the direction found by getDelta + ccw = totalDeltaAngle > 0; + } + + /* + * When the user slides the thumb through the tick that corresponds + * to a zoom bound, we don't want to abruptly stop there. Instead, + * let the user slide it to the next tick, and then animate it back + * to the original zoom bound tick. Because of this, we make sure + * the delta from the bound is more than halfway to the next tick. + * We make sure the bound is between the touch and the previous + * callback to ensure we just passed the bound. + */ + int oldTouchAngle = touchAngle; + if (ccw && mThumbCcwBound != Integer.MIN_VALUE) { + int deltaCcwBoundAndTouch = + getDelta(mThumbCcwBound, touchAngle, useDirection, true); + if (deltaCcwBoundAndTouch >= mCallbackThreshold / 2) { + // The touch has past a bound + int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, + touchAngle, useDirection, true); + if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) { + // The bound is between the previous callback angle and the touch + touchAngle = mThumbCcwBound; + // We're moving the touch BACK to the bound, so opposite direction + ccw = false; + } + } + } else if (!ccw && mThumbCwBound != Integer.MIN_VALUE) { + // See block above for general comments + int deltaCwBoundAndTouch = + getDelta(mThumbCwBound, touchAngle, useDirection, false); + if (deltaCwBoundAndTouch <= -mCallbackThreshold / 2) { + int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, + touchAngle, useDirection, false); + /* + * Both of these will be negative since we got delta in + * clockwise direction, and we want the magnitude of + * deltaPreviousCbAndTouch to be greater than the magnitude + * of deltaCwBoundAndTouch + */ + if (deltaPreviousCbAndTouch <= deltaCwBoundAndTouch) { + touchAngle = mThumbCwBound; + ccw = true; + } + } + } + if (touchAngle != oldTouchAngle) { + // We bounded the touch angle + totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); + animateThumbToNewAngle = true; + mMode = MODE_IGNORE_UNTIL_UP; + } + + + // Prevent it from jumping too far + if (mEnforceMaxAbsJump) { + if (totalDeltaAngle <= -MAX_ABS_JUMP_DELTA_ANGLE) { + totalDeltaAngle = -MAX_ABS_JUMP_DELTA_ANGLE; + animateThumbToNewAngle = true; + } else if (totalDeltaAngle >= MAX_ABS_JUMP_DELTA_ANGLE) { + totalDeltaAngle = MAX_ABS_JUMP_DELTA_ANGLE; + animateThumbToNewAngle = true; } } - // Get the closest tick and lock on there - mPreviousCallbackAngle = getClosestTickAngle(touchAngle); + /* + * We need to cover the edge case of a user grabbing the thumb, + * going into the center of the widget, and then coming out from the + * center to an angle that's slightly below the angle he's trying to + * hit. If we do int division, we'll end up with one level lower + * than the one he was going for. + */ + int deltaLevels = Math.round((float) totalDeltaAngle / mCallbackThreshold); + if (deltaLevels != 0) { + boolean canStillZoom = mCallback.onZoomRingThumbDragged( + deltaLevels, mThumbDragStartAngle, touchAngle); + + // TODO: we're trying the haptics to see how it goes with + // users, so we're ignoring the settings (for now) + performHapticFeedback(HapticFeedbackConstants.ZOOM_RING_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + + // Set the callback angle to the actual angle based on how many delta levels we gave + mPreviousCallbackAngle = getValidAngle( + mPreviousCallbackAngle + (deltaLevels * mCallbackThreshold)); + } } - setThumbAngle(touchAngle); - mIsThumbAngleValid = true; + int deltaAngle = getDelta(mThumbAngle, touchAngle, useDirection, ccw); + mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER); + + if (animateThumbToNewAngle) { + if (useDirection) { + setThumbAngleAnimated(touchAngle, 0, ccw); + } else { + setThumbAngleAnimated(touchAngle, 0); + } + } else { + setThumbAngleAuto(touchAngle, useDirection, ccw); + } + } + + private int getValidAngle(int invalidAngle) { + if (invalidAngle < 0) { + return (invalidAngle % TWO_PI_INT_MULTIPLIED) + TWO_PI_INT_MULTIPLIED; + } else if (invalidAngle >= TWO_PI_INT_MULTIPLIED) { + return invalidAngle % TWO_PI_INT_MULTIPLIED; + } else { + return invalidAngle; + } } private int getClosestTickAngle(int angle) { @@ -403,12 +798,12 @@ public class ZoomRing extends View { return smallerAngle; } else { // Closer to the bigger angle (premodding) - return (smallerAngle + mCallbackThreshold) % (PI_INT_MULTIPLIED * 2); + return (smallerAngle + mCallbackThreshold) % TWO_PI_INT_MULTIPLIED; } } - private void onThumbDragStopped(int stopAngle) { - mCallback.onZoomRingThumbDraggingStopped(stopAngle); + private void onThumbDragStopped() { + mCallback.onZoomRingThumbDraggingStopped(); } private void onZoomRingMoved(int x, int y) { @@ -416,9 +811,7 @@ public class ZoomRing extends View { int deltaX = x - mPreviousWidgetDragX; int deltaY = y - mPreviousWidgetDragY; - if (mCallback != null) { - mCallback.onZoomRingMoved(deltaX, deltaY); - } + mCallback.onZoomRingMoved(deltaX, deltaY); } mPreviousWidgetDragX = x; @@ -429,11 +822,11 @@ public class ZoomRing extends View { public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus && mCallback != null) { - mCallback.onZoomRingDismissed(); + if (!hasWindowFocus) { + mCallback.onZoomRingDismissed(true); } } - + private int getAngle(int localX, int localY) { int radians = (int) (Math.atan2(localY, localX) * RADIAN_INT_MULTIPLIER); @@ -458,45 +851,65 @@ public class ZoomRing extends View { if (DRAW_TRAIL) { mTrail.draw(canvas); } + + // If we aren't near the bounds, draw the corresponding arrows + int callbackAngle = mPreviousCallbackAngle; + if (callbackAngle < mThumbCwBound - RADIAN_INT_ERROR || + callbackAngle > mThumbCwBound + RADIAN_INT_ERROR) { + mThumbPlusArrowDrawable.draw(canvas); + } + if (callbackAngle < mThumbCcwBound - RADIAN_INT_ERROR || + callbackAngle > mThumbCcwBound + RADIAN_INT_ERROR) { + mThumbMinusArrowDrawable.draw(canvas); + } mThumbDrawable.draw(canvas); } } - - private class Disabler implements Runnable { - private static final int DELAY = 15; - private static final float ENABLE_RATE = 1.05f; - private static final float DISABLE_RATE = 0.95f; - - private int mAlpha = 255; - private boolean mEnabling; - - public int getAlpha() { - return mAlpha; - } - - public void setEnabling(boolean enabling) { - if ((enabling && mAlpha != 255) || (!enabling && mAlpha != DISABLED_ALPHA)) { - mEnabling = enabling; - post(this); - } + + private void setThumbArrowsAngle(int angle) { + int level = -angle * 10000 / ZoomRing.TWO_PI_INT_MULTIPLIED; + mThumbPlusArrowDrawable.setLevel(level); + mThumbMinusArrowDrawable.setLevel(level); + } + + public void setThumbArrowsVisible(boolean visible) { + if (visible) { + mThumbArrowsAlpha = 255; + mThumbPlusArrowDrawable.setAlpha(255); + mThumbMinusArrowDrawable.setAlpha(255); + invalidate(); + } else if (mThumbArrowsAlpha == 255) { + // Only start fade if we're fully visible (otherwise another fade is happening already) + mThumbArrowsFadeStartTime = SystemClock.elapsedRealtime(); + onThumbArrowsFadeTick(); } + } + + private void onThumbArrowsFadeTick() { + if (mThumbArrowsAlpha <= 0) return; - public void run() { - mAlpha *= mEnabling ? ENABLE_RATE : DISABLE_RATE; - if (mAlpha < DISABLED_ALPHA) { - mAlpha = DISABLED_ALPHA; - } else if (mAlpha > 255) { - mAlpha = 255; - } else { - // Still more to go - postDelayed(this, DELAY); - } - - getBackground().setAlpha(mAlpha); - invalidate(); + mThumbArrowsAlpha = (int) + (255 - (255 * (SystemClock.elapsedRealtime() - mThumbArrowsFadeStartTime) + / THUMB_ARROWS_FADE_DURATION)); + if (mThumbArrowsAlpha < 0) mThumbArrowsAlpha = 0; + mThumbPlusArrowDrawable.setAlpha(mThumbArrowsAlpha); + mThumbMinusArrowDrawable.setAlpha(mThumbArrowsAlpha); + invalidateDrawable(mThumbPlusArrowDrawable); + invalidateDrawable(mThumbMinusArrowDrawable); + + if (!mHandler.hasMessages(MSG_THUMB_ARROWS_FADE_TICK)) { + mHandler.sendEmptyMessage(MSG_THUMB_ARROWS_FADE_TICK); } } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + setThumbArrowsAngle(mThumbAngle); + setThumbArrowsVisible(true); + } + public interface OnZoomRingCallback { void onZoomRingSetMovableHintVisible(boolean visible); @@ -504,11 +917,17 @@ public class ZoomRing extends View { boolean onZoomRingMoved(int deltaX, int deltaY); void onZoomRingMovingStopped(); - void onZoomRingThumbDraggingStarted(int startAngle); + void onZoomRingThumbDraggingStarted(); boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle); - void onZoomRingThumbDraggingStopped(int endAngle); + void onZoomRingThumbDraggingStopped(); - void onZoomRingDismissed(); + void onZoomRingDismissed(boolean dismissImmediately); + + void onUserInteractionStarted(); + void onUserInteractionStopped(); } + private static void printAngle(String angleName, int angle) { + Log.d(TAG, angleName + ": " + (long) angle * 180 / PI_INT_MULTIPLIED); + } } diff --git a/core/java/android/widget/ZoomRingController.java b/core/java/android/widget/ZoomRingController.java index eb28767..31074b6 100644 --- a/core/java/android/widget/ZoomRingController.java +++ b/core/java/android/widget/ZoomRingController.java @@ -16,6 +16,8 @@ package android.widget; +import android.app.AlertDialog; +import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -27,7 +29,6 @@ import android.graphics.Rect; import android.os.Handler; import android.os.Message; import android.os.SystemClock; -import android.os.Vibrator; import android.provider.Settings; import android.util.Log; import android.view.Gravity; @@ -35,6 +36,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; +import android.view.Window; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.Animation; @@ -46,12 +48,16 @@ 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 SHOW_TUTORIAL_TOAST_DELAY = 1000; + private static final int ZOOM_RING_RADIUS_INSET = 10; private static final int ZOOM_RING_RECENTERING_DURATION = 500; @@ -69,9 +75,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // TODO: scale px values based on latest from ViewConfiguration private static final int SECOND_TAP_TIMEOUT = 500; private static final int ZOOM_RING_DISMISS_DELAY = SECOND_TAP_TIMEOUT / 2; - private static final int SECOND_TAP_SLOP = 70; - private static final int SECOND_TAP_MOVE_SLOP = 15; - private static final int MAX_PAN_GAP = 30; + // TODO: view config? at least scaled + private static final int MAX_PAN_GAP = 20; + private static final int MAX_INITIATE_PAN_GAP = 10; + // TODO view config + private static final int INITIATE_PAN_DELAY = 400; private static final String SETTING_NAME_SHOWN_TOAST = "shown_zoom_ring_toast"; @@ -95,6 +103,23 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private FrameLayout mContainer; private LayoutParams mContainerLayoutParams; + /** + * The view (or null) that should receive touch events. This will get set if + * the touch down hits the container. It will be reset on the touch up. + */ + private View mTouchTargetView; + /** + * The {@link #mTouchTargetView}'s location in window, set on touch down. + */ + private int[] mTouchTargetLocationInWindow = new int[2]; + /** + * If the zoom ring is dismissed but the user is still in a touch + * interaction, we set this to true. This will ignore all touch events until + * up/cancel, and then set the owner's touch listener to null. + */ + private boolean mReleaseTouchListenerOnUp; + + /* * Tap-drag is an interaction where the user first taps and then (quickly) * does the clockwise or counter-clockwise drag. In reality, this is: (down, @@ -122,6 +147,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** Invokes panning of owner view if the zoom ring is touching an edge. */ private Panner mPanner = new Panner(); + private long mTouchingEdgeStartTime; + private boolean mPanningEnabledForThisInteraction; private ImageView mPanningArrows; private Animation mPanningArrowsEnterAnimation; @@ -162,26 +189,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * the UI thread so it will be exceuted AFTER the layout. This is the logic. */ private Runnable mPostedVisibleInitializer; - - // TODO: need a better way to persist this value, becuase right now this - // requires the WRITE_SETTINGS perimssion which the app may not have -// private Runnable mShowTutorialToast = new Runnable() { -// public void run() { -// if (Settings.System.getInt(mContext.getContentResolver(), -// SETTING_NAME_SHOWN_TOAST, 0) == 1) { -// return; -// } -// try { -// Settings.System.putInt(mContext.getContentResolver(), SETTING_NAME_SHOWN_TOAST, 1); -// } catch (SecurityException e) { -// // The app does not have permission to clear this flag, oh well! -// } -// -// Toast.makeText(mContext, -// com.android.internal.R.string.tutorial_double_tap_to_zoom_message, -// Toast.LENGTH_LONG).show(); -// } -// }; + + /** + * Only touch from the main thread. + */ + private static Dialog sTutorialDialog; + private static long sTutorialShowTime; + private static final int TUTORIAL_MIN_DISPLAY_TIME = 2000; private IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); @@ -230,38 +244,37 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mOwnerView = ownerView; mZoomRing = new ZoomRing(context); + mZoomRing.setId(com.android.internal.R.id.zoomControls); mZoomRing.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER)); mZoomRing.setCallback(this); createPanningArrows(); - mContainer = new FrameLayout(context); - mContainer.setMeasureAllChildren(true); - mContainer.setOnTouchListener(this); - - mContainer.addView(mZoomRing); - mContainer.addView(mPanningArrows); - mContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - mContainerLayoutParams = new LayoutParams(); mContainerLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - mContainerLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | + mContainerLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCHABLE | LayoutParams.FLAG_NOT_FOCUSABLE | - LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | LayoutParams.FLAG_LAYOUT_NO_LIMITS; + LayoutParams.FLAG_LAYOUT_NO_LIMITS; mContainerLayoutParams.height = LayoutParams.WRAP_CONTENT; mContainerLayoutParams.width = LayoutParams.WRAP_CONTENT; mContainerLayoutParams.type = LayoutParams.TYPE_APPLICATION_PANEL; - mContainerLayoutParams.format = PixelFormat.TRANSLUCENT; + mContainerLayoutParams.format = PixelFormat.TRANSPARENT; // TODO: make a new animation for this mContainerLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Dialog; + + mContainer = new FrameLayout(context); + mContainer.setLayoutParams(mContainerLayoutParams); + mContainer.setMeasureAllChildren(true); + + mContainer.addView(mZoomRing); + mContainer.addView(mPanningArrows); mScroller = new Scroller(context, new DecelerateInterpolator()); mViewConfig = ViewConfiguration.get(context); - -// mHandler.postDelayed(mShowTutorialToast, SHOW_TUTORIAL_TOAST_DELAY); } private void createPanningArrows() { @@ -272,7 +285,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); - mPanningArrows.setVisibility(View.GONE); + mPanningArrows.setVisibility(View.INVISIBLE); mPanningArrowsEnterAnimation = AnimationUtils.loadAnimation(mContext, com.android.internal.R.anim.fade_in); @@ -291,6 +304,17 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setZoomCallbackThreshold(float callbackThreshold) { mZoomRing.setCallbackThreshold((int) (callbackThreshold * ZoomRing.RADIAN_INT_MULTIPLIER)); } + + /** + * Sets a drawable for the zoom ring track. + * + * @param drawable The drawable to use for the track. + * @hide Need a better way of doing this, but this one-off for browser so it + * can have its final look for the usability study + */ + public void setZoomRingTrack(int drawable) { + mZoomRing.setBackgroundResource(drawable); + } public void setCallback(OnZoomListener callback) { mCallback = callback; @@ -300,10 +324,26 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mZoomRing.setThumbAngle((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER)); } + public void setThumbAngleAnimated(float angle) { + mZoomRing.setThumbAngleAnimated((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER), 0); + } + public void setResetThumbAutomatically(boolean resetThumbAutomatically) { mZoomRing.setResetThumbAutomatically(resetThumbAutomatically); } + public void setThumbClockwiseBound(float angle) { + mZoomRing.setThumbClockwiseBound(angle >= 0 ? + (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : + Integer.MIN_VALUE); + } + + public void setThumbCounterclockwiseBound(float angle) { + mZoomRing.setThumbCounterclockwiseBound(angle >= 0 ? + (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : + Integer.MIN_VALUE); + } + public boolean isVisible() { return mIsZoomRingVisible; } @@ -321,12 +361,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, if (mIsZoomRingVisible == visible) { return; } + mIsZoomRingVisible = visible; if (visible) { if (mContainerLayoutParams.token == null) { mContainerLayoutParams.token = mOwnerView.getWindowToken(); } - + mWindowManager.addView(mContainer, mContainerLayoutParams); if (mPostedVisibleInitializer == null) { @@ -340,6 +381,10 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // 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); + } } }; } @@ -349,24 +394,49 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // Handle configuration changes when visible mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); - // Steal key events from the owner + // Steal key/touches events from the owner mOwnerView.setOnKeyListener(this); + mOwnerView.setOnTouchListener(this); + mReleaseTouchListenerOnUp = false; } else { - // Don't want to steal any more keys + // Don't want to steal any more keys/touches mOwnerView.setOnKeyListener(null); + if (mTouchTargetView != null) { + // We are still stealing the touch events for this touch + // sequence, so release the touch listener later + mReleaseTouchListenerOnUp = true; + } else { + mOwnerView.setOnTouchListener(null); + } // No longer care about configuration changes mContext.unregisterReceiver(mConfigurationChangedReceiver); mWindowManager.removeView(mContainer); - } - - mIsZoomRingVisible = visible; - if (mCallback != null) { - mCallback.onVisibilityChanged(visible); + 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) { @@ -484,77 +554,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mOwnerViewBounds, mTempRect); mCenteredContainerX = mTempRect.left; mCenteredContainerY = mTempRect.top; - } - // MOVE ALL THIS TO GESTURE DETECTOR -// public boolean onTouch(View v, MotionEvent event) { -// int action = event.getAction(); -// -// if (mListenForInvocation) { -// switch (mTouchMode) { -// case TOUCH_MODE_IDLE: { -// if (action == MotionEvent.ACTION_DOWN) { -// setFirstTap(event); -// } -// break; -// } -// -// case TOUCH_MODE_WAITING_FOR_SECOND_TAP: { -// switch (action) { -// case MotionEvent.ACTION_DOWN: -// if (isSecondTapWithinSlop(event)) { -// handleDoubleTapEvent(event); -// } else { -// setFirstTap(event); -// } -// break; -// -// case MotionEvent.ACTION_MOVE: -// int deltaX = (int) event.getX() - mFirstTapX; -// if (deltaX < -SECOND_TAP_MOVE_SLOP || -// deltaX > SECOND_TAP_MOVE_SLOP) { -// mTouchMode = TOUCH_MODE_IDLE; -// } else { -// int deltaY = (int) event.getY() - mFirstTapY; -// if (deltaY < -SECOND_TAP_MOVE_SLOP || -// deltaY > SECOND_TAP_MOVE_SLOP) { -// mTouchMode = TOUCH_MODE_IDLE; -// } -// } -// break; -// } -// break; -// } -// -// case TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT: -// case TOUCH_MODE_FORWARDING_FOR_TAP_DRAG: { -// handleDoubleTapEvent(event); -// break; -// } -// } -// -// if (action == MotionEvent.ACTION_CANCEL) { -// mTouchMode = TOUCH_MODE_IDLE; -// } -// } -// -// return false; -// } -// -// private void setFirstTap(MotionEvent event) { -// mFirstTapTime = event.getEventTime(); -// mFirstTapX = (int) event.getX(); -// mFirstTapY = (int) event.getY(); -// mTouchMode = TOUCH_MODE_WAITING_FOR_SECOND_TAP; -// } -// -// private boolean isSecondTapWithinSlop(MotionEvent event) { -// return mFirstTapTime + SECOND_TAP_TIMEOUT > event.getEventTime() && -// Math.abs((int) event.getX() - mFirstTapX) < SECOND_TAP_SLOP && -// Math.abs((int) event.getY() - mFirstTapY) < SECOND_TAP_SLOP; -// } - /** * Centers the point (in owner view's coordinates). */ @@ -575,16 +576,28 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void onZoomRingSetMovableHintVisible(boolean visible) { setPanningArrowsVisible(visible); } + + public void onUserInteractionStarted() { + mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + } + + public void onUserInteractionStopped() { + dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + } public void onZoomRingMovingStarted() { - mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); mScroller.abortAnimation(); + mPanningEnabledForThisInteraction = false; + mTouchingEdgeStartTime = 0; + if (mCallback != null) { + mCallback.onBeginPan(); + } } private void setPanningArrowsVisible(boolean visible) { mPanningArrows.startAnimation(visible ? mPanningArrowsEnterAnimation : mPanningArrowsExitAnimation); - mPanningArrows.setVisibility(visible ? View.VISIBLE : View.GONE); + mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } public boolean onZoomRingMoved(int deltaX, int deltaY) { @@ -611,37 +624,73 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mWindowManager.updateViewLayout(mContainer, lp); // Check for pan + boolean horizontalPanning = true; int leftGap = newZoomRingX - ownerBounds.left; if (leftGap < MAX_PAN_GAP) { - mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); + if (shouldPan(leftGap)) { + mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); + } } else { int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft); if (rightGap < MAX_PAN_GAP) { - mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); + if (shouldPan(rightGap)) { + mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); + } } else { mPanner.setHorizontalStrength(0); + horizontalPanning = false; } } int topGap = newZoomRingY - ownerBounds.top; if (topGap < MAX_PAN_GAP) { - mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); + if (shouldPan(topGap)) { + mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); + } } else { int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop); if (bottomGap < MAX_PAN_GAP) { - mPanner.setVerticalStrength(getStrengthFromGap(bottomGap)); + if (shouldPan(bottomGap)) { + mPanner.setVerticalStrength(getStrengthFromGap(bottomGap)); + } } else { mPanner.setVerticalStrength(0); + if (!horizontalPanning) { + // Neither are panning, reset any timer to start pan mode + mTouchingEdgeStartTime = 0; + } } } return true; } + private boolean shouldPan(int gap) { + if (mPanningEnabledForThisInteraction) return true; + + if (gap < MAX_INITIATE_PAN_GAP) { + long time = SystemClock.elapsedRealtime(); + if (mTouchingEdgeStartTime != 0 && + mTouchingEdgeStartTime + INITIATE_PAN_DELAY < time) { + mPanningEnabledForThisInteraction = true; + return true; + } else if (mTouchingEdgeStartTime == 0) { + mTouchingEdgeStartTime = time; + } else { + } + } else { + // Moved away from the initiate pan gap, so reset the timer + mTouchingEdgeStartTime = 0; + } + return false; + } + public void onZoomRingMovingStopped() { - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); mPanner.stop(); - setPanningArrowsVisible(false); + setPanningArrowsVisible(false); + if (mCallback != null) { + mCallback.onEndPan(); + } } private int getStrengthFromGap(int gap) { @@ -649,10 +698,9 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, (MAX_PAN_GAP - gap) * 100 / MAX_PAN_GAP; } - public void onZoomRingThumbDraggingStarted(int startAngle) { - mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + public void onZoomRingThumbDraggingStarted() { if (mCallback != null) { - mCallback.onBeginDrag((float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER); + mCallback.onBeginDrag(); } } @@ -674,25 +722,122 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return false; } - public void onZoomRingThumbDraggingStopped(int endAngle) { - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + public void onZoomRingThumbDraggingStopped() { if (mCallback != null) { - mCallback.onEndDrag((float) endAngle / ZoomRing.RADIAN_INT_MULTIPLIER); + mCallback.onEndDrag(); } } - public void onZoomRingDismissed() { - dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + public void onZoomRingDismissed(boolean dismissImmediately) { + if (dismissImmediately) { + mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + setVisible(false); + } else { + dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + } } - + + public void onRingDown(int tickAngle, int touchAngle) { + } + public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { - // If the user touches outside of the zoom ring, dismiss the zoom ring - dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + if (sTutorialDialog != null && sTutorialDialog.isShowing() && + SystemClock.elapsedRealtime() - sTutorialShowTime >= TUTORIAL_MIN_DISPLAY_TIME) { + finishZoomTutorial(); + } + + int action = event.getAction(); + + if (mReleaseTouchListenerOnUp) { + // The ring was dismissed but we need to throw away all events until the up + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mOwnerView.setOnTouchListener(null); + mReleaseTouchListenerOnUp = false; + } + + // Eat this event return true; } - return false; + View targetView = mTouchTargetView; + + switch (action) { + case MotionEvent.ACTION_DOWN: + targetView = mTouchTargetView = + getViewForTouch((int) event.getRawX(), (int) event.getRawY()); + if (targetView != null) { + targetView.getLocationInWindow(mTouchTargetLocationInWindow); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mTouchTargetView = null; + break; + } + + if (targetView != null) { + // The upperleft corner of the target view in raw coordinates + int targetViewRawX = mContainerLayoutParams.x + mTouchTargetLocationInWindow[0]; + int targetViewRawY = mContainerLayoutParams.y + mTouchTargetLocationInWindow[1]; + + MotionEvent containerEvent = MotionEvent.obtain(event); + // Convert the motion event into the target view's coordinates (from + // owner view's coordinates) + containerEvent.offsetLocation(mOwnerViewBounds.left - targetViewRawX, + mOwnerViewBounds.top - targetViewRawY); + boolean retValue = targetView.dispatchTouchEvent(containerEvent); + containerEvent.recycle(); + return retValue; + + } else { + if (action == MotionEvent.ACTION_DOWN) { + dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + } + + return false; + } + } + + /** + * Returns the View that should receive a touch at the given coordinates. + * + * @param rawX The raw X. + * @param rawY The raw Y. + * @return The view that should receive the touches, or null if there is not one. + */ + private View getViewForTouch(int rawX, int rawY) { + // Check to see if it is touching the ring + int containerCenterX = mContainerLayoutParams.x + mContainer.getWidth() / 2; + int containerCenterY = mContainerLayoutParams.y + mContainer.getHeight() / 2; + int distanceFromCenterX = rawX - containerCenterX; + int distanceFromCenterY = rawY - containerCenterY; + int zoomRingRadius = mZoomRingWidth / 2 - ZOOM_RING_RADIUS_INSET; + if (distanceFromCenterX * distanceFromCenterX + + distanceFromCenterY * distanceFromCenterY <= + zoomRingRadius * zoomRingRadius) { + return mZoomRing; + } + + // Check to see if it is touching any other clickable View. + // Reverse order so the child drawn on top gets first dibs. + int containerCoordsX = rawX - mContainerLayoutParams.x; + int containerCoordsY = rawY - mContainerLayoutParams.y; + Rect frame = mTempRect; + for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { + View child = mContainer.getChildAt(i); + if (child == mZoomRing || child.getVisibility() != View.VISIBLE || + !child.isClickable()) { + continue; + } + + child.getHitRect(frame); + if (frame.contains(containerCoordsX, containerCoordsY)) { + return child; + } + } + + return null; } /** Steals key events from the owner view. */ @@ -707,6 +852,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, 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); @@ -734,9 +881,14 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, ensureZoomRingIsCentered(); } + /* + * This is static so Activities can call this instead of the Views + * (Activities usually do not have a reference to the ZoomRingController + * instance.) + */ /** * Shows a "tutorial" (some text) to the user teaching her the new zoom - * invocation method. + * invocation method. Must call from the main thread. * <p> * It checks the global system setting to ensure this has not been seen * before. Furthermore, if the application does not have privilege to write @@ -757,20 +909,45 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return; } + if (sTutorialDialog != null && sTutorialDialog.isShowing()) { + sTutorialDialog.dismiss(); + } + + sTutorialDialog = new AlertDialog.Builder(context) + .setMessage( + com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short) + .setIcon(0) + .create(); + + Window window = sTutorialDialog.getWindow(); + window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND | + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + + sTutorialDialog.show(); + sTutorialShowTime = SystemClock.elapsedRealtime(); + } + + public void finishZoomTutorial() { + if (sTutorialDialog == null) return; + + sTutorialDialog.dismiss(); + sTutorialDialog = null; + + // Record that they have seen the tutorial try { - Settings.System.putInt(cr, SETTING_NAME_SHOWN_TOAST, 1); + 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(); } - - Toast.makeText(context, - com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short, - Toast.LENGTH_LONG).show(); } private class Panner implements Runnable { @@ -861,12 +1038,14 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } public interface OnZoomListener { - void onBeginDrag(float startAngle); + void onBeginDrag(); boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, float curAngle); - void onEndDrag(float endAngle); + void onEndDrag(); void onSimpleZoom(boolean deltaZoomLevel); + void onBeginPan(); boolean onPan(int deltaX, int deltaY); + void onEndPan(); void onCenter(int x, int y); void onVisibilityChanged(boolean visible); } |