From 1487466dc2ce14cccf0ff2bd2f824238aaa0044e Mon Sep 17 00:00:00 2001 From: Adam Powell Date: Thu, 18 Jul 2013 19:42:41 -0700 Subject: Add View#cancelPendingInputEvents API This API allows an application to cancel deferred high-level input events already in flight. It forms one tool of several to help apps debounce input events and prevent things like multiple startActivity calls, FragmentTransactions, etc. from executing when only one was desired since it's otherwise not desirable for things like click events to fire synchronously. Change-Id: I60b12cd5350898065f0019d616e24d779eb8cff9 --- api/current.txt | 2 + core/java/android/app/Activity.java | 7 +++ core/java/android/app/ActivityThread.java | 7 +-- core/java/android/app/Fragment.java | 1 + core/java/android/app/FragmentManager.java | 1 + .../java/android/util/SuperNotCalledException.java | 27 +++++++++ core/java/android/view/View.java | 66 ++++++++++++++++++++++ core/java/android/view/ViewGroup.java | 11 ++++ core/java/android/widget/AbsListView.java | 17 ++++++ 9 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 core/java/android/util/SuperNotCalledException.java diff --git a/api/current.txt b/api/current.txt index e50d769b57bf..283880ec49c6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -27431,6 +27431,7 @@ package android.view { method public boolean canScrollHorizontally(int); method public boolean canScrollVertically(int); method public void cancelLongPress(); + method public final void cancelPendingInputEvents(); method public boolean checkInputConnectionProxy(android.view.View); method public void clearAnimation(); method public void clearFocus(); @@ -27658,6 +27659,7 @@ package android.view { method protected void onAnimationEnd(); method protected void onAnimationStart(); method protected void onAttachedToWindow(); + method public void onCancelPendingInputEvents(); method public boolean onCheckIsTextEditor(); method protected void onConfigurationChanged(android.content.res.Configuration); method protected void onCreateContextMenu(android.view.ContextMenu); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index e02410ac55bf..57686a455a7e 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -17,6 +17,7 @@ package android.app; import android.util.ArrayMap; +import android.util.SuperNotCalledException; import com.android.internal.app.ActionBarImpl; import com.android.internal.policy.PolicyManager; @@ -3436,6 +3437,12 @@ public class Activity extends ContextThemeWrapper // activity is finished, no matter what happens to it. mStartedActivity = true; } + + final View decor = mWindow != null ? mWindow.peekDecorView() : null; + if (decor != null) { + decor.cancelPendingInputEvents(); + } + // TODO Consider clearing/flushing other event sources and events for child windows. } else { if (options != null) { mParent.startActivityFromChild(this, intent, requestCode, options); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index e6960b36f663..018fbe01decb 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -76,6 +76,7 @@ import android.util.Log; import android.util.LogPrinter; import android.util.PrintWriterPrinter; import android.util.Slog; +import android.util.SuperNotCalledException; import android.view.Display; import android.view.HardwareRenderer; import android.view.View; @@ -116,12 +117,6 @@ import libcore.io.IoUtils; import dalvik.system.CloseGuard; -final class SuperNotCalledException extends AndroidRuntimeException { - public SuperNotCalledException(String msg) { - super(msg); - } -} - final class RemoteServiceException extends AndroidRuntimeException { public RemoteServiceException(String msg) { super(msg); diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index f8a1d82f76f2..d626e5f54aee 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -31,6 +31,7 @@ import android.util.AttributeSet; import android.util.DebugUtils; import android.util.Log; import android.util.SparseArray; +import android.util.SuperNotCalledException; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index a7789d6c0b70..4371907ad590 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/android/app/FragmentManager.java @@ -31,6 +31,7 @@ import android.util.DebugUtils; import android.util.Log; import android.util.LogWriter; import android.util.SparseArray; +import android.util.SuperNotCalledException; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; diff --git a/core/java/android/util/SuperNotCalledException.java b/core/java/android/util/SuperNotCalledException.java new file mode 100644 index 000000000000..18361420b813 --- /dev/null +++ b/core/java/android/util/SuperNotCalledException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.util; + +/** + * @hide + */ +public final class SuperNotCalledException extends AndroidRuntimeException { + public SuperNotCalledException(String msg) { + super(msg); + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index f2b3e8967aee..398ad17b5866 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -57,6 +57,7 @@ import android.util.LongSparseLongArray; import android.util.Pools.SynchronizedPool; import android.util.Property; import android.util.SparseArray; +import android.util.SuperNotCalledException; import android.util.TypedValue; import android.view.ContextMenu.ContextMenuInfo; import android.view.AccessibilityIterators.TextSegmentIterator; @@ -2204,6 +2205,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT = 0x8; + /** + * Flag indicating that an overridden method correctly called down to + * the superclass implementation as required by the API spec. + */ + static final int PFLAG3_CALLED_SUPER = 0x10; + /* End of masks for mPrivateFlags3 */ @@ -5955,6 +5962,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Invalidate too, since the default behavior for views is to be // be drawn at 50% alpha rather than to change the drawable. invalidate(true); + + if (!enabled) { + cancelPendingInputEvents(); + } } /** @@ -12363,6 +12374,61 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + /** + * Cancel any deferred high-level input events that were previously posted to the event queue. + * + *

Many views post high-level events such as click handlers to the event queue + * to run deferred in order to preserve a desired user experience - clearing visible + * pressed states before executing, etc. This method will abort any events of this nature + * that are currently in flight.

+ * + *

Custom views that generate their own high-level deferred input events should override + * {@link #onCancelPendingInputEvents()} and remove those pending events from the queue.

+ * + *

This will also cancel pending input events for any child views.

+ * + *

Note that this may not be sufficient as a debouncing strategy for clicks in all cases. + * This will not impact newer events posted after this call that may occur as a result of + * lower-level input events still waiting in the queue. If you are trying to prevent + * double-submitted events for the duration of some sort of asynchronous transaction + * you should also take other steps to protect against unexpected double inputs e.g. calling + * {@link #setEnabled(boolean) setEnabled(false)} and re-enabling the view when + * the transaction completes, tracking already submitted transaction IDs, etc.

+ */ + public final void cancelPendingInputEvents() { + dispatchCancelPendingInputEvents(); + } + + /** + * Called by {@link #cancelPendingInputEvents()} to cancel input events in flight. + * Overridden by ViewGroup to dispatch. Package scoped to prevent app-side meddling. + */ + void dispatchCancelPendingInputEvents() { + mPrivateFlags3 &= ~PFLAG3_CALLED_SUPER; + onCancelPendingInputEvents(); + if ((mPrivateFlags3 & PFLAG3_CALLED_SUPER) != PFLAG3_CALLED_SUPER) { + throw new SuperNotCalledException("View " + getClass().getSimpleName() + + " did not call through to super.onCancelPendingInputEvents()"); + } + } + + /** + * Called as the result of a call to {@link #cancelPendingInputEvents()} on this view or + * a parent view. + * + *

This method is responsible for removing any pending high-level input events that were + * posted to the event queue to run later. Custom view classes that post their own deferred + * high-level events via {@link #post(Runnable)}, {@link #postDelayed(Runnable, long)} or + * {@link android.os.Handler} should override this method, call + * super.onCancelPendingInputEvents() and remove those callbacks as appropriate. + *

+ */ + public void onCancelPendingInputEvents() { + removePerformClickCallback(); + cancelLongPress(); + mPrivateFlags3 |= PFLAG3_CALLED_SUPER; + } + /** * Store this view hierarchy's frozen state into the given container. * diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 2d75b063ca74..faeee3fa5ce7 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3182,6 +3182,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + @Override + void dispatchCancelPendingInputEvents() { + super.dispatchCancelPendingInputEvents(); + + final View[] children = mChildren; + final int count = mChildrenCount; + for (int i = 0; i < count; i++) { + children[i].dispatchCancelPendingInputEvents(); + } + } + /** * When this property is set to true, this ViewGroup supports static transformations on * children; this causes diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index c30802451099..29b7cf2095db 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2843,6 +2843,23 @@ public abstract class AbsListView extends AdapterView implements Te return new AdapterContextMenuInfo(view, position, id); } + @Override + public void onCancelPendingInputEvents() { + super.onCancelPendingInputEvents(); + if (mPerformClick != null) { + removeCallbacks(mPerformClick); + } + if (mPendingCheckForTap != null) { + removeCallbacks(mPendingCheckForTap); + } + if (mPendingCheckForLongPress != null) { + removeCallbacks(mPendingCheckForLongPress); + } + if (mPendingCheckForKeyLongPress != null) { + removeCallbacks(mPendingCheckForKeyLongPress); + } + } + /** * A base class for Runnables that will check that their view is still attached to * the original window as when the Runnable was created. -- cgit v1.2.3-59-g8ed1b