summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/widget')
-rw-r--r--core/java/android/widget/AbsListView.java73
-rw-r--r--core/java/android/widget/Chronometer.java48
-rw-r--r--core/java/android/widget/FrameLayout.java2
-rw-r--r--core/java/android/widget/ImageView.java7
-rw-r--r--core/java/android/widget/LinearLayout.java6
-rw-r--r--core/java/android/widget/ListView.java4
-rw-r--r--core/java/android/widget/PopupWindow.java91
-rw-r--r--core/java/android/widget/ProgressBar.java4
-rw-r--r--core/java/android/widget/RelativeLayout.java4
-rw-r--r--core/java/android/widget/RemoteViews.java664
-rw-r--r--core/java/android/widget/TabHost.java6
-rw-r--r--core/java/android/widget/TextView.java65
-rw-r--r--core/java/android/widget/ViewFlipper.java2
-rw-r--r--core/java/android/widget/ZoomRing.java687
-rw-r--r--core/java/android/widget/ZoomRingController.java471
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);
}