From fd3a3a1163c5096821cef351309fcdd9a4f48002 Mon Sep 17 00:00:00 2001 From: Abodunrinwa Toki Date: Tue, 5 May 2015 20:04:34 +0100 Subject: [PATCH] Hide floating toolbar when user interacts with screen. - Adds an ActionMode.snooze(int) API. - Clients call this to hide the floating toolbar on DOWN touch event. - This is called repeatedly as a snooze timeout will re-show the toolbar. - ActionMode.snooze(0) will "wake" the toolbar, reshowing it. - Clients call this to re-show the toolbar on UP touch event. - This CL also adds code to hide the toolbar when the "content rect" is changing. Bug: 20148125 Change-Id: If5a9a15f72c73cad8ca01a4328a58570b3e29f66 --- api/current.txt | 3 + api/system-current.txt | 3 + core/java/android/view/ActionMode.java | 19 +++ core/java/android/view/ViewConfiguration.java | 12 ++ core/java/android/widget/Editor.java | 56 +++++++++ .../internal/view/FloatingActionMode.java | 108 +++++++++++++++++- 6 files changed, 195 insertions(+), 6 deletions(-) diff --git a/api/current.txt b/api/current.txt index a400fa6b70d2d..ea10901e19839 100644 --- a/api/current.txt +++ b/api/current.txt @@ -34438,6 +34438,8 @@ package android.view { method public abstract void setTitle(int); method public void setTitleOptionalHint(boolean); method public void setType(int); + method public void snooze(int); + field public static final int SNOOZE_TIME_DEFAULT; field public static final int TYPE_FLOATING = 1; // 0x1 field public static final int TYPE_PRIMARY = 0; // 0x0 } @@ -36501,6 +36503,7 @@ package android.view { public class ViewConfiguration { ctor public deprecated ViewConfiguration(); method public static android.view.ViewConfiguration get(android.content.Context); + method public static int getDefaultActionModeSnoozeTime(); method public static int getDoubleTapTimeout(); method public static deprecated int getEdgeSlop(); method public static deprecated int getFadingEdgeLength(); diff --git a/api/system-current.txt b/api/system-current.txt index 351ec8e703fd2..0530c247437d7 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -36700,6 +36700,8 @@ package android.view { method public abstract void setTitle(int); method public void setTitleOptionalHint(boolean); method public void setType(int); + method public void snooze(int); + field public static final int SNOOZE_TIME_DEFAULT; field public static final int TYPE_FLOATING = 1; // 0x1 field public static final int TYPE_PRIMARY = 0; // 0x0 } @@ -38763,6 +38765,7 @@ package android.view { public class ViewConfiguration { ctor public deprecated ViewConfiguration(); method public static android.view.ViewConfiguration get(android.content.Context); + method public static int getDefaultActionModeSnoozeTime(); method public static int getDoubleTapTimeout(); method public static deprecated int getEdgeSlop(); method public static deprecated int getFadingEdgeLength(); diff --git a/core/java/android/view/ActionMode.java b/core/java/android/view/ActionMode.java index 9f202a952479e..9f00455172eaf 100644 --- a/core/java/android/view/ActionMode.java +++ b/core/java/android/view/ActionMode.java @@ -44,6 +44,12 @@ public abstract class ActionMode { */ public static final int TYPE_FLOATING = 1; + /** + * Default snooze time. + */ + public static final int SNOOZE_TIME_DEFAULT = + ViewConfiguration.getDefaultActionModeSnoozeTime(); + private Object mTag; private boolean mTitleOptionalHint; private int mType = TYPE_PRIMARY; @@ -206,6 +212,19 @@ public abstract class ActionMode { */ public void invalidateContentRect() {} + /** + * Hide the action mode view from obstructing the content below for a short period. + * This only makes sense for action modes that support dynamic positioning on the screen. + * If this method is called again before the snooze time expires, the later snooze will + * cancel the former and then take effect. + * NOTE that there is an internal limit to how long the mode can be snoozed for. It's typically + * about a few seconds. + * + * @param snoozeTime The number of milliseconds to snooze for. + * @see #SNOOZE_TIME_DEFAULT + */ + public void snooze(int snoozeTime) {} + /** * Finish and close this action mode. The action mode's {@link ActionMode.Callback} will * have its {@link Callback#onDestroyActionMode(ActionMode)} method called. diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 4e91ad4db9778..8c6fa3ff006d4 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -212,6 +212,11 @@ public class ViewConfiguration { */ private static final int OVERFLING_DISTANCE = 6; + /** + * Default time to snooze an action mode for. + */ + private static final int ACTION_MODE_SNOOZE_TIME_DEFAULT = 2000; + /** * Configuration values for overriding {@link #hasPermanentMenuKey()} behavior. * These constants must match the definition in res/values/config.xml. @@ -731,6 +736,13 @@ public class ViewConfiguration { return SCROLL_FRICTION; } + /** + * @return the default duration in milliseconds for {@link ActionMode#snooze(int)}. + */ + public static int getDefaultActionModeSnoozeTime() { + return ACTION_MODE_SNOOZE_TIME_DEFAULT; + } + /** * Report if the device has a permanent menu key available to the user. * diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index c829783e685df..d558c7bc7725c 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -233,6 +233,24 @@ public class Editor { final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier(); + private final Runnable mHideFloatingToolbar = new Runnable() { + @Override + public void run() { + if (mSelectionActionMode != null) { + mSelectionActionMode.snooze(ActionMode.SNOOZE_TIME_DEFAULT); + } + } + }; + + private final Runnable mShowFloatingToolbar = new Runnable() { + @Override + public void run() { + if (mSelectionActionMode != null) { + mSelectionActionMode.snooze(0); // snooze off. + } + } + }; + Editor(TextView textView) { mTextView = textView; // Synchronize the filter list, which places the undo input filter at the end. @@ -358,6 +376,9 @@ public class Editor { mTextView.removeCallbacks(mSelectionModeWithoutSelectionRunnable); } + mTextView.removeCallbacks(mHideFloatingToolbar); + mTextView.removeCallbacks(mShowFloatingToolbar); + destroyDisplayListsData(); if (mSpellChecker != null) { @@ -1169,6 +1190,8 @@ public class Editor { } void onTouchEvent(MotionEvent event) { + updateFloatingToolbarVisibility(event); + if (hasSelectionController()) { getSelectionController().onTouchEvent(event); } @@ -1189,6 +1212,37 @@ public class Editor { } } + private void updateFloatingToolbarVisibility(MotionEvent event) { + if (mSelectionActionMode != null) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_MOVE: + hideFloatingToolbar(); + break; + case MotionEvent.ACTION_UP: // fall through + case MotionEvent.ACTION_CANCEL: + showFloatingToolbar(); + } + } + } + + private void hideFloatingToolbar() { + if (mSelectionActionMode != null) { + mTextView.removeCallbacks(mShowFloatingToolbar); + // Delay the "hide" a little bit just in case a "show" will happen almost immediately. + mTextView.postDelayed(mHideFloatingToolbar, 100); + } + } + + private void showFloatingToolbar() { + if (mSelectionActionMode != null) { + mTextView.removeCallbacks(mHideFloatingToolbar); + // Delay "show" so it doesn't interfere with click confirmations + // or double-clicks that could "dismiss" the floating toolbar. + int delay = ViewConfiguration.getDoubleTapTimeout(); + mTextView.postDelayed(mShowFloatingToolbar, delay); + } + } + public void beginBatchEdit() { mInBatchEditControllers = true; final InputMethodState ims = mInputMethodState; @@ -3661,6 +3715,8 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent ev) { + updateFloatingToolbarVisibility(ev); + switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { startTouchUpFilter(getCurrentCursorOffset()); diff --git a/core/java/com/android/internal/view/FloatingActionMode.java b/core/java/com/android/internal/view/FloatingActionMode.java index c3f4da7cce5c7..8402cc0d9d06d 100644 --- a/core/java/com/android/internal/view/FloatingActionMode.java +++ b/core/java/com/android/internal/view/FloatingActionMode.java @@ -30,6 +30,9 @@ import com.android.internal.widget.FloatingToolbar; public class FloatingActionMode extends ActionMode { + private static final int MAX_SNOOZE_TIME = 3000; + private static final int MOVING_HIDE_DELAY = 300; + private final Context mContext; private final ActionMode.Callback2 mCallback; private final MenuBuilder mMenu; @@ -38,12 +41,26 @@ public class FloatingActionMode extends ActionMode { private final Rect mPreviousContentRectOnWindow; private final int[] mViewPosition; private final View mOriginatingView; + + private final Runnable mMovingOff = new Runnable() { + public void run() { + mFloatingToolbarVisibilityHelper.setMoving(false); + } + }; + + private final Runnable mSnoozeOff = new Runnable() { + public void run() { + mFloatingToolbarVisibilityHelper.setSnoozed(false); + } + }; + private FloatingToolbar mFloatingToolbar; + private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper; public FloatingActionMode( Context context, ActionMode.Callback2 callback, View originatingView) { - mContext = context; - mCallback = callback; + mContext = Preconditions.checkNotNull(context); + mCallback = Preconditions.checkNotNull(callback); mMenu = new MenuBuilder(context).setDefaultShowAsAction( MenuItem.SHOW_AS_ACTION_IF_ROOM); setType(ActionMode.TYPE_FLOATING); @@ -51,7 +68,8 @@ public class FloatingActionMode extends ActionMode { mContentRectOnWindow = new Rect(); mPreviousContentRectOnWindow = new Rect(); mViewPosition = new int[2]; - mOriginatingView = originatingView; + mOriginatingView = Preconditions.checkNotNull(originatingView); + mOriginatingView.getLocationInWindow(mViewPosition); } public void setFloatingToolbar(FloatingToolbar floatingToolbar) { @@ -63,6 +81,7 @@ public class FloatingActionMode extends ActionMode { return mCallback.onActionItemClicked(FloatingActionMode.this, item); } }); + mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar); } @Override @@ -82,7 +101,7 @@ public class FloatingActionMode extends ActionMode { @Override public void invalidate() { - Preconditions.checkNotNull(mFloatingToolbar); + checkToolbarInitialized(); mCallback.onPrepareActionMode(this, mMenu); mFloatingToolbar.updateLayout(); invalidateContentRect(); @@ -90,32 +109,57 @@ public class FloatingActionMode extends ActionMode { @Override public void invalidateContentRect() { - Preconditions.checkNotNull(mFloatingToolbar); + checkToolbarInitialized(); mCallback.onGetContentRect(this, mOriginatingView, mContentRect); repositionToolbar(); } public void updateViewLocationInWindow() { - Preconditions.checkNotNull(mFloatingToolbar); + checkToolbarInitialized(); mOriginatingView.getLocationInWindow(mViewPosition); repositionToolbar(); } private void repositionToolbar() { + checkToolbarInitialized(); mContentRectOnWindow.set( mContentRect.left + mViewPosition[0], mContentRect.top + mViewPosition[1], mContentRect.right + mViewPosition[0], mContentRect.bottom + mViewPosition[1]); if (!mContentRectOnWindow.equals(mPreviousContentRectOnWindow)) { + if (!mPreviousContentRectOnWindow.isEmpty()) { + notifyContentRectMoving(); + } mFloatingToolbar.setContentRect(mContentRectOnWindow); mFloatingToolbar.updateLayout(); } mPreviousContentRectOnWindow.set(mContentRectOnWindow); } + private void notifyContentRectMoving() { + mOriginatingView.removeCallbacks(mMovingOff); + mFloatingToolbarVisibilityHelper.setMoving(true); + mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY); + } + + @Override + public void snooze(int snoozeTime) { + checkToolbarInitialized(); + snoozeTime = Math.min(MAX_SNOOZE_TIME, snoozeTime); + mOriginatingView.removeCallbacks(mSnoozeOff); + if (snoozeTime <= 0) { + mSnoozeOff.run(); + } else { + mFloatingToolbarVisibilityHelper.setSnoozed(true); + mOriginatingView.postDelayed(mSnoozeOff, snoozeTime); + } + } + @Override public void finish() { + checkToolbarInitialized(); + reset(); mCallback.onDestroyActionMode(this); } @@ -144,4 +188,56 @@ public class FloatingActionMode extends ActionMode { return new MenuInflater(mContext); } + /** + * @throws IlllegalStateException + */ + private void checkToolbarInitialized() { + Preconditions.checkState(mFloatingToolbar != null); + Preconditions.checkState(mFloatingToolbarVisibilityHelper != null); + } + + private void reset() { + mOriginatingView.removeCallbacks(mMovingOff); + mOriginatingView.removeCallbacks(mSnoozeOff); + } + + + /** + * A helper that shows/hides the floating toolbar depending on certain states. + */ + private static final class FloatingToolbarVisibilityHelper { + + private final FloatingToolbar mToolbar; + + private boolean mSnoozed; + private boolean mMoving; + private boolean mOutOfBounds; + + public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) { + mToolbar = Preconditions.checkNotNull(toolbar); + } + + public void setSnoozed(boolean snoozed) { + mSnoozed = snoozed; + updateToolbarVisibility(); + } + + public void setMoving(boolean moving) { + mMoving = moving; + updateToolbarVisibility(); + } + + public void setOutOfBounds(boolean outOfBounds) { + mOutOfBounds = outOfBounds; + updateToolbarVisibility(); + } + + private void updateToolbarVisibility() { + if (mSnoozed || mMoving || mOutOfBounds) { + mToolbar.hide(); + } else if (mToolbar.isHidden()) { + mToolbar.show(); + } + } + } }