Loading core/java/android/widget/Editor.java +10 −0 Original line number Original line Diff line number Diff line Loading @@ -123,6 +123,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions; import com.android.internal.view.FloatingActionMode; import com.android.internal.widget.EditableInputConnection; import com.android.internal.widget.EditableInputConnection; import java.lang.annotation.Retention; import java.lang.annotation.Retention; Loading Loading @@ -2215,6 +2216,15 @@ public class Editor { ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode); ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode); mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); final boolean selectableText = mTextView.isTextEditable() || mTextView.isTextSelectable(); if (actionMode == TextActionMode.TEXT_LINK && !selectableText && mTextActionMode instanceof FloatingActionMode) { // Make the toolbar outside-touchable so that it can be dismissed when the user clicks // outside of it. ((FloatingActionMode) mTextActionMode).setOutsideTouchable(true, () -> stopTextActionMode()); } final boolean selectionStarted = mTextActionMode != null; final boolean selectionStarted = mTextActionMode != null; if (selectionStarted if (selectionStarted && mTextView.isTextEditable() && !mTextView.isTextSelectable() && mTextView.isTextEditable() && !mTextView.isTextSelectable() Loading core/java/com/android/internal/view/FloatingActionMode.java +19 −0 Original line number Original line Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.internal.view; package com.android.internal.view; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Context; import android.graphics.Point; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; Loading @@ -30,6 +31,7 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewParent; import android.view.WindowManager; import android.view.WindowManager; import android.widget.PopupWindow; import com.android.internal.R; import com.android.internal.R; import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuBuilder; Loading Loading @@ -241,6 +243,23 @@ public final class FloatingActionMode extends ActionMode { } } } } /** * If this is set to true, the action mode view will dismiss itself on touch events outside of * its window. This only makes sense if the action mode view is a PopupWindow that is touchable * but not focusable, which means touches outside of the window will be delivered to the window * behind. The default is false. * * This is for internal use only and the approach to this may change. * @hide * * @param outsideTouchable whether or not this action mode is "outside touchable" * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself */ public void setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss); } @Override @Override public void onWindowFocusChanged(boolean hasWindowFocus) { public void onWindowFocusChanged(boolean hasWindowFocus) { mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); Loading core/java/com/android/internal/widget/FloatingToolbar.java +43 −0 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.animation.ValueAnimator; import android.annotation.Nullable; import android.content.Context; import android.content.Context; import android.content.res.TypedArray; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Color; Loading Loading @@ -259,6 +260,22 @@ public final class FloatingToolbar { return mPopup.isHidden(); return mPopup.isHidden(); } } /** * If this is set to true, the action mode view will dismiss itself on touch events outside of * its window. If the toolbar is already showing, it will be re-shown so that this setting takes * effect immediately. * * @param outsideTouchable whether or not this action mode is "outside touchable" * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself */ public void setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { if (mPopup.setOutsideTouchable(outsideTouchable, onDismiss) && isShowing()) { dismiss(); doShow(); } } private void doShow() { private void doShow() { List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); menuItems.sort(mMenuItemComparator); menuItems.sort(mMenuItemComparator); Loading Loading @@ -512,6 +529,32 @@ public final class FloatingToolbar { }); }); } } /** * Makes this toolbar "outside touchable" and sets the onDismissListener. * This will take effect the next time the toolbar is re-shown. * * @param outsideTouchable if true, the popup will be made "outside touchable" and * "non focusable". The reverse will happen if false. * @param onDismiss * * @return true if the "outsideTouchable" setting was modified. Otherwise returns false * * @see PopupWindow#setOutsideTouchable(boolean) * @see PopupWindow#setFocusable(boolean) * @see PopupWindow.OnDismissListener */ public boolean setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { boolean ret = false; if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) { mPopupWindow.setOutsideTouchable(outsideTouchable); mPopupWindow.setFocusable(!outsideTouchable); ret = true; } mPopupWindow.setOnDismissListener(onDismiss); return ret; } /** /** * Lays out buttons for the specified menu items. * Lays out buttons for the specified menu items. * Requires a subsequent call to {@link #show()} to show the items. * Requires a subsequent call to {@link #show()} to show the items. Loading core/tests/coretests/res/layout/activity_text_view.xml +3 −1 Original line number Original line Diff line number Diff line Loading @@ -31,6 +31,8 @@ <TextView <TextView android:id="@+id/nonselectable_textview" android:id="@+id/nonselectable_textview" android:layout_width="match_parent" android:layout_width="match_parent" android:layout_height="wrap_content" /> android:layout_height="wrap_content" android:focusable="false" android:focusableInTouchMode="false" /> </LinearLayout> </LinearLayout> core/tests/coretests/src/android/widget/TextViewActivityTest.java +29 −17 Original line number Original line Diff line number Diff line Loading @@ -318,40 +318,52 @@ public class TextViewActivityTest { onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position)); onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarIsDisplayed(); } } @Test @Test public void testToolbarAppearsAfterLinkClickedNonselectable() throws Throwable { public void testToolbarAppearsAfterLinkClickedNonselectable() throws Throwable { TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); final TextView textView = mActivity.findViewById(R.id.nonselectable_textview); int position = (textLink.getStart() + textLink.getEnd()) / 2; final TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); final int position = (textLink.getStart() + textLink.getEnd()) / 2; onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertTrue(textView.hasSelection()); // toggle onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); assertFalse(textView.hasSelection()); onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarIsDisplayed(); assertTrue(textView.hasSelection()); // click outside onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(0)); sleepForFloatingToolbarPopup(); assertFalse(textView.hasSelection()); } } @Test @Test public void testSelectionRemovedWhenNonselectableTextLosesFocus() throws Throwable { public void testSelectionRemovedWhenNonselectableTextLosesFocus() throws Throwable { // Add a link to both selectable and nonselectable TextViews: final TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.textview); final int position = (textLink.getStart() + textLink.getEnd()) / 2; int selectablePosition = (textLink.getStart() + textLink.getEnd()) / 2; final TextView textView = mActivity.findViewById(R.id.nonselectable_textview); textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); mActivityRule.runOnUiThread(() -> textView.setFocusableInTouchMode(true)); int nonselectablePosition = (textLink.getStart() + textLink.getEnd()) / 2; TextView selectableTextView = mActivity.findViewById(R.id.textview); TextView nonselectableTextView = mActivity.findViewById(R.id.nonselectable_textview); onView(withId(R.id.nonselectable_textview)) onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); .perform(clickOnTextAtIndex(nonselectablePosition)); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarIsDisplayed(); assertTrue(nonselectableTextView.hasSelection()); assertTrue(textView.hasSelection()); onView(withId(R.id.textview)).perform(clickOnTextAtIndex(selectablePosition)); mActivityRule.runOnUiThread(() -> textView.clearFocus()); mInstrumentation.waitForIdleSync(); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertTrue(selectableTextView.hasSelection()); assertFalse(textView.hasSelection()); assertFalse(nonselectableTextView.hasSelection()); } } @Test @Test Loading Loading
core/java/android/widget/Editor.java +10 −0 Original line number Original line Diff line number Diff line Loading @@ -123,6 +123,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions; import com.android.internal.view.FloatingActionMode; import com.android.internal.widget.EditableInputConnection; import com.android.internal.widget.EditableInputConnection; import java.lang.annotation.Retention; import java.lang.annotation.Retention; Loading Loading @@ -2215,6 +2216,15 @@ public class Editor { ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode); ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode); mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); final boolean selectableText = mTextView.isTextEditable() || mTextView.isTextSelectable(); if (actionMode == TextActionMode.TEXT_LINK && !selectableText && mTextActionMode instanceof FloatingActionMode) { // Make the toolbar outside-touchable so that it can be dismissed when the user clicks // outside of it. ((FloatingActionMode) mTextActionMode).setOutsideTouchable(true, () -> stopTextActionMode()); } final boolean selectionStarted = mTextActionMode != null; final boolean selectionStarted = mTextActionMode != null; if (selectionStarted if (selectionStarted && mTextView.isTextEditable() && !mTextView.isTextSelectable() && mTextView.isTextEditable() && !mTextView.isTextSelectable() Loading
core/java/com/android/internal/view/FloatingActionMode.java +19 −0 Original line number Original line Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.internal.view; package com.android.internal.view; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Context; import android.graphics.Point; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; Loading @@ -30,6 +31,7 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewParent; import android.view.WindowManager; import android.view.WindowManager; import android.widget.PopupWindow; import com.android.internal.R; import com.android.internal.R; import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuBuilder; Loading Loading @@ -241,6 +243,23 @@ public final class FloatingActionMode extends ActionMode { } } } } /** * If this is set to true, the action mode view will dismiss itself on touch events outside of * its window. This only makes sense if the action mode view is a PopupWindow that is touchable * but not focusable, which means touches outside of the window will be delivered to the window * behind. The default is false. * * This is for internal use only and the approach to this may change. * @hide * * @param outsideTouchable whether or not this action mode is "outside touchable" * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself */ public void setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss); } @Override @Override public void onWindowFocusChanged(boolean hasWindowFocus) { public void onWindowFocusChanged(boolean hasWindowFocus) { mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); Loading
core/java/com/android/internal/widget/FloatingToolbar.java +43 −0 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.animation.ValueAnimator; import android.annotation.Nullable; import android.content.Context; import android.content.Context; import android.content.res.TypedArray; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Color; Loading Loading @@ -259,6 +260,22 @@ public final class FloatingToolbar { return mPopup.isHidden(); return mPopup.isHidden(); } } /** * If this is set to true, the action mode view will dismiss itself on touch events outside of * its window. If the toolbar is already showing, it will be re-shown so that this setting takes * effect immediately. * * @param outsideTouchable whether or not this action mode is "outside touchable" * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself */ public void setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { if (mPopup.setOutsideTouchable(outsideTouchable, onDismiss) && isShowing()) { dismiss(); doShow(); } } private void doShow() { private void doShow() { List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); menuItems.sort(mMenuItemComparator); menuItems.sort(mMenuItemComparator); Loading Loading @@ -512,6 +529,32 @@ public final class FloatingToolbar { }); }); } } /** * Makes this toolbar "outside touchable" and sets the onDismissListener. * This will take effect the next time the toolbar is re-shown. * * @param outsideTouchable if true, the popup will be made "outside touchable" and * "non focusable". The reverse will happen if false. * @param onDismiss * * @return true if the "outsideTouchable" setting was modified. Otherwise returns false * * @see PopupWindow#setOutsideTouchable(boolean) * @see PopupWindow#setFocusable(boolean) * @see PopupWindow.OnDismissListener */ public boolean setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { boolean ret = false; if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) { mPopupWindow.setOutsideTouchable(outsideTouchable); mPopupWindow.setFocusable(!outsideTouchable); ret = true; } mPopupWindow.setOnDismissListener(onDismiss); return ret; } /** /** * Lays out buttons for the specified menu items. * Lays out buttons for the specified menu items. * Requires a subsequent call to {@link #show()} to show the items. * Requires a subsequent call to {@link #show()} to show the items. Loading
core/tests/coretests/res/layout/activity_text_view.xml +3 −1 Original line number Original line Diff line number Diff line Loading @@ -31,6 +31,8 @@ <TextView <TextView android:id="@+id/nonselectable_textview" android:id="@+id/nonselectable_textview" android:layout_width="match_parent" android:layout_width="match_parent" android:layout_height="wrap_content" /> android:layout_height="wrap_content" android:focusable="false" android:focusableInTouchMode="false" /> </LinearLayout> </LinearLayout>
core/tests/coretests/src/android/widget/TextViewActivityTest.java +29 −17 Original line number Original line Diff line number Diff line Loading @@ -318,40 +318,52 @@ public class TextViewActivityTest { onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position)); onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarIsDisplayed(); } } @Test @Test public void testToolbarAppearsAfterLinkClickedNonselectable() throws Throwable { public void testToolbarAppearsAfterLinkClickedNonselectable() throws Throwable { TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); final TextView textView = mActivity.findViewById(R.id.nonselectable_textview); int position = (textLink.getStart() + textLink.getEnd()) / 2; final TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); final int position = (textLink.getStart() + textLink.getEnd()) / 2; onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertTrue(textView.hasSelection()); // toggle onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); assertFalse(textView.hasSelection()); onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarIsDisplayed(); assertTrue(textView.hasSelection()); // click outside onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(0)); sleepForFloatingToolbarPopup(); assertFalse(textView.hasSelection()); } } @Test @Test public void testSelectionRemovedWhenNonselectableTextLosesFocus() throws Throwable { public void testSelectionRemovedWhenNonselectableTextLosesFocus() throws Throwable { // Add a link to both selectable and nonselectable TextViews: final TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.textview); final int position = (textLink.getStart() + textLink.getEnd()) / 2; int selectablePosition = (textLink.getStart() + textLink.getEnd()) / 2; final TextView textView = mActivity.findViewById(R.id.nonselectable_textview); textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview); mActivityRule.runOnUiThread(() -> textView.setFocusableInTouchMode(true)); int nonselectablePosition = (textLink.getStart() + textLink.getEnd()) / 2; TextView selectableTextView = mActivity.findViewById(R.id.textview); TextView nonselectableTextView = mActivity.findViewById(R.id.nonselectable_textview); onView(withId(R.id.nonselectable_textview)) onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position)); .perform(clickOnTextAtIndex(nonselectablePosition)); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarIsDisplayed(); assertTrue(nonselectableTextView.hasSelection()); assertTrue(textView.hasSelection()); onView(withId(R.id.textview)).perform(clickOnTextAtIndex(selectablePosition)); mActivityRule.runOnUiThread(() -> textView.clearFocus()); mInstrumentation.waitForIdleSync(); sleepForFloatingToolbarPopup(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertTrue(selectableTextView.hasSelection()); assertFalse(textView.hasSelection()); assertFalse(nonselectableTextView.hasSelection()); } } @Test @Test Loading