From e14579bac59aa84523c2bfbfc3f28c7842ec664d Mon Sep 17 00:00:00 2001 From: Adam Powell Date: Wed, 16 Dec 2009 18:39:52 -0800 Subject: [PATCH] Tap delays; making tap UI feel better. Addresses bug 2317577 Views now will not enter PRESSED state until ViewConfiguration.getTapTimeout() milliseconds have elapsed. This prevents scrolls and other gestures from causing views to enter PRESSED state prematurely. Quick taps within the same view still work as expected. There is now a PREPRESSED state flag within View to help track quick taps. If tapped quickly, views will remain pressed for ViewConfiguration.getPressedStateDuration(). Tap timeout has been changed to 115ms. Pressed state duration has been changed to 125ms. --- core/java/android/view/View.java | 78 ++++++++++++++----- core/java/android/view/ViewConfiguration.java | 4 +- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 4176ed115237f..5bd45a66a2d87 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -1494,6 +1494,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @hide */ static final int OPAQUE_MASK = 0x01800000; + + /** + * Indicates a prepressed state; + * the short time between ACTION_DOWN and recognizing + * a 'real' press. Prepressed is used to recognize quick taps + * even when they are shorter than ViewConfiguration.getTapTimeout(). + * + * @hide + */ + private static final int PREPRESSED = 0x02000000; /** * The parent this view is attached to. @@ -1722,6 +1732,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private int mNextFocusDownId = View.NO_ID; private CheckForLongPress mPendingCheckForLongPress; + private CheckForTap mPendingCheckForTap = null; + private UnsetPressedState mUnsetPressedState; /** @@ -1762,6 +1774,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Special tree observer used when mAttachInfo is null. */ private ViewTreeObserver mFloatingTreeObserver; + + /** + * Cache the touch slop from the context that created the view. + */ + private int mTouchSlop; // Used for debug only static long sInstanceCount = 0; @@ -1777,6 +1794,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mResources = context != null ? context.getResources() : null; mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED; ++sInstanceCount; + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } /** @@ -3951,7 +3969,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (event.getRepeatCount() == 0)) { setPressed(true); if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(); + postCheckForLongClick(0); } return true; } @@ -4174,7 +4192,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: - if ((mPrivateFlags & PRESSED) != 0) { + boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; + if ((mPrivateFlags & PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; @@ -4196,24 +4215,31 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mUnsetPressedState = new UnsetPressedState(); } - if (!post(mUnsetPressedState)) { + if (prepressed) { + mPrivateFlags |= PRESSED; + refreshDrawableState(); + postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } + removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: - mPrivateFlags |= PRESSED; - refreshDrawableState(); - if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(); + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); } + mPrivateFlags |= PREPRESSED; + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); + removeTapCallback(); break; case MotionEvent.ACTION_MOVE: @@ -4221,25 +4247,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final int y = (int) event.getY(); // Be lenient about moving outside of buttons - int slop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button + removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { - // Remove any future long press checks + // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } - } else { - // Inside button - if ((mPrivateFlags & PRESSED) == 0) { - // Need to switch from not pressed to pressed - mPrivateFlags |= PRESSED; - refreshDrawableState(); - } } break; } @@ -4257,6 +4277,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility removeCallbacks(mPendingCheckForLongPress); } } + + /** + * Remove the tap detection timer. + */ + private void removeTapCallback() { + if (mPendingCheckForTap != null) { + mPrivateFlags &= ~PREPRESSED; + removeCallbacks(mPendingCheckForTap); + } + } /** * Cancels a pending long press. Your subclass can use this if you @@ -8427,14 +8457,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } } - private void postCheckForLongClick() { + private void postCheckForLongClick(int delayOffset) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); - postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout()); + postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); } private static int[] stateSetUnion(final int[] stateSet1, @@ -8611,6 +8642,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mOriginalWindowAttachCount = mWindowAttachCount; } } + + private final class CheckForTap implements Runnable { + public void run() { + mPrivateFlags &= ~PREPRESSED; + mPrivateFlags |= PRESSED; + refreshDrawableState(); + if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { + postCheckForLongClick(ViewConfiguration.getTapTimeout()); + } + } + } /** * Interface definition for a callback to be invoked when a key event is diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 993048f5ecf27..2344c42e24648 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -49,7 +49,7 @@ public class ViewConfiguration { * Defines the duration in milliseconds of the pressed state in child * components. */ - private static final int PRESSED_STATE_DURATION = 85; + private static final int PRESSED_STATE_DURATION = 125; /** * Defines the duration in milliseconds before a press turns into @@ -69,7 +69,7 @@ public class ViewConfiguration { * is a tap or a scroll. If the user does not move within this interval, it is * considered to be a tap. */ - private static final int TAP_TIMEOUT = 100; + private static final int TAP_TIMEOUT = 115; /** * Defines the duration in milliseconds we will wait to see if a touch event