Refine FloatingToolbarPopup to allow multiple implementations

This change refined the FloatingToolbarPopup to allow the local and
system implementation. Use the flag to control which implementation
will be used. Use the local implementation as default, we will switch
the system version when it is done and stable.

Bug: 190030331
Bug: 205822301
Test: manual. The toolbar still work after refinement
Test: atest TextViewActivityTest
Test: atest TextViewIntegrationTest
Ignore-AOSP-First: new feature for T.

Change-Id: I191cb02fc4857855f5b63bdab21c014fa0879a0c
This commit is contained in:
Joanne Chung
2021-12-21 22:35:56 +08:00
parent 6e398eb8ab
commit f0f831e546
10 changed files with 281 additions and 82 deletions

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.SystemService;
import android.content.Context;
import android.os.RemoteException;
import android.provider.DeviceConfig;
import java.util.Objects;
@@ -40,6 +41,11 @@ public final class SelectionToolbarManager {
*/
public static final String LOG_TAG = "SelectionToolbar";
/**
* Whether system selection toolbar is enabled.
*/
private static final String REMOTE_SELECTION_TOOLBAR_ENABLED =
"remote_selection_toolbar_enabled";
@NonNull
private final Context mContext;
@@ -86,4 +92,21 @@ public final class SelectionToolbarManager {
throw e.rethrowFromSystemServer();
}
}
private boolean isRemoteSelectionToolbarEnabled() {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SELECTION_TOOLBAR,
REMOTE_SELECTION_TOOLBAR_ENABLED, false);
}
/**
* Returns {@code true} if remote render selection toolbar enabled, otherwise
* returns {@code false}.
*/
public static boolean isRemoteSelectionToolbarEnabled(Context context) {
SelectionToolbarManager manager = context.getSystemService(SelectionToolbarManager.class);
if (manager != null) {
return manager.isRemoteSelectionToolbarEnabled();
}
return false;
}
}

View File

@@ -121,7 +121,7 @@ import com.android.internal.view.menu.MenuHelper;
import com.android.internal.widget.ActionBarContextView;
import com.android.internal.widget.BackgroundFallback;
import com.android.internal.widget.DecorCaptionView;
import com.android.internal.widget.FloatingToolbar;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import java.util.List;
import java.util.function.Consumer;

View File

@@ -33,7 +33,7 @@ import android.widget.PopupWindow;
import com.android.internal.R;
import com.android.internal.view.menu.MenuBuilder;
import com.android.internal.widget.FloatingToolbar;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import java.util.Arrays;
import java.util.Objects;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.internal.widget;
package com.android.internal.widget.floatingtoolbar;
import android.annotation.Nullable;
import android.graphics.Rect;
@@ -50,14 +50,10 @@ public final class FloatingToolbar {
private final FloatingToolbarPopup mPopup;
private final Rect mContentRect = new Rect();
private final Rect mPreviousContentRect = new Rect();
private Menu mMenu;
private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
private int mSuggestedWidth;
private boolean mWidthChanged = true;
private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
private final Rect mNewRect = new Rect();
@@ -71,7 +67,7 @@ public final class FloatingToolbar {
mNewRect.set(newLeft, newRight, newTop, newBottom);
mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
mWidthChanged = true;
mPopup.setWidthChanged(true);
updateLayout();
}
}
@@ -114,7 +110,7 @@ public final class FloatingToolbar {
// TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
// supports multi-display.
mWindow = Objects.requireNonNull(window);
mPopup = new FloatingToolbarPopup(window.getContext(), window.getDecorView());
mPopup = FloatingToolbarPopup.createInstance(window.getContext(), window.getDecorView());
}
/**
@@ -159,11 +155,7 @@ public final class FloatingToolbar {
* toolbar.
*/
public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
// Check if there's been a substantial width spec change.
int difference = Math.abs(suggestedWidth - mSuggestedWidth);
mWidthChanged = difference > (mSuggestedWidth * 0.2);
mSuggestedWidth = suggestedWidth;
mPopup.setSuggestedWidth(suggestedWidth);
return this;
}
@@ -232,19 +224,7 @@ public final class FloatingToolbar {
private void doShow() {
List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
menuItems.sort(mMenuItemComparator);
if (mPopup.isLayoutRequired(menuItems) || mWidthChanged) {
mPopup.dismiss();
mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
} else {
mPopup.updateMenuItems(menuItems, mMenuItemClickListener);
}
if (!mPopup.isShowing()) {
mPopup.show(mContentRect);
} else if (!mPreviousContentRect.equals(mContentRect)) {
mPopup.updateCoordinates(mContentRect);
}
mWidthChanged = false;
mPreviousContentRect.set(mContentRect);
mPopup.show(menuItems, mMenuItemClickListener, mContentRect);
}
/**

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2021 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 com.android.internal.widget.floatingtoolbar;
import android.content.Context;
import android.graphics.Rect;
import android.view.MenuItem;
import android.view.View;
import android.view.selectiontoolbar.SelectionToolbarManager;
import android.widget.PopupWindow;
import java.util.List;
/**
* A popup window used by the {@link FloatingToolbar} to render menu items.
*
*/
public interface FloatingToolbarPopup {
/**
* Sets the suggested dp width of this floating toolbar.
* The actual width will be about this size but there are no guarantees that it will be exactly
* the suggested width.
*/
void setSuggestedWidth(int suggestedWidth);
/**
* Sets if the floating toolbar width changed.
*/
void setWidthChanged(boolean widthChanged);
/**
* Shows this popup at the specified coordinates.
* The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
*/
void show(List<MenuItem> menuItems, MenuItem.OnMenuItemClickListener menuItemClickListener,
Rect contentRect);
/**
* Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
*/
void dismiss();
/**
* Hides this popup. This is a no-op if this popup is not showing.
* Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
*/
void hide();
/**
* Returns {@code true} if this popup is currently showing. {@code false} otherwise.
*/
boolean isShowing();
/**
* Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
*/
boolean isHidden();
/**
* Makes this toolbar "outside touchable" and sets the onDismissListener.
*
* @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
*/
boolean setOutsideTouchable(boolean outsideTouchable, PopupWindow.OnDismissListener onDismiss);
/**
* Returns {@link RemoteFloatingToolbarPopup} implementation if the system selection toolbar
* enabled, otherwise returns {@link LocalFloatingToolbarPopup} implementation.
*/
static FloatingToolbarPopup createInstance(Context context, View parent) {
boolean enabled = SelectionToolbarManager.isRemoteSelectionToolbarEnabled(context);
return enabled
? new RemoteFloatingToolbarPopup(context, parent)
: new LocalFloatingToolbarPopup(context, parent);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.internal.widget;
package com.android.internal.widget.floatingtoolbar;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -70,13 +70,13 @@ import java.util.Map;
import java.util.Objects;
/**
* A popup window used by the floating toolbar.
* A popup window used by the floating toolbar to render menu items in the local app process.
*
* This class is responsible for the rendering/animation of the floating toolbar.
* It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
* to transition between panels.
*/
public final class FloatingToolbarPopup {
public final class LocalFloatingToolbarPopup implements FloatingToolbarPopup {
/* Minimum and maximum number of items allowed in the overflow. */
private static final int MIN_OVERFLOW_SIZE = 2;
@@ -94,7 +94,7 @@ public final class FloatingToolbarPopup {
private final ViewGroup mContentContainer; // holds all contents.
private final ViewGroup mMainPanel; // holds menu items that are initially displayed.
// holds menu items hidden in the overflow.
private final FloatingToolbarPopup.OverflowPanel mOverflowPanel;
private final OverflowPanel mOverflowPanel;
private final ImageButton mOverflowButton; // opens/closes the overflow.
/* overflow button drawables. */
private final Drawable mArrow;
@@ -102,8 +102,7 @@ public final class FloatingToolbarPopup {
private final AnimatedVectorDrawable mToArrow;
private final AnimatedVectorDrawable mToOverflow;
private final FloatingToolbarPopup.OverflowPanelViewHelper
mOverflowPanelViewHelper;
private final OverflowPanelViewHelper mOverflowPanelViewHelper;
/* Animation interpolators. */
private final Interpolator mLogAccelerateInterpolator;
@@ -138,7 +137,7 @@ public final class FloatingToolbarPopup {
private final int mIconTextSpacing;
/**
* @see FloatingToolbarPopup.OverflowPanelViewHelper#preparePopupContent().
* @see OverflowPanelViewHelper#preparePopupContent().
*/
private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
@Override
@@ -184,16 +183,20 @@ public final class FloatingToolbarPopup {
private int mTransitionDurationScale; // Used to scale the toolbar transition duration.
private final Rect mPreviousContentRect = new Rect();
private int mSuggestedWidth;
private boolean mWidthChanged = true;
/**
* Initializes a new floating toolbar popup.
*
* @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
* from.
*/
public FloatingToolbarPopup(Context context, View parent) {
public LocalFloatingToolbarPopup(Context context, View parent) {
mParent = Objects.requireNonNull(parent);
mContext = applyDefaultTheme(context);
mContentContainer = createContentContainer(context);
mContentContainer = createContentContainer(mContext);
mPopupWindow = createPopupWindow(mContentContainer);
mMarginHorizontal = parent.getResources()
.getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
@@ -205,7 +208,7 @@ public final class FloatingToolbarPopup {
.getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
// Interpolators
mLogAccelerateInterpolator = new FloatingToolbarPopup.LogAccelerateInterpolator();
mLogAccelerateInterpolator = new LogAccelerateInterpolator();
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
mContext, android.R.interpolator.fast_out_slow_in);
mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
@@ -231,8 +234,7 @@ public final class FloatingToolbarPopup {
mOverflowButton = createOverflowButton();
mOverflowButtonSize = measure(mOverflowButton);
mMainPanel = createMainPanel();
mOverflowPanelViewHelper =
new FloatingToolbarPopup.OverflowPanelViewHelper(mContext, mIconTextSpacing);
mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext, mIconTextSpacing);
mOverflowPanel = createOverflowPanel();
// Animation. Need views.
@@ -263,19 +265,7 @@ public final class FloatingToolbarPopup {
});
}
/**
* Makes this toolbar "outside touchable" and sets the onDismissListener.
*
* @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
*/
@Override
public boolean setOutsideTouchable(
boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
boolean ret = false;
@@ -293,7 +283,7 @@ public final class FloatingToolbarPopup {
* Lays out buttons for the specified menu items.
* Requires a subsequent call to {@link FloatingToolbar#show()} to show the items.
*/
public void layoutMenuItems(
private void layoutMenuItems(
List<MenuItem> menuItems,
MenuItem.OnMenuItemClickListener menuItemClickListener,
int suggestedWidth) {
@@ -314,7 +304,7 @@ public final class FloatingToolbarPopup {
*
* @see #isLayoutRequired(List<MenuItem>)
*/
public void updateMenuItems(
private void updateMenuItems(
List<MenuItem> menuItems, MenuItem.OnMenuItemClickListener menuItemClickListener) {
mMenuItems.clear();
for (MenuItem menuItem : menuItems) {
@@ -326,15 +316,42 @@ public final class FloatingToolbarPopup {
/**
* Returns true if this popup needs a relayout to properly render the specified menu items.
*/
public boolean isLayoutRequired(List<MenuItem> menuItems) {
private boolean isLayoutRequired(List<MenuItem> menuItems) {
return !MenuItemRepr.reprEquals(menuItems, mMenuItems.values());
}
/**
* Shows this popup at the specified coordinates.
* The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
*/
public void show(Rect contentRectOnScreen) {
@Override
public void setWidthChanged(boolean widthChanged) {
mWidthChanged = widthChanged;
}
@Override
public void setSuggestedWidth(int suggestedWidth) {
// Check if there's been a substantial width spec change.
int difference = Math.abs(suggestedWidth - mSuggestedWidth);
mWidthChanged = difference > (mSuggestedWidth * 0.2);
mSuggestedWidth = suggestedWidth;
}
@Override
public void show(List<MenuItem> menuItems,
MenuItem.OnMenuItemClickListener menuItemClickListener, Rect contentRect) {
if (isLayoutRequired(menuItems) || mWidthChanged) {
dismiss();
layoutMenuItems(menuItems, menuItemClickListener, mSuggestedWidth);
} else {
updateMenuItems(menuItems, menuItemClickListener);
}
if (!isShowing()) {
show(contentRect);
} else if (!mPreviousContentRect.equals(contentRect)) {
updateCoordinates(contentRect);
}
mWidthChanged = false;
mPreviousContentRect.set(contentRect);
}
private void show(Rect contentRectOnScreen) {
Objects.requireNonNull(contentRectOnScreen);
if (isShowing()) {
@@ -357,9 +374,7 @@ public final class FloatingToolbarPopup {
runShowAnimation();
}
/**
* Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
*/
@Override
public void dismiss() {
if (mDismissed) {
return;
@@ -373,10 +388,7 @@ public final class FloatingToolbarPopup {
setZeroTouchableSurface();
}
/**
* Hides this popup. This is a no-op if this popup is not showing.
* Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
*/
@Override
public void hide() {
if (!isShowing()) {
return;
@@ -387,16 +399,12 @@ public final class FloatingToolbarPopup {
setZeroTouchableSurface();
}
/**
* Returns {@code true} if this popup is currently showing. {@code false} otherwise.
*/
@Override
public boolean isShowing() {
return !mDismissed && !mHidden;
}
/**
* Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
*/
@Override
public boolean isHidden() {
return mHidden;
}
@@ -406,7 +414,7 @@ public final class FloatingToolbarPopup {
* The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
* This is a no-op if this popup is not showing.
*/
public void updateCoordinates(Rect contentRectOnScreen) {
private void updateCoordinates(Rect contentRectOnScreen) {
Objects.requireNonNull(contentRectOnScreen);
if (!isShowing() || !mPopupWindow.isShowing()) {
@@ -1206,9 +1214,8 @@ public final class FloatingToolbarPopup {
return overflowButton;
}
private FloatingToolbarPopup.OverflowPanel createOverflowPanel() {
final FloatingToolbarPopup.OverflowPanel
overflowPanel = new FloatingToolbarPopup.OverflowPanel(this);
private OverflowPanel createOverflowPanel() {
final OverflowPanel overflowPanel = new OverflowPanel(this);
overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
overflowPanel.setDivider(null);
@@ -1307,9 +1314,9 @@ public final class FloatingToolbarPopup {
*/
private static final class OverflowPanel extends ListView {
private final FloatingToolbarPopup mPopup;
private final LocalFloatingToolbarPopup mPopup;
OverflowPanel(FloatingToolbarPopup popup) {
OverflowPanel(LocalFloatingToolbarPopup popup) {
super(Objects.requireNonNull(popup).mContext);
this.mPopup = popup;
setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);

View File

@@ -0,0 +1 @@
include /core/java/android/view/selectiontoolbar/OWNERS

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2021 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 com.android.internal.widget.floatingtoolbar;
import android.content.Context;
import android.graphics.Rect;
import android.view.MenuItem;
import android.view.View;
import android.view.selectiontoolbar.SelectionToolbarManager;
import android.widget.PopupWindow;
import java.util.List;
import java.util.Objects;
/**
* A popup window used by the floating toolbar to render menu items in the remote system process.
*
* It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
* to transition between panels.
*/
public final class RemoteFloatingToolbarPopup implements FloatingToolbarPopup {
private final SelectionToolbarManager mSelectionToolbarManager;
// Parent for the popup window.
private final View mParent;
public RemoteFloatingToolbarPopup(Context context, View parent) {
// TODO: implement it
mParent = Objects.requireNonNull(parent);
mSelectionToolbarManager = context.getSystemService(SelectionToolbarManager.class);
}
@Override
public void show(List<MenuItem> menuItems,
MenuItem.OnMenuItemClickListener menuItemClickListener, Rect contentRect) {
// TODO: implement it
}
@Override
public void hide() {
// TODO: implement it
}
@Override
public void setSuggestedWidth(int suggestedWidth) {
// TODO: implement it
}
@Override
public void setWidthChanged(boolean widthChanged) {
// no-op
}
@Override
public void dismiss() {
// TODO: implement it
}
@Override
public boolean isHidden() {
return false;
}
@Override
public boolean isShowing() {
return false;
}
@Override
public boolean setOutsideTouchable(boolean outsideTouchable,
PopupWindow.OnDismissListener onDismiss) {
return false;
}
}

View File

@@ -28,7 +28,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withTagValue;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.android.internal.widget.FloatingToolbarPopup.MenuItemRepr;
import static com.android.internal.widget.floatingtoolbar.LocalFloatingToolbarPopup.MenuItemRepr;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
@@ -42,7 +42,7 @@ import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import com.android.internal.widget.FloatingToolbar;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

View File

@@ -50,7 +50,7 @@ import android.view.WindowInsetsController;
import android.widget.FrameLayout;
import com.android.internal.view.FloatingActionMode;
import com.android.internal.widget.FloatingToolbar;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import com.android.systemui.R;
/**