summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java1619
1 files changed, 1619 insertions, 0 deletions
diff --git a/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
new file mode 100644
index 000000000000..95ecc4e16446
--- /dev/null
+++ b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
@@ -0,0 +1,1619 @@
+/*
+ * Copyright (C) 2022 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.service.selectiontoolbar;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Size;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.internal.widget.floatingtoolbar.FloatingToolbarPopup;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 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.
+ */
+
+final class RemoteSelectionToolbar implements FloatingToolbarPopup {
+
+ /* Minimum and maximum number of items allowed in the overflow. */
+ private static final int MIN_OVERFLOW_SIZE = 2;
+ private static final int MAX_OVERFLOW_SIZE = 4;
+
+ private final Context mContext;
+ private final View mParent; // Parent for the popup window.
+ private final PopupWindow mPopupWindow;
+
+ /* Margins between the popup window and its content. */
+ private final int mMarginHorizontal;
+ private final int mMarginVertical;
+
+ /* View components */
+ 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 OverflowPanel mOverflowPanel;
+ private final ImageButton mOverflowButton; // opens/closes the overflow.
+ /* overflow button drawables. */
+ private final Drawable mArrow;
+ private final Drawable mOverflow;
+ private final AnimatedVectorDrawable mToArrow;
+ private final AnimatedVectorDrawable mToOverflow;
+
+ private final OverflowPanelViewHelper mOverflowPanelViewHelper;
+
+ /* Animation interpolators. */
+ private final Interpolator mLogAccelerateInterpolator;
+ private final Interpolator mFastOutSlowInInterpolator;
+ private final Interpolator mLinearOutSlowInInterpolator;
+ private final Interpolator mFastOutLinearInInterpolator;
+
+ /* Animations. */
+ private final AnimatorSet mShowAnimation;
+ private final AnimatorSet mDismissAnimation;
+ private final AnimatorSet mHideAnimation;
+ private final AnimationSet mOpenOverflowAnimation;
+ private final AnimationSet mCloseOverflowAnimation;
+ private final Animation.AnimationListener mOverflowAnimationListener;
+
+ private final Rect mViewPortOnScreen = new Rect(); // portion of screen we can draw in.
+ private final Point mCoordsOnWindow = new Point(); // popup window coordinates.
+ /* Temporary data holders. Reset values before using. */
+ private final int[] mTmpCoords = new int[2];
+
+ private final Region mTouchableRegion = new Region();
+ private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
+ info -> {
+ info.contentInsets.setEmpty();
+ info.visibleInsets.setEmpty();
+ info.touchableRegion.set(mTouchableRegion);
+ info.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ };
+
+ private final int mLineHeight;
+ private final int mIconTextSpacing;
+
+ /**
+ * @see OverflowPanelViewHelper#preparePopupContent().
+ */
+ private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
+ @Override
+ public void run() {
+ setPanelsStatesAtRestingPosition();
+ setContentAreaAsTouchableSurface();
+ mContentContainer.setAlpha(1);
+ }
+ };
+
+ private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
+ private boolean mHidden; // tracks whether this popup is hidden or hiding.
+
+ /* Calculated sizes for panels and overflow button. */
+ private final Size mOverflowButtonSize;
+ private Size mOverflowPanelSize; // Should be null when there is no overflow.
+ private Size mMainPanelSize;
+
+ /* Menu items and click listeners */
+ private final Map<MenuItemRepr, MenuItem> mMenuItems = new LinkedHashMap<>();
+ private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
+ private final View.OnClickListener mMenuItemButtonOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnMenuItemClickListener == null) {
+ return;
+ }
+ final Object tag = v.getTag();
+ if (!(tag instanceof MenuItemRepr)) {
+ return;
+ }
+ final MenuItem menuItem = mMenuItems.get((MenuItemRepr) tag);
+ if (menuItem == null) {
+ return;
+ }
+ mOnMenuItemClickListener.onMenuItemClick(menuItem);
+ }
+ };
+
+ private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards.
+ private boolean mIsOverflowOpen;
+
+ 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.
+ */
+ RemoteSelectionToolbar(Context context, View parent) {
+ mParent = Objects.requireNonNull(parent);
+ mContext = applyDefaultTheme(context);
+ mContentContainer = createContentContainer(mContext);
+ mPopupWindow = createPopupWindow(mContentContainer);
+ mMarginHorizontal = parent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
+ mMarginVertical = parent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
+ mLineHeight = context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_height);
+ mIconTextSpacing = context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
+
+ // Interpolators
+ mLogAccelerateInterpolator = new LogAccelerateInterpolator();
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_slow_in);
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.linear_out_slow_in);
+ mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_linear_in);
+
+ // Drawables. Needed for views.
+ mArrow = mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
+ mArrow.setAutoMirrored(true);
+ mOverflow = mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
+ mOverflow.setAutoMirrored(true);
+ mToArrow = (AnimatedVectorDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
+ mToArrow.setAutoMirrored(true);
+ mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
+ mToOverflow.setAutoMirrored(true);
+
+ // Views
+ mOverflowButton = createOverflowButton();
+ mOverflowButtonSize = measure(mOverflowButton);
+ mMainPanel = createMainPanel();
+ mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext, mIconTextSpacing);
+ mOverflowPanel = createOverflowPanel();
+
+ // Animation. Need views.
+ mOverflowAnimationListener = createOverflowAnimationListener();
+ mOpenOverflowAnimation = new AnimationSet(true);
+ mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
+ mCloseOverflowAnimation = new AnimationSet(true);
+ mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
+ mShowAnimation = createEnterAnimation(mContentContainer);
+ mDismissAnimation = createExitAnimation(
+ mContentContainer,
+ 150, // startDelay
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPopupWindow.dismiss();
+ mContentContainer.removeAllViews();
+ }
+ });
+ mHideAnimation = createExitAnimation(
+ mContentContainer,
+ 0, // startDelay
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPopupWindow.dismiss();
+ }
+ });
+ }
+
+ @Override
+ public boolean setOutsideTouchable(
+ boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
+ boolean ret = false;
+ if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) {
+ mPopupWindow.setOutsideTouchable(outsideTouchable);
+ mPopupWindow.setFocusable(!outsideTouchable);
+ mPopupWindow.update();
+ ret = true;
+ }
+ mPopupWindow.setOnDismissListener(onDismiss);
+ return ret;
+ }
+
+ /**
+ * Lays out buttons for the specified menu items.
+ * Requires a subsequent call to {@link FloatingToolbar#show()} to show the items.
+ */
+ private void layoutMenuItems(
+ List<MenuItem> menuItems,
+ MenuItem.OnMenuItemClickListener menuItemClickListener,
+ int suggestedWidth) {
+ cancelOverflowAnimations();
+ clearPanels();
+ updateMenuItems(menuItems, menuItemClickListener);
+ menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
+ if (!menuItems.isEmpty()) {
+ // Add remaining items to the overflow.
+ layoutOverflowPanelItems(menuItems);
+ }
+ updatePopupSize();
+ }
+
+ /**
+ * Updates the popup's menu items without rebuilding the widget.
+ * Use in place of layoutMenuItems() when the popup's views need not be reconstructed.
+ *
+ * @see #isLayoutRequired(List<MenuItem>)
+ */
+ private void updateMenuItems(
+ List<MenuItem> menuItems, MenuItem.OnMenuItemClickListener menuItemClickListener) {
+ mMenuItems.clear();
+ for (MenuItem menuItem : menuItems) {
+ mMenuItems.put(MenuItemRepr.of(menuItem), menuItem);
+ }
+ mOnMenuItemClickListener = menuItemClickListener;
+ }
+
+ /**
+ * Returns true if this popup needs a relayout to properly render the specified menu items.
+ */
+ private boolean isLayoutRequired(List<MenuItem> menuItems) {
+ return !MenuItemRepr.reprEquals(menuItems, mMenuItems.values());
+ }
+
+ @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()) {
+ return;
+ }
+
+ mHidden = false;
+ mDismissed = false;
+ cancelDismissAndHideAnimations();
+ cancelOverflowAnimations();
+
+ refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
+ preparePopupContent();
+ // We need to specify the position in window coordinates.
+ // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
+ // specify the popup position in screen coordinates.
+ mPopupWindow.showAtLocation(
+ mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
+ setTouchableSurfaceInsetsComputer();
+ runShowAnimation();
+ }
+
+ @Override
+ public void dismiss() {
+ if (mDismissed) {
+ return;
+ }
+
+ mHidden = false;
+ mDismissed = true;
+ mHideAnimation.cancel();
+
+ runDismissAnimation();
+ setZeroTouchableSurface();
+ }
+
+ @Override
+ public void hide() {
+ if (!isShowing()) {
+ return;
+ }
+
+ mHidden = true;
+ runHideAnimation();
+ setZeroTouchableSurface();
+ }
+
+ @Override
+ public boolean isShowing() {
+ return !mDismissed && !mHidden;
+ }
+
+ @Override
+ public boolean isHidden() {
+ return mHidden;
+ }
+
+ /**
+ * Updates the coordinates of this popup.
+ * 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.
+ */
+ private void updateCoordinates(Rect contentRectOnScreen) {
+ Objects.requireNonNull(contentRectOnScreen);
+
+ if (!isShowing() || !mPopupWindow.isShowing()) {
+ return;
+ }
+
+ cancelOverflowAnimations();
+ refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
+ preparePopupContent();
+ // We need to specify the position in window coordinates.
+ // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
+ // specify the popup position in screen coordinates.
+ mPopupWindow.update(
+ mCoordsOnWindow.x, mCoordsOnWindow.y,
+ mPopupWindow.getWidth(), mPopupWindow.getHeight());
+ }
+
+ private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
+ refreshViewPort();
+
+ // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
+ // landscape.
+ final int x = Math.min(
+ contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
+ mViewPortOnScreen.right - mPopupWindow.getWidth());
+
+ final int y;
+
+ final int availableHeightAboveContent =
+ contentRectOnScreen.top - mViewPortOnScreen.top;
+ final int availableHeightBelowContent =
+ mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
+
+ final int margin = 2 * mMarginVertical;
+ final int toolbarHeightWithVerticalMargin = mLineHeight + margin;
+
+ if (!hasOverflow()) {
+ if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
+ // There is enough space at the top of the content.
+ y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
+ } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
+ // There is enough space at the bottom of the content.
+ y = contentRectOnScreen.bottom;
+ } else if (availableHeightBelowContent >= mLineHeight) {
+ // Just enough space to fit the toolbar with no vertical margins.
+ y = contentRectOnScreen.bottom - mMarginVertical;
+ } else {
+ // Not enough space. Prefer to position as high as possible.
+ y = Math.max(
+ mViewPortOnScreen.top,
+ contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
+ }
+ } else {
+ // Has an overflow.
+ final int minimumOverflowHeightWithMargin =
+ calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
+ final int availableHeightThroughContentDown =
+ mViewPortOnScreen.bottom - contentRectOnScreen.top
+ + toolbarHeightWithVerticalMargin;
+ final int availableHeightThroughContentUp =
+ contentRectOnScreen.bottom - mViewPortOnScreen.top
+ + toolbarHeightWithVerticalMargin;
+
+ if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the top of the content rect for the overflow.
+ // Position above and open upwards.
+ updateOverflowHeight(availableHeightAboveContent - margin);
+ y = contentRectOnScreen.top - mPopupWindow.getHeight();
+ mOpenOverflowUpwards = true;
+ } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
+ && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the top of the content rect for the main panel
+ // but not the overflow.
+ // Position above but open downwards.
+ updateOverflowHeight(availableHeightThroughContentDown - margin);
+ y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
+ mOpenOverflowUpwards = false;
+ } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the bottom of the content rect for the overflow.
+ // Position below and open downwards.
+ updateOverflowHeight(availableHeightBelowContent - margin);
+ y = contentRectOnScreen.bottom;
+ mOpenOverflowUpwards = false;
+ } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
+ && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the bottom of the content rect for the main panel
+ // but not the overflow.
+ // Position below but open upwards.
+ updateOverflowHeight(availableHeightThroughContentUp - margin);
+ y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin
+ - mPopupWindow.getHeight();
+ mOpenOverflowUpwards = true;
+ } else {
+ // Not enough space.
+ // Position at the top of the view port and open downwards.
+ updateOverflowHeight(mViewPortOnScreen.height() - margin);
+ y = mViewPortOnScreen.top;
+ mOpenOverflowUpwards = false;
+ }
+ }
+
+ // We later specify the location of PopupWindow relative to the attached window.
+ // The idea here is that 1) we can get the location of a View in both window coordinates
+ // and screen coordinates, where the offset between them should be equal to the window
+ // origin, and 2) we can use an arbitrary for this calculation while calculating the
+ // location of the rootview is supposed to be least expensive.
+ // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can avoid
+ // the following calculation.
+ mParent.getRootView().getLocationOnScreen(mTmpCoords);
+ int rootViewLeftOnScreen = mTmpCoords[0];
+ int rootViewTopOnScreen = mTmpCoords[1];
+ mParent.getRootView().getLocationInWindow(mTmpCoords);
+ int rootViewLeftOnWindow = mTmpCoords[0];
+ int rootViewTopOnWindow = mTmpCoords[1];
+ int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
+ int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
+ mCoordsOnWindow.set(
+ Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
+ }
+
+ /**
+ * Performs the "show" animation on the floating popup.
+ */
+ private void runShowAnimation() {
+ mShowAnimation.start();
+ }
+
+ /**
+ * Performs the "dismiss" animation on the floating popup.
+ */
+ private void runDismissAnimation() {
+ mDismissAnimation.start();
+ }
+
+ /**
+ * Performs the "hide" animation on the floating popup.
+ */
+ private void runHideAnimation() {
+ mHideAnimation.start();
+ }
+
+ private void cancelDismissAndHideAnimations() {
+ mDismissAnimation.cancel();
+ mHideAnimation.cancel();
+ }
+
+ private void cancelOverflowAnimations() {
+ mContentContainer.clearAnimation();
+ mMainPanel.animate().cancel();
+ mOverflowPanel.animate().cancel();
+ mToArrow.stop();
+ mToOverflow.stop();
+ }
+
+ private void openOverflow() {
+ final int targetWidth = mOverflowPanelSize.getWidth();
+ final int targetHeight = mOverflowPanelSize.getHeight();
+ final int startWidth = mContentContainer.getWidth();
+ final int startHeight = mContentContainer.getHeight();
+ final float startY = mContentContainer.getY();
+ final float left = mContentContainer.getX();
+ final float right = left + mContentContainer.getWidth();
+ Animation widthAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+ setWidth(mContentContainer, startWidth + deltaWidth);
+ if (isInRTLMode()) {
+ mContentContainer.setX(left);
+
+ // Lock the panels in place.
+ mMainPanel.setX(0);
+ mOverflowPanel.setX(0);
+ } else {
+ mContentContainer.setX(right - mContentContainer.getWidth());
+
+ // Offset the panels' positions so they look like they're locked in place
+ // on the screen.
+ mMainPanel.setX(mContentContainer.getWidth() - startWidth);
+ mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
+ }
+ }
+ };
+ Animation heightAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+ setHeight(mContentContainer, startHeight + deltaHeight);
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY(
+ startY - (mContentContainer.getHeight() - startHeight));
+ positionContentYCoordinatesIfOpeningOverflowUpwards();
+ }
+ }
+ };
+ final float overflowButtonStartX = mOverflowButton.getX();
+ final float overflowButtonTargetX =
+ isInRTLMode() ? overflowButtonStartX + targetWidth - mOverflowButton.getWidth()
+ : overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
+ Animation overflowButtonAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float overflowButtonX = overflowButtonStartX
+ + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
+ float deltaContainerWidth =
+ isInRTLMode() ? 0 : mContentContainer.getWidth() - startWidth;
+ float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
+ mOverflowButton.setX(actualOverflowButtonX);
+ }
+ };
+ widthAnimation.setInterpolator(mLogAccelerateInterpolator);
+ widthAnimation.setDuration(getAdjustedDuration(250));
+ heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ heightAnimation.setDuration(getAdjustedDuration(250));
+ overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ overflowButtonAnimation.setDuration(getAdjustedDuration(250));
+ mOpenOverflowAnimation.getAnimations().clear();
+ mOpenOverflowAnimation.getAnimations().clear();
+ mOpenOverflowAnimation.addAnimation(widthAnimation);
+ mOpenOverflowAnimation.addAnimation(heightAnimation);
+ mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
+ mContentContainer.startAnimation(mOpenOverflowAnimation);
+ mIsOverflowOpen = true;
+ mMainPanel.animate()
+ .alpha(0).withLayer()
+ .setInterpolator(mLinearOutSlowInInterpolator)
+ .setDuration(250)
+ .start();
+ mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
+ }
+
+ private void closeOverflow() {
+ final int targetWidth = mMainPanelSize.getWidth();
+ final int startWidth = mContentContainer.getWidth();
+ final float left = mContentContainer.getX();
+ final float right = left + mContentContainer.getWidth();
+ Animation widthAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+ setWidth(mContentContainer, startWidth + deltaWidth);
+ if (isInRTLMode()) {
+ mContentContainer.setX(left);
+
+ // Lock the panels in place.
+ mMainPanel.setX(0);
+ mOverflowPanel.setX(0);
+ } else {
+ mContentContainer.setX(right - mContentContainer.getWidth());
+
+ // Offset the panels' positions so they look like they're locked in place
+ // on the screen.
+ mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
+ mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
+ }
+ }
+ };
+ final int targetHeight = mMainPanelSize.getHeight();
+ final int startHeight = mContentContainer.getHeight();
+ final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
+ Animation heightAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+ setHeight(mContentContainer, startHeight + deltaHeight);
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY(bottom - mContentContainer.getHeight());
+ positionContentYCoordinatesIfOpeningOverflowUpwards();
+ }
+ }
+ };
+ final float overflowButtonStartX = mOverflowButton.getX();
+ final float overflowButtonTargetX =
+ isInRTLMode() ? overflowButtonStartX - startWidth + mOverflowButton.getWidth()
+ : overflowButtonStartX + startWidth - mOverflowButton.getWidth();
+ Animation overflowButtonAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float overflowButtonX = overflowButtonStartX
+ + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
+ float deltaContainerWidth =
+ isInRTLMode() ? 0 : mContentContainer.getWidth() - startWidth;
+ float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
+ mOverflowButton.setX(actualOverflowButtonX);
+ }
+ };
+ widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ widthAnimation.setDuration(getAdjustedDuration(250));
+ heightAnimation.setInterpolator(mLogAccelerateInterpolator);
+ heightAnimation.setDuration(getAdjustedDuration(250));
+ overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ overflowButtonAnimation.setDuration(getAdjustedDuration(250));
+ mCloseOverflowAnimation.getAnimations().clear();
+ mCloseOverflowAnimation.addAnimation(widthAnimation);
+ mCloseOverflowAnimation.addAnimation(heightAnimation);
+ mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
+ mContentContainer.startAnimation(mCloseOverflowAnimation);
+ mIsOverflowOpen = false;
+ mMainPanel.animate()
+ .alpha(1).withLayer()
+ .setInterpolator(mFastOutLinearInInterpolator)
+ .setDuration(100)
+ .start();
+ mOverflowPanel.animate()
+ .alpha(0).withLayer()
+ .setInterpolator(mLinearOutSlowInInterpolator)
+ .setDuration(150)
+ .start();
+ }
+
+ /**
+ * Defines the position of the floating toolbar popup panels when transition animation has
+ * stopped.
+ */
+ private void setPanelsStatesAtRestingPosition() {
+ mOverflowButton.setEnabled(true);
+ mOverflowPanel.awakenScrollBars();
+
+ if (mIsOverflowOpen) {
+ // Set open state.
+ final Size containerSize = mOverflowPanelSize;
+ setSize(mContentContainer, containerSize);
+ mMainPanel.setAlpha(0);
+ mMainPanel.setVisibility(View.INVISIBLE);
+ mOverflowPanel.setAlpha(1);
+ mOverflowPanel.setVisibility(View.VISIBLE);
+ mOverflowButton.setImageDrawable(mArrow);
+ mOverflowButton.setContentDescription(mContext.getString(
+ R.string.floating_toolbar_close_overflow_description));
+
+ // Update x-coordinates depending on RTL state.
+ if (isInRTLMode()) {
+ mContentContainer.setX(mMarginHorizontal); // align left
+ mMainPanel.setX(0); // align left
+ mOverflowButton.setX(// align right
+ containerSize.getWidth() - mOverflowButtonSize.getWidth());
+ mOverflowPanel.setX(0); // align left
+ } else {
+ mContentContainer.setX(// align right
+ mPopupWindow.getWidth() - containerSize.getWidth() - mMarginHorizontal);
+ mMainPanel.setX(-mContentContainer.getX()); // align right
+ mOverflowButton.setX(0); // align left
+ mOverflowPanel.setX(0); // align left
+ }
+
+ // Update y-coordinates depending on overflow's open direction.
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setY(// align bottom
+ containerSize.getHeight() - mContentContainer.getHeight());
+ mOverflowButton.setY(// align bottom
+ containerSize.getHeight() - mOverflowButtonSize.getHeight());
+ mOverflowPanel.setY(0); // align top
+ } else {
+ // opens downwards.
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setY(0); // align top
+ mOverflowButton.setY(0); // align top
+ mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
+ }
+ } else {
+ // Overflow not open. Set closed state.
+ final Size containerSize = mMainPanelSize;
+ setSize(mContentContainer, containerSize);
+ mMainPanel.setAlpha(1);
+ mMainPanel.setVisibility(View.VISIBLE);
+ mOverflowPanel.setAlpha(0);
+ mOverflowPanel.setVisibility(View.INVISIBLE);
+ mOverflowButton.setImageDrawable(mOverflow);
+ mOverflowButton.setContentDescription(mContext.getString(
+ R.string.floating_toolbar_open_overflow_description));
+
+ if (hasOverflow()) {
+ // Update x-coordinates depending on RTL state.
+ if (isInRTLMode()) {
+ mContentContainer.setX(mMarginHorizontal); // align left
+ mMainPanel.setX(0); // align left
+ mOverflowButton.setX(0); // align left
+ mOverflowPanel.setX(0); // align left
+ } else {
+ mContentContainer.setX(// align right
+ mPopupWindow.getWidth() - containerSize.getWidth() - mMarginHorizontal);
+ mMainPanel.setX(0); // align left
+ mOverflowButton.setX(// align right
+ containerSize.getWidth() - mOverflowButtonSize.getWidth());
+ mOverflowPanel.setX(// align right
+ containerSize.getWidth() - mOverflowPanelSize.getWidth());
+ }
+
+ // Update y-coordinates depending on overflow's open direction.
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY(// align bottom
+ mMarginVertical + mOverflowPanelSize.getHeight()
+ - containerSize.getHeight());
+ mMainPanel.setY(0); // align top
+ mOverflowButton.setY(0); // align top
+ mOverflowPanel.setY(// align bottom
+ containerSize.getHeight() - mOverflowPanelSize.getHeight());
+ } else {
+ // opens downwards.
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setY(0); // align top
+ mOverflowButton.setY(0); // align top
+ mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
+ }
+ } else {
+ // No overflow.
+ mContentContainer.setX(mMarginHorizontal); // align left
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setX(0); // align left
+ mMainPanel.setY(0); // align top
+ }
+ }
+ }
+
+ private void updateOverflowHeight(int suggestedHeight) {
+ if (hasOverflow()) {
+ final int maxItemSize =
+ (suggestedHeight - mOverflowButtonSize.getHeight()) / mLineHeight;
+ final int newHeight = calculateOverflowHeight(maxItemSize);
+ if (mOverflowPanelSize.getHeight() != newHeight) {
+ mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
+ }
+ setSize(mOverflowPanel, mOverflowPanelSize);
+ if (mIsOverflowOpen) {
+ setSize(mContentContainer, mOverflowPanelSize);
+ if (mOpenOverflowUpwards) {
+ final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
+ mContentContainer.setY(mContentContainer.getY() + deltaHeight);
+ mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
+ }
+ } else {
+ setSize(mContentContainer, mMainPanelSize);
+ }
+ updatePopupSize();
+ }
+ }
+
+ private void updatePopupSize() {
+ int width = 0;
+ int height = 0;
+ if (mMainPanelSize != null) {
+ width = Math.max(width, mMainPanelSize.getWidth());
+ height = Math.max(height, mMainPanelSize.getHeight());
+ }
+ if (mOverflowPanelSize != null) {
+ width = Math.max(width, mOverflowPanelSize.getWidth());
+ height = Math.max(height, mOverflowPanelSize.getHeight());
+ }
+ mPopupWindow.setWidth(width + mMarginHorizontal * 2);
+ mPopupWindow.setHeight(height + mMarginVertical * 2);
+ maybeComputeTransitionDurationScale();
+ }
+
+ private void refreshViewPort() {
+ mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
+ }
+
+ private int getAdjustedToolbarWidth(int suggestedWidth) {
+ int width = suggestedWidth;
+ refreshViewPort();
+ int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
+ if (width <= 0) {
+ width = mParent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
+ }
+ return Math.min(width, maximumWidth);
+ }
+
+ /**
+ * Sets the touchable region of this popup to be zero. This means that all touch events on
+ * this popup will go through to the surface behind it.
+ */
+ private void setZeroTouchableSurface() {
+ mTouchableRegion.setEmpty();
+ }
+
+ /**
+ * Sets the touchable region of this popup to be the area occupied by its content.
+ */
+ private void setContentAreaAsTouchableSurface() {
+ Objects.requireNonNull(mMainPanelSize);
+ final int width;
+ final int height;
+ if (mIsOverflowOpen) {
+ Objects.requireNonNull(mOverflowPanelSize);
+ width = mOverflowPanelSize.getWidth();
+ height = mOverflowPanelSize.getHeight();
+ } else {
+ width = mMainPanelSize.getWidth();
+ height = mMainPanelSize.getHeight();
+ }
+ mTouchableRegion.set(
+ (int) mContentContainer.getX(),
+ (int) mContentContainer.getY(),
+ (int) mContentContainer.getX() + width,
+ (int) mContentContainer.getY() + height);
+ }
+
+ /**
+ * Make the touchable area of this popup be the area specified by mTouchableRegion.
+ * This should be called after the popup window has been dismissed (dismiss/hide)
+ * and is probably being re-shown with a new content root view.
+ */
+ private void setTouchableSurfaceInsetsComputer() {
+ ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
+ .getRootView()
+ .getViewTreeObserver();
+ viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
+ viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
+ }
+
+ private boolean isInRTLMode() {
+ return mContext.getApplicationInfo().hasRtlSupport()
+ && mContext.getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ private boolean hasOverflow() {
+ return mOverflowPanelSize != null;
+ }
+
+ /**
+ * Fits as many menu items in the main panel and returns a list of the menu items that
+ * were not fit in.
+ *
+ * @return The menu items that are not included in this main panel.
+ */
+ public List<MenuItem> layoutMainPanelItems(
+ List<MenuItem> menuItems, final int toolbarWidth) {
+ Objects.requireNonNull(menuItems);
+
+ int availableWidth = toolbarWidth;
+
+ final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>();
+ // add the overflow menu items to the end of the remainingMenuItems list.
+ final LinkedList<MenuItem> overflowMenuItems = new LinkedList();
+ for (MenuItem menuItem : menuItems) {
+ if (menuItem.getItemId() != android.R.id.textAssist
+ && menuItem.requiresOverflow()) {
+ overflowMenuItems.add(menuItem);
+ } else {
+ remainingMenuItems.add(menuItem);
+ }
+ }
+ remainingMenuItems.addAll(overflowMenuItems);
+
+ mMainPanel.removeAllViews();
+ mMainPanel.setPaddingRelative(0, 0, 0, 0);
+
+ int lastGroupId = -1;
+ boolean isFirstItem = true;
+ while (!remainingMenuItems.isEmpty()) {
+ final MenuItem menuItem = remainingMenuItems.peek();
+
+ // if this is the first item, regardless of requiresOverflow(), it should be
+ // displayed on the main panel. Otherwise all items including this one will be
+ // overflow items, and should be displayed in overflow panel.
+ if (!isFirstItem && menuItem.requiresOverflow()) {
+ break;
+ }
+
+ final boolean showIcon = isFirstItem && menuItem.getItemId() == R.id.textAssist;
+ final View menuItemButton = createMenuItemButton(
+ mContext, menuItem, mIconTextSpacing, showIcon);
+ if (!showIcon && menuItemButton instanceof LinearLayout) {
+ ((LinearLayout) menuItemButton).setGravity(Gravity.CENTER);
+ }
+
+ // Adding additional start padding for the first button to even out button spacing.
+ if (isFirstItem) {
+ menuItemButton.setPaddingRelative(
+ (int) (1.5 * menuItemButton.getPaddingStart()),
+ menuItemButton.getPaddingTop(),
+ menuItemButton.getPaddingEnd(),
+ menuItemButton.getPaddingBottom());
+ }
+
+ // Adding additional end padding for the last button to even out button spacing.
+ boolean isLastItem = remainingMenuItems.size() == 1;
+ if (isLastItem) {
+ menuItemButton.setPaddingRelative(
+ menuItemButton.getPaddingStart(),
+ menuItemButton.getPaddingTop(),
+ (int) (1.5 * menuItemButton.getPaddingEnd()),
+ menuItemButton.getPaddingBottom());
+ }
+
+ menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ final int menuItemButtonWidth = Math.min(
+ menuItemButton.getMeasuredWidth(), toolbarWidth);
+
+ // Check if we can fit an item while reserving space for the overflowButton.
+ final boolean canFitWithOverflow =
+ menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth();
+ final boolean canFitNoOverflow =
+ isLastItem && menuItemButtonWidth <= availableWidth;
+ if (canFitWithOverflow || canFitNoOverflow) {
+ setButtonTagAndClickListener(menuItemButton, menuItem);
+ // Set tooltips for main panel items, but not overflow items (b/35726766).
+ menuItemButton.setTooltipText(menuItem.getTooltipText());
+ mMainPanel.addView(menuItemButton);
+ final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
+ params.width = menuItemButtonWidth;
+ menuItemButton.setLayoutParams(params);
+ availableWidth -= menuItemButtonWidth;
+ remainingMenuItems.pop();
+ } else {
+ break;
+ }
+ lastGroupId = menuItem.getGroupId();
+ isFirstItem = false;
+ }
+
+ if (!remainingMenuItems.isEmpty()) {
+ // Reserve space for overflowButton.
+ mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
+ }
+
+ mMainPanelSize = measure(mMainPanel);
+ return remainingMenuItems;
+ }
+
+ private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
+ ArrayAdapter<MenuItem> overflowPanelAdapter =
+ (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
+ overflowPanelAdapter.clear();
+ final int size = menuItems.size();
+ for (int i = 0; i < size; i++) {
+ overflowPanelAdapter.add(menuItems.get(i));
+ }
+ mOverflowPanel.setAdapter(overflowPanelAdapter);
+ if (mOpenOverflowUpwards) {
+ mOverflowPanel.setY(0);
+ } else {
+ mOverflowPanel.setY(mOverflowButtonSize.getHeight());
+ }
+
+ int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
+ int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
+ mOverflowPanelSize = new Size(width, height);
+ setSize(mOverflowPanel, mOverflowPanelSize);
+ }
+
+ /**
+ * Resets the content container and appropriately position it's panels.
+ */
+ private void preparePopupContent() {
+ mContentContainer.removeAllViews();
+
+ // Add views in the specified order so they stack up as expected.
+ // Order: overflowPanel, mainPanel, overflowButton.
+ if (hasOverflow()) {
+ mContentContainer.addView(mOverflowPanel);
+ }
+ mContentContainer.addView(mMainPanel);
+ if (hasOverflow()) {
+ mContentContainer.addView(mOverflowButton);
+ }
+ setPanelsStatesAtRestingPosition();
+ setContentAreaAsTouchableSurface();
+
+ // The positioning of contents in RTL is wrong when the view is first rendered.
+ // Hide the view and post a runnable to recalculate positions and render the view.
+ // TODO: Investigate why this happens and fix.
+ if (isInRTLMode()) {
+ mContentContainer.setAlpha(0);
+ mContentContainer.post(mPreparePopupContentRTLHelper);
+ }
+ }
+
+ /**
+ * Clears out the panels and their container. Resets their calculated sizes.
+ */
+ private void clearPanels() {
+ mOverflowPanelSize = null;
+ mMainPanelSize = null;
+ mIsOverflowOpen = false;
+ mMainPanel.removeAllViews();
+ ArrayAdapter<MenuItem> overflowPanelAdapter =
+ (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
+ overflowPanelAdapter.clear();
+ mOverflowPanel.setAdapter(overflowPanelAdapter);
+ mContentContainer.removeAllViews();
+ }
+
+ private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
+ if (mOpenOverflowUpwards) {
+ mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
+ mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
+ mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
+ }
+ }
+
+ private int getOverflowWidth() {
+ int overflowWidth = 0;
+ final int count = mOverflowPanel.getAdapter().getCount();
+ for (int i = 0; i < count; i++) {
+ MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
+ overflowWidth =
+ Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
+ }
+ return overflowWidth;
+ }
+
+ private int calculateOverflowHeight(int maxItemSize) {
+ // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
+ int actualSize = Math.min(
+ MAX_OVERFLOW_SIZE,
+ Math.min(
+ Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
+ mOverflowPanel.getCount()));
+ int extension = 0;
+ if (actualSize < mOverflowPanel.getCount()) {
+ // The overflow will require scrolling to get to all the items.
+ // Extend the height so that part of the hidden items is displayed.
+ extension = (int) (mLineHeight * 0.5f);
+ }
+ return actualSize * mLineHeight
+ + mOverflowButtonSize.getHeight()
+ + extension;
+ }
+
+ private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
+ menuItemButton.setTag(MenuItemRepr.of(menuItem));
+ menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
+ }
+
+ /**
+ * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
+ * animations. See comment about this in the code.
+ */
+ private int getAdjustedDuration(int originalDuration) {
+ if (mTransitionDurationScale < 150) {
+ // For smaller transition, decrease the time.
+ return Math.max(originalDuration - 50, 0);
+ } else if (mTransitionDurationScale > 300) {
+ // For bigger transition, increase the time.
+ return originalDuration + 50;
+ }
+
+ // Scale the animation duration with getDurationScale(). This allows
+ // android.view.animation.* animations to scale just like android.animation.* animations
+ // when animator duration scale is adjusted in "Developer Options".
+ // For this reason, do not use this method for android.animation.* animations.
+ return (int) (originalDuration * ValueAnimator.getDurationScale());
+ }
+
+ private void maybeComputeTransitionDurationScale() {
+ if (mMainPanelSize != null && mOverflowPanelSize != null) {
+ int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
+ int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
+ mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h)
+ / mContentContainer.getContext().getResources().getDisplayMetrics().density);
+ }
+ }
+
+ private ViewGroup createMainPanel() {
+ ViewGroup mainPanel = new LinearLayout(mContext) {
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (isOverflowAnimating()) {
+ // Update widthMeasureSpec to make sure that this view is not clipped
+ // as we offset its coordinates with respect to its parent.
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ mMainPanelSize.getWidth(),
+ MeasureSpec.EXACTLY);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ // Intercept the touch event while the overflow is animating.
+ return isOverflowAnimating();
+ }
+ };
+ return mainPanel;
+ }
+
+ private ImageButton createOverflowButton() {
+ final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
+ .inflate(R.layout.floating_popup_overflow_button, null);
+ overflowButton.setImageDrawable(mOverflow);
+ overflowButton.setOnClickListener(v -> {
+ if (mIsOverflowOpen) {
+ overflowButton.setImageDrawable(mToOverflow);
+ mToOverflow.start();
+ closeOverflow();
+ } else {
+ overflowButton.setImageDrawable(mToArrow);
+ mToArrow.start();
+ openOverflow();
+ }
+ });
+ return overflowButton;
+ }
+
+ 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);
+ overflowPanel.setDividerHeight(0);
+
+ final ArrayAdapter adapter =
+ new ArrayAdapter<MenuItem>(mContext, 0) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return mOverflowPanelViewHelper.getView(
+ getItem(position), mOverflowPanelSize.getWidth(), convertView);
+ }
+ };
+ overflowPanel.setAdapter(adapter);
+
+ overflowPanel.setOnItemClickListener((parent, view, position, id) -> {
+ MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
+ if (mOnMenuItemClickListener != null) {
+ mOnMenuItemClickListener.onMenuItemClick(menuItem);
+ }
+ });
+
+ return overflowPanel;
+ }
+
+ private boolean isOverflowAnimating() {
+ final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
+ && !mOpenOverflowAnimation.hasEnded();
+ final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
+ && !mCloseOverflowAnimation.hasEnded();
+ return overflowOpening || overflowClosing;
+ }
+
+ private Animation.AnimationListener createOverflowAnimationListener() {
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ // Disable the overflow button while it's animating.
+ // It will be re-enabled when the animation stops.
+ mOverflowButton.setEnabled(false);
+ // Ensure both panels have visibility turned on when the overflow animation
+ // starts.
+ mMainPanel.setVisibility(View.VISIBLE);
+ mOverflowPanel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // Posting this because it seems like this is called before the animation
+ // actually ends.
+ mContentContainer.post(() -> {
+ setPanelsStatesAtRestingPosition();
+ setContentAreaAsTouchableSurface();
+ });
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ };
+ return listener;
+ }
+
+ private static Size measure(View view) {
+ Preconditions.checkState(view.getParent() == null);
+ view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
+ }
+
+ private static void setSize(View view, int width, int height) {
+ view.setMinimumWidth(width);
+ view.setMinimumHeight(height);
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
+ params.width = width;
+ params.height = height;
+ view.setLayoutParams(params);
+ }
+
+ private static void setSize(View view, Size size) {
+ setSize(view, size.getWidth(), size.getHeight());
+ }
+
+ private static void setWidth(View view, int width) {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ setSize(view, width, params.height);
+ }
+
+ private static void setHeight(View view, int height) {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ setSize(view, params.width, height);
+ }
+
+ /**
+ * A custom ListView for the overflow panel.
+ */
+ private static final class OverflowPanel extends ListView {
+
+ private final RemoteSelectionToolbar mPopup;
+
+ OverflowPanel(RemoteSelectionToolbar popup) {
+ super(Objects.requireNonNull(popup).mContext);
+ this.mPopup = popup;
+ setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
+ setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Update heightMeasureSpec to make sure that this view is not clipped
+ // as we offset it's coordinates with respect to its parent.
+ int height = mPopup.mOverflowPanelSize.getHeight()
+ - mPopup.mOverflowButtonSize.getHeight();
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mPopup.isOverflowAnimating()) {
+ // Eat the touch event.
+ return true;
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ protected boolean awakenScrollBars() {
+ return super.awakenScrollBars();
+ }
+ }
+
+ /**
+ * A custom interpolator used for various floating toolbar animations.
+ */
+ private static final class LogAccelerateInterpolator implements Interpolator {
+
+ private static final int BASE = 100;
+ private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
+
+ private static float computeLog(float t, int base) {
+ return (float) (1 - Math.pow(base, -t));
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
+ }
+ }
+
+ /**
+ * A helper for generating views for the overflow panel.
+ */
+ private static final class OverflowPanelViewHelper {
+
+ private final View mCalculator;
+ private final int mIconTextSpacing;
+ private final int mSidePadding;
+
+ private final Context mContext;
+
+ OverflowPanelViewHelper(Context context, int iconTextSpacing) {
+ mContext = Objects.requireNonNull(context);
+ mIconTextSpacing = iconTextSpacing;
+ mSidePadding = context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
+ mCalculator = createMenuButton(null);
+ }
+
+ public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
+ Objects.requireNonNull(menuItem);
+ if (convertView != null) {
+ updateMenuItemButton(
+ convertView, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
+ } else {
+ convertView = createMenuButton(menuItem);
+ }
+ convertView.setMinimumWidth(minimumWidth);
+ return convertView;
+ }
+
+ public int calculateWidth(MenuItem menuItem) {
+ updateMenuItemButton(
+ mCalculator, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
+ mCalculator.measure(
+ View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ return mCalculator.getMeasuredWidth();
+ }
+
+ private View createMenuButton(MenuItem menuItem) {
+ View button = createMenuItemButton(
+ mContext, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
+ button.setPadding(mSidePadding, 0, mSidePadding, 0);
+ return button;
+ }
+
+ private boolean shouldShowIcon(MenuItem menuItem) {
+ if (menuItem != null) {
+ return menuItem.getGroupId() == android.R.id.textAssist;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Creates and returns a menu button for the specified menu item.
+ */
+ private static View createMenuItemButton(
+ Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
+ final View menuItemButton = LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_menu_button, null);
+ if (menuItem != null) {
+ updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon);
+ }
+ return menuItemButton;
+ }
+
+ /**
+ * Updates the specified menu item button with the specified menu item data.
+ */
+ private static void updateMenuItemButton(
+ View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
+ final TextView buttonText = menuItemButton.findViewById(
+ R.id.floating_toolbar_menu_item_text);
+ buttonText.setEllipsize(null);
+ if (TextUtils.isEmpty(menuItem.getTitle())) {
+ buttonText.setVisibility(View.GONE);
+ } else {
+ buttonText.setVisibility(View.VISIBLE);
+ buttonText.setText(menuItem.getTitle());
+ }
+ final ImageView buttonIcon = menuItemButton.findViewById(
+ R.id.floating_toolbar_menu_item_image);
+ if (menuItem.getIcon() == null || !showIcon) {
+ buttonIcon.setVisibility(View.GONE);
+ if (buttonText != null) {
+ buttonText.setPaddingRelative(0, 0, 0, 0);
+ }
+ } else {
+ buttonIcon.setVisibility(View.VISIBLE);
+ buttonIcon.setImageDrawable(menuItem.getIcon());
+ if (buttonText != null) {
+ buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
+ }
+ }
+ final CharSequence contentDescription = menuItem.getContentDescription();
+ if (TextUtils.isEmpty(contentDescription)) {
+ menuItemButton.setContentDescription(menuItem.getTitle());
+ } else {
+ menuItemButton.setContentDescription(contentDescription);
+ }
+ }
+
+ private static ViewGroup createContentContainer(Context context) {
+ ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_container, null);
+ contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ contentContainer.setTag("floating_toolbar");
+ contentContainer.setClipToOutline(true);
+ return contentContainer;
+ }
+
+ private static PopupWindow createPopupWindow(ViewGroup content) {
+ ViewGroup popupContentHolder = new LinearLayout(content.getContext());
+ PopupWindow popupWindow = new PopupWindow(popupContentHolder);
+ // TODO: Use .setIsLaidOutInScreen(true) instead of .setClippingEnabled(false)
+ // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
+ popupWindow.setClippingEnabled(false);
+ popupWindow.setWindowLayoutType(
+ WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
+ popupWindow.setAnimationStyle(0);
+ popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ content.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ popupContentHolder.addView(content);
+ return popupWindow;
+ }
+
+ /**
+ * Creates an "appear" animation for the specified view.
+ *
+ * @param view The view to animate
+ */
+ private static AnimatorSet createEnterAnimation(View view) {
+ AnimatorSet animation = new AnimatorSet();
+ animation.playTogether(
+ ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
+ return animation;
+ }
+
+ /**
+ * Creates a "disappear" animation for the specified view.
+ *
+ * @param view The view to animate
+ * @param startDelay The start delay of the animation
+ * @param listener The animation listener
+ */
+ private static AnimatorSet createExitAnimation(
+ View view, int startDelay, Animator.AnimatorListener listener) {
+ AnimatorSet animation = new AnimatorSet();
+ animation.playTogether(
+ ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
+ animation.setStartDelay(startDelay);
+ animation.addListener(listener);
+ return animation;
+ }
+
+ /**
+ * Returns a re-themed context with controlled look and feel for views.
+ */
+ private static Context applyDefaultTheme(Context originalContext) {
+ TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
+ boolean isLightTheme = a.getBoolean(0, true);
+ int themeId =
+ isLightTheme ? R.style.Theme_DeviceDefault_Light : R.style.Theme_DeviceDefault;
+ a.recycle();
+ return new ContextThemeWrapper(originalContext, themeId);
+ }
+
+ /**
+ * Represents the identity of a MenuItem that is rendered in a FloatingToolbarPopup.
+ */
+ @VisibleForTesting
+ public static final class MenuItemRepr {
+
+ public final int itemId;
+ public final int groupId;
+ @Nullable public final String title;
+ @Nullable private final Drawable mIcon;
+
+ private MenuItemRepr(
+ int itemId, int groupId, @Nullable CharSequence title, @Nullable Drawable icon) {
+ this.itemId = itemId;
+ this.groupId = groupId;
+ this.title = (title == null) ? null : title.toString();
+ mIcon = icon;
+ }
+
+ /**
+ * Creates an instance of MenuItemRepr for the specified menu item.
+ */
+ public static MenuItemRepr of(MenuItem menuItem) {
+ return new MenuItemRepr(
+ menuItem.getItemId(),
+ menuItem.getGroupId(),
+ menuItem.getTitle(),
+ menuItem.getIcon());
+ }
+
+ /**
+ * Returns this object's hashcode.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(itemId, groupId, title, mIcon);
+ }
+
+ /**
+ * Returns true if this object is the same as the specified object.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof MenuItemRepr)) {
+ return false;
+ }
+ final MenuItemRepr other = (MenuItemRepr) o;
+ return itemId == other.itemId
+ && groupId == other.groupId
+ && TextUtils.equals(title, other.title)
+ // Many Drawables (icons) do not implement equals(). Using equals() here instead
+ // of reference comparisons in case a Drawable subclass implements equals().
+ && Objects.equals(mIcon, other.mIcon);
+ }
+
+ /**
+ * Returns true if the two menu item collections are the same based on MenuItemRepr.
+ */
+ public static boolean reprEquals(
+ Collection<MenuItem> menuItems1, Collection<MenuItem> menuItems2) {
+ if (menuItems1.size() != menuItems2.size()) {
+ return false;
+ }
+
+ final Iterator<MenuItem> menuItems2Iter = menuItems2.iterator();
+ for (MenuItem menuItem1 : menuItems1) {
+ final MenuItem menuItem2 = menuItems2Iter.next();
+ if (!MenuItemRepr.of(menuItem1).equals(MenuItemRepr.of(menuItem2))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}