summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/internal/widget/FloatingToolbar.java1045
-rw-r--r--core/res/res/layout/floating_popup_close_overflow_button.xml24
-rw-r--r--core/res/res/layout/floating_popup_open_overflow_button.xml2
-rw-r--r--core/res/res/layout/floating_popup_overflow_list_item33
-rw-r--r--core/res/res/values/dimens.xml2
-rw-r--r--core/res/res/values/strings.xml6
-rwxr-xr-xcore/res/res/values/symbols.xml4
7 files changed, 858 insertions, 258 deletions
diff --git a/core/java/com/android/internal/widget/FloatingToolbar.java b/core/java/com/android/internal/widget/FloatingToolbar.java
index be9945d35fbc..2219ad10c720 100644
--- a/core/java/com/android/internal/widget/FloatingToolbar.java
+++ b/core/java/com/android/internal/widget/FloatingToolbar.java
@@ -24,7 +24,9 @@ import android.content.Context;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.Region;
import android.graphics.drawable.ColorDrawable;
+import android.util.Size;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -32,19 +34,28 @@ import android.view.MenuItem;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.Transformation;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
+import android.widget.ListView;
import android.widget.PopupWindow;
-
-import com.android.internal.R;
-import com.android.internal.util.Preconditions;
+import android.widget.TextView;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+
/**
* A floating toolbar for showing contextual menu items.
* This view shows as many menu item buttons as can fit in the horizontal toolbar and the
@@ -53,6 +64,9 @@ import java.util.List;
*/
public final class FloatingToolbar {
+ // This class is responsible for the public API of the floating toolbar.
+ // It delegates rendering operations to the FloatingToolbarPopup.
+
private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
new MenuItem.OnMenuItemClickListener() {
@Override
@@ -63,17 +77,6 @@ public final class FloatingToolbar {
private final Context mContext;
private final FloatingToolbarPopup mPopup;
- private final ViewGroup mMenuItemButtonsContainer;
- private final View.OnClickListener mMenuItemButtonOnClickListener =
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (v.getTag() instanceof MenuItem) {
- mMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
- mPopup.dismiss();
- }
- }
- };
private final Rect mContentRect = new Rect();
private final Point mCoordinates = new Point();
@@ -81,17 +84,17 @@ public final class FloatingToolbar {
private Menu mMenu;
private List<CharSequence> mShowingTitles = new ArrayList<CharSequence>();
private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
- private View mOpenOverflowButton;
private int mSuggestedWidth;
+ private boolean mWidthChanged = true;
+ private int mOverflowDirection;
/**
* Initializes a floating toolbar.
*/
public FloatingToolbar(Context context, Window window) {
mContext = Preconditions.checkNotNull(context);
- mPopup = new FloatingToolbarPopup(Preconditions.checkNotNull(window.getDecorView()));
- mMenuItemButtonsContainer = createMenuButtonsContainer(context);
+ mPopup = new FloatingToolbarPopup(window.getDecorView());
}
/**
@@ -137,6 +140,10 @@ 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;
return this;
}
@@ -146,16 +153,18 @@ public final class FloatingToolbar {
*/
public FloatingToolbar show() {
List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
- if (hasContentChanged(menuItems) || hasWidthChanged()) {
+ if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
mPopup.dismiss();
- layoutMenuItemButtons(menuItems);
+ mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
mShowingTitles = getMenuItemTitles(menuItems);
}
refreshCoordinates();
+ mPopup.setOverflowDirection(mOverflowDirection);
mPopup.updateCoordinates(mCoordinates.x, mCoordinates.y);
if (!mPopup.isShowing()) {
mPopup.show(mCoordinates.x, mCoordinates.y);
}
+ mWidthChanged = false;
return this;
}
@@ -189,45 +198,26 @@ public final class FloatingToolbar {
* Refreshes {@link #mCoordinates} with values based on {@link #mContentRect}.
*/
private void refreshCoordinates() {
- int popupWidth = mPopup.getWidth();
- int popupHeight = mPopup.getHeight();
- if (!mPopup.isShowing()) {
- // Popup isn't yet shown, get estimated size from the menu item buttons container.
- mMenuItemButtonsContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- popupWidth = mMenuItemButtonsContainer.getMeasuredWidth();
- popupHeight = mMenuItemButtonsContainer.getMeasuredHeight();
- }
- int x = mContentRect.centerX() - popupWidth / 2;
+ int x = mContentRect.centerX() - mPopup.getWidth() / 2;
int y;
- if (shouldDisplayAtTopOfContent()) {
- y = mContentRect.top - popupHeight;
+ if (mContentRect.top > mPopup.getHeight()) {
+ y = mContentRect.top - mPopup.getHeight();
+ mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_UP;
+ } else if (mContentRect.top > getEstimatedToolbarHeight(mContext)) {
+ y = mContentRect.top - getEstimatedToolbarHeight(mContext);
+ mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
} else {
y = mContentRect.bottom;
+ mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
}
mCoordinates.set(x, y);
}
/**
- * Returns true if this floating toolbar's menu items have been reordered or changed.
- */
- private boolean hasContentChanged(List<MenuItem> menuItems) {
- return !mShowingTitles.equals(getMenuItemTitles(menuItems));
- }
-
- /**
- * Returns true if there is a significant change in width of the toolbar.
- */
- private boolean hasWidthChanged() {
- int actualWidth = mPopup.getWidth();
- int difference = Math.abs(actualWidth - mSuggestedWidth);
- return difference > (actualWidth * 0.2);
- }
-
- /**
- * Returns true if the preferred positioning of the toolbar is above the content rect.
+ * Returns true if this floating toolbar is currently showing the specified menu items.
*/
- private boolean shouldDisplayAtTopOfContent() {
- return mContentRect.top - getMinimumOverflowHeight(mContext) > 0;
+ private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
+ return mShowingTitles.equals(getMenuItemTitles(menuItems));
}
/**
@@ -258,178 +248,162 @@ public final class FloatingToolbar {
return titles;
}
- private void layoutMenuItemButtons(List<MenuItem> menuItems) {
- final int toolbarWidth = getAdjustedToolbarWidth(mContext, mSuggestedWidth)
- // Reserve space for the "open overflow" button.
- - getEstimatedOpenOverflowButtonWidth(mContext);
-
- int availableWidth = toolbarWidth;
- LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
-
- mMenuItemButtonsContainer.removeAllViews();
-
- boolean isFirstItem = true;
- while (!remainingMenuItems.isEmpty()) {
- final MenuItem menuItem = remainingMenuItems.peek();
- Button menuItemButton = createMenuItemButton(mContext, menuItem);
-
- // Adding additional left padding for the first button to even out button spacing.
- if (isFirstItem) {
- menuItemButton.setPadding(
- 2 * menuItemButton.getPaddingLeft(),
- menuItemButton.getPaddingTop(),
- menuItemButton.getPaddingRight(),
- menuItemButton.getPaddingBottom());
- isFirstItem = false;
- }
-
- // Adding additional right padding for the last button to even out button spacing.
- if (remainingMenuItems.size() == 1) {
- menuItemButton.setPadding(
- menuItemButton.getPaddingLeft(),
- menuItemButton.getPaddingTop(),
- 2 * menuItemButton.getPaddingRight(),
- menuItemButton.getPaddingBottom());
- }
-
- menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
- if (menuItemButtonWidth <= availableWidth) {
- menuItemButton.setTag(menuItem);
- menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
- mMenuItemButtonsContainer.addView(menuItemButton);
- menuItemButton.getLayoutParams().width = menuItemButtonWidth;
- availableWidth -= menuItemButtonWidth;
- remainingMenuItems.pop();
- } else {
- // The "open overflow" button launches the vertical overflow from the
- // floating toolbar.
- createOpenOverflowButtonIfNotExists();
- mMenuItemButtonsContainer.addView(mOpenOverflowButton);
- break;
- }
- }
- mPopup.setContentView(mMenuItemButtonsContainer);
- }
-
- /**
- * Creates and returns the button that opens the vertical overflow.
- */
- private void createOpenOverflowButtonIfNotExists() {
- mOpenOverflowButton = (ImageButton) LayoutInflater.from(mContext)
- .inflate(R.layout.floating_popup_open_overflow_button, null);
- mOpenOverflowButton.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- // Open the overflow.
- }
- });
- }
-
- /**
- * Creates and returns a floating toolbar menu buttons container.
- */
- private static ViewGroup createMenuButtonsContainer(Context context) {
- return (ViewGroup) LayoutInflater.from(context)
- .inflate(R.layout.floating_popup_container, null);
- }
/**
- * Creates and returns a menu button for the specified menu item.
+ * A popup window used by the floating toolbar.
+ *
+ * This class is responsible for the rendering/animation of the floating toolbar.
+ * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time.
+ * It delegates specific panel functionality to the appropriate panel.
*/
- private static Button createMenuItemButton(Context context, MenuItem menuItem) {
- Button menuItemButton = (Button) LayoutInflater.from(context)
- .inflate(R.layout.floating_popup_menu_button, null);
- menuItemButton.setText(menuItem.getTitle());
- menuItemButton.setContentDescription(menuItem.getTitle());
- return menuItemButton;
- }
-
- private static int getMinimumOverflowHeight(Context context) {
- return context.getResources().
- getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
- }
+ private static final class FloatingToolbarPopup {
- private static int getEstimatedOpenOverflowButtonWidth(Context context) {
- return context.getResources()
- .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
- }
+ public static final int OVERFLOW_DIRECTION_UP = 0;
+ public static final int OVERFLOW_DIRECTION_DOWN = 1;
- private static int getAdjustedToolbarWidth(Context context, int width) {
- if (width <= 0 || width > getScreenWidth(context)) {
- width = context.getResources()
- .getDimensionPixelSize(R.dimen.floating_toolbar_default_width);
- }
- return width;
- }
+ private final View mParent;
+ private final PopupWindow mPopupWindow;
+ private final ViewGroup mContentContainer;
+ private final int mPadding;
- /**
- * Returns the device's screen width.
- */
- public static int getScreenWidth(Context context) {
- return context.getResources().getDisplayMetrics().widthPixels;
- }
+ private final Animation.AnimationListener mOnOverflowOpened =
+ new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
- /**
- * Returns the device's screen height.
- */
- public static int getScreenHeight(Context context) {
- return context.getResources().getDisplayMetrics().heightPixels;
- }
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // This animation should never be run if the overflow panel has not been
+ // initialized.
+ Preconditions.checkNotNull(mOverflowPanel);
+ mContentContainer.removeAllViews();
+ mContentContainer.addView(mOverflowPanel.getView());
+ mOverflowPanel.fadeIn(true);
+ setContentAreaAsTouchableSurface();
+ }
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ };
+ private final Animation.AnimationListener mOnOverflowClosed =
+ new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
- /**
- * A popup window used by the floating toolbar.
- */
- private static final class FloatingToolbarPopup {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // This animation should never be run if the main panel has not been
+ // initialized.
+ Preconditions.checkNotNull(mMainPanel);
+ mContentContainer.removeAllViews();
+ mContentContainer.addView(mMainPanel.getView());
+ mMainPanel.fadeIn(true);
+ setContentAreaAsTouchableSurface();
+ }
- private final View mParent;
- private final PopupWindow mPopupWindow;
- private final ViewGroup mContentContainer;
- private final Animator.AnimatorListener mOnDismissEnd =
- new AnimatorListenerAdapter() {
@Override
- public void onAnimationEnd(Animator animation) {
- mPopupWindow.dismiss();
- mDismissAnimating = false;
+ public void onAnimationRepeat(Animation animation) {
}
};
private final AnimatorSet mGrowFadeInFromBottomAnimation;
private final AnimatorSet mShrinkFadeOutFromBottomAnimation;
+ private final Runnable mOpenOverflow = new Runnable() {
+ @Override
+ public void run() {
+ openOverflow();
+ }
+ };
+ private final Runnable mCloseOverflow = new Runnable() {
+ @Override
+ public void run() {
+ closeOverflow();
+ }
+ };
+
+ private final Region mTouchableRegion = new Region();
+
private boolean mDismissAnimating;
+ private FloatingToolbarOverflowPanel mOverflowPanel;
+ private FloatingToolbarMainPanel mMainPanel;
+ private int mOverflowDirection;
+
/**
- * Initializes a new floating bar popup.
+ * Initializes a new floating toolbar popup.
*
- * @param parent A parent view to get the {@link View#getWindowToken()} token from.
+ * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
+ * from.
*/
public FloatingToolbarPopup(View parent) {
mParent = Preconditions.checkNotNull(parent);
mContentContainer = createContentContainer(parent.getContext());
mPopupWindow = createPopupWindow(mContentContainer);
mGrowFadeInFromBottomAnimation = createGrowFadeInFromBottom(mContentContainer);
- mShrinkFadeOutFromBottomAnimation =
- createShrinkFadeOutFromBottomAnimation(mContentContainer, mOnDismissEnd);
+ mShrinkFadeOutFromBottomAnimation = createShrinkFadeOutFromBottomAnimation(
+ mContentContainer,
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPopupWindow.dismiss();
+ mDismissAnimating = false;
+ setMainPanelAsContent();
+ }
+ });
+ // Make the touchable area of this popup be the area specified by mTouchableRegion.
+ mPopupWindow.getContentView()
+ .getRootView()
+ .getViewTreeObserver()
+ .addOnComputeInternalInsetsListener(
+ new ViewTreeObserver.OnComputeInternalInsetsListener() {
+ public void onComputeInternalInsets(
+ ViewTreeObserver.InternalInsetsInfo info) {
+ info.contentInsets.setEmpty();
+ info.visibleInsets.setEmpty();
+ info.touchableRegion.set(mTouchableRegion);
+ info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
+ .TOUCHABLE_INSETS_REGION);
+ }
+ });
+ mPadding = parent.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_margin);
+ }
+
+ /**
+ * Lays out buttons for the specified menu items.
+ */
+ public void layoutMenuItems(List<MenuItem> menuItems,
+ MenuItem.OnMenuItemClickListener menuItemClickListener, int suggestedWidth) {
+ mContentContainer.removeAllViews();
+ if (mMainPanel == null) {
+ mMainPanel = new FloatingToolbarMainPanel(mParent.getContext(), mOpenOverflow);
+ }
+ List<MenuItem> overflowMenuItems =
+ mMainPanel.layoutMenuItems(menuItems, suggestedWidth);
+ mMainPanel.setOnMenuItemClickListener(menuItemClickListener);
+ if (!overflowMenuItems.isEmpty()) {
+ if (mOverflowPanel == null) {
+ mOverflowPanel =
+ new FloatingToolbarOverflowPanel(mParent.getContext(), mCloseOverflow);
+ }
+ mOverflowPanel.setMenuItems(overflowMenuItems);
+ mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener);
+ }
+ updatePopupSize();
}
/**
* Shows this popup at the specified coordinates.
* The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
- * If this popup is already showing, this will be a no-op.
*/
public void show(int x, int y) {
if (isShowing()) {
- updateCoordinates(x, y);
return;
}
- mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, 0, 0);
- positionOnScreen(x, y);
+ stopDismissAnimation();
+ preparePopupContent();
+ mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, x, y);
growFadeInFromBottom();
-
- mDismissAnimating = false;
}
/**
@@ -440,12 +414,9 @@ public final class FloatingToolbar {
return;
}
- if (mDismissAnimating) {
- // This window is already dismissing. Don't restart the animation.
- return;
- }
mDismissAnimating = true;
shrinkFadeOutFromBottom();
+ setZeroTouchableSurface();
}
/**
@@ -460,32 +431,40 @@ public final class FloatingToolbar {
* The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
*/
public void updateCoordinates(int x, int y) {
- if (isShowing()) {
- positionOnScreen(x, y);
+ if (mDismissAnimating) {
+ // Already being dismissed. Ignore.
+ return;
}
+
+ preparePopupContent();
+ mPopupWindow.update(x, y, getWidth(), getHeight());
}
/**
- * Sets the content of this popup.
+ * Sets the direction in which the overflow will open. i.e. up or down.
+ *
+ * @param overflowDirection Either {@link #OVERFLOW_DIRECTION_UP}
+ * or {@link #OVERFLOW_DIRECTION_DOWN}.
*/
- public void setContentView(View view) {
- Preconditions.checkNotNull(view);
- mContentContainer.removeAllViews();
- mContentContainer.addView(view);
+ public void setOverflowDirection(int overflowDirection) {
+ mOverflowDirection = overflowDirection;
+ if (mOverflowPanel != null) {
+ mOverflowPanel.setOverflowDirection(mOverflowDirection);
+ }
}
/**
* Returns the width of this popup.
*/
public int getWidth() {
- return mContentContainer.getWidth();
+ return mPopupWindow.getWidth();
}
/**
* Returns the height of this popup.
*/
public int getHeight() {
- return mContentContainer.getHeight();
+ return mPopupWindow.getHeight();
}
/**
@@ -495,24 +474,10 @@ public final class FloatingToolbar {
return mContentContainer.getContext();
}
- private void positionOnScreen(int x, int y) {
- if (getWidth() == 0) {
- // content size is yet to be measured.
- mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- }
- x = clamp(x, 0, getScreenWidth(getContext()) - getWidth());
- y = clamp(y, 0, getScreenHeight(getContext()) - getHeight());
-
- // Position the view w.r.t. the window.
- mContentContainer.setX(x);
- mContentContainer.setY(y);
- }
-
/**
* Performs the "grow and fade in from the bottom" animation on the floating popup.
*/
private void growFadeInFromBottom() {
- setPivot();
mGrowFadeInFromBottomAnimation.start();
}
@@ -520,77 +485,643 @@ public final class FloatingToolbar {
* Performs the "shrink and fade out from bottom" animation on the floating popup.
*/
private void shrinkFadeOutFromBottom() {
- setPivot();
mShrinkFadeOutFromBottomAnimation.start();
}
+ private void stopDismissAnimation() {
+ mDismissAnimating = false;
+ mShrinkFadeOutFromBottomAnimation.cancel();
+ }
+
+ /**
+ * Opens the floating toolbar overflow.
+ * This method should not be called if menu items have not been laid out with
+ * {@link #layoutMenuItems(List, MenuItem.OnMenuItemClickListener, int)}.
+ *
+ * @throws IllegalStateException if called when menu items have not been laid out.
+ */
+ private void openOverflow() {
+ Preconditions.checkNotNull(mMainPanel);
+ Preconditions.checkNotNull(mOverflowPanel);
+
+ mMainPanel.fadeOut(true);
+ Size overflowPanelSize = mOverflowPanel.measure();
+ final int targetWidth = getOverflowWidth(mParent.getContext());
+ final int targetHeight = overflowPanelSize.getHeight();
+ final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
+ final int startWidth = mContentContainer.getWidth();
+ final int startHeight = mContentContainer.getHeight();
+ final float startY = mContentContainer.getY();
+ final float right = mContentContainer.getX() + mContentContainer.getWidth();
+ Animation widthAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
+ int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+ params.width = startWidth + deltaWidth;
+ mContentContainer.setLayoutParams(params);
+ mContentContainer.setX(right - mContentContainer.getWidth());
+ }
+ };
+ Animation heightAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
+ int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+ params.height = startHeight + deltaHeight;
+ mContentContainer.setLayoutParams(params);
+ if (morphUpwards) {
+ float y = startY - (mContentContainer.getHeight() - startHeight);
+ mContentContainer.setY(y);
+ }
+ }
+ };
+ widthAnimation.setDuration(240);
+ heightAnimation.setDuration(180);
+ heightAnimation.setStartOffset(60);
+ AnimationSet animation = new AnimationSet(true);
+ animation.setAnimationListener(mOnOverflowOpened);
+ animation.addAnimation(widthAnimation);
+ animation.addAnimation(heightAnimation);
+ mContentContainer.startAnimation(animation);
+ }
+
+ /**
+ * Opens the floating toolbar overflow.
+ * This method should not be called if menu items have not been laid out with
+ * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
+ *
+ * @throws IllegalStateException
+ */
+ private void closeOverflow() {
+ Preconditions.checkNotNull(mMainPanel);
+ Preconditions.checkNotNull(mOverflowPanel);
+
+ mOverflowPanel.fadeOut(true);
+ Size mainPanelSize = mMainPanel.measure();
+ final int targetWidth = mainPanelSize.getWidth();
+ final int targetHeight = mainPanelSize.getHeight();
+ final int startWidth = mContentContainer.getWidth();
+ final int startHeight = mContentContainer.getHeight();
+ final float right = mContentContainer.getX() + mContentContainer.getWidth();
+ final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
+ final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
+ Animation widthAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
+ int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+ params.width = startWidth + deltaWidth;
+ mContentContainer.setLayoutParams(params);
+ mContentContainer.setX(right - mContentContainer.getWidth());
+ }
+ };
+ Animation heightAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
+ int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+ params.height = startHeight + deltaHeight;
+ mContentContainer.setLayoutParams(params);
+ if (morphedUpwards) {
+ mContentContainer.setY(bottom - mContentContainer.getHeight());
+ }
+ }
+ };
+ widthAnimation.setDuration(150);
+ widthAnimation.setStartOffset(150);
+ heightAnimation.setDuration(210);
+ AnimationSet animation = new AnimationSet(true);
+ animation.setAnimationListener(mOnOverflowClosed);
+ animation.addAnimation(widthAnimation);
+ animation.addAnimation(heightAnimation);
+ mContentContainer.startAnimation(animation);
+ }
+
+ /**
+ * Prepares the content container for show and update calls.
+ */
+ private void preparePopupContent() {
+ // Do not call this method if main view panel has not been initialized.
+ Preconditions.checkNotNull(mMainPanel);
+
+ // If we're yet to show the popup, set the container visibility to zero.
+ // The "show" animation will make this visible.
+ if (!mPopupWindow.isShowing()) {
+ mContentContainer.setAlpha(0);
+ }
+
+ // Make sure panels are visible.
+ mMainPanel.fadeIn(false);
+ if (mOverflowPanel != null) {
+ mOverflowPanel.fadeIn(false);
+ }
+
+ // Make sure a panel is set as the content.
+ if (mContentContainer.getChildCount() == 0) {
+ mContentContainer.addView(mMainPanel.getView());
+ }
+
+ // Make sure the main panel is at the correct position.
+ if (mContentContainer.getChildAt(0) == mMainPanel.getView()) {
+ mContentContainer.setX(mPadding);
+ float y = mPadding;
+ if (mOverflowDirection == OVERFLOW_DIRECTION_UP) {
+ y = getHeight() - getEstimatedToolbarHeight(mParent.getContext()) - mPadding;
+ }
+ mContentContainer.setY(y);
+ }
+
+ setContentAreaAsTouchableSurface();
+ }
+
+ /**
+ * Sets the current content to be the main view panel.
+ */
+ private void setMainPanelAsContent() {
+ mContentContainer.removeAllViews();
+ Size mainPanelSize = mMainPanel.measure();
+ ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
+ params.width = mainPanelSize.getWidth();
+ params.height = mainPanelSize.getHeight();
+ mContentContainer.setLayoutParams(params);
+ mContentContainer.addView(mMainPanel.getView());
+ }
+
+ private void updatePopupSize() {
+ int width = 0;
+ int height = 0;
+ if (mMainPanel != null) {
+ Size mainPanelSize = mMainPanel.measure();
+ width = mainPanelSize.getWidth();
+ height = mainPanelSize.getHeight();
+ }
+ if (mOverflowPanel != null) {
+ Size overflowPanelSize = mOverflowPanel.measure();
+ width = Math.max(width, overflowPanelSize.getWidth());
+ height = Math.max(height, overflowPanelSize.getHeight());
+ }
+ mPopupWindow.setWidth(width + mPadding * 2);
+ mPopupWindow.setHeight(height + mPadding * 2);
+ }
+
+ /**
+ * 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() {
+ if (!mPopupWindow.isShowing()) {
+ mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ }
+ int width = mContentContainer.getMeasuredWidth();
+ int height = mContentContainer.getMeasuredHeight();
+ mTouchableRegion.set(
+ (int) mContentContainer.getX(),
+ (int) mContentContainer.getY(),
+ (int) mContentContainer.getX() + width,
+ (int) mContentContainer.getY() + height);
+ }
+ }
+
+ /**
+ * A widget that holds the primary menu items in the floating toolbar.
+ */
+ private static final class FloatingToolbarMainPanel {
+
+ private final Context mContext;
+ private final ViewGroup mContentView;
+ private final View.OnClickListener mMenuItemButtonOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (v.getTag() instanceof MenuItem) {
+ if (mOnMenuItemClickListener != null) {
+ mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
+ }
+ }
+ }
+ };
+ private final ViewFader viewFader;
+ private final Runnable mOpenOverflow;
+
+ private View mOpenOverflowButton;
+ private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
+
+ /**
+ * Initializes a floating toolbar popup main view panel.
+ *
+ * @param context
+ * @param openOverflow The code that opens the toolbar popup overflow.
+ */
+ public FloatingToolbarMainPanel(Context context, Runnable openOverflow) {
+ mContext = Preconditions.checkNotNull(context);
+ mContentView = new LinearLayout(context);
+ viewFader = new ViewFader(mContentView);
+ mOpenOverflow = Preconditions.checkNotNull(openOverflow);
+ }
+
/**
- * Sets the popup content container's pivot.
+ * 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.
*/
- private void setPivot() {
- mContentContainer.setPivotX(mContentContainer.getMeasuredWidth() / 2);
- mContentContainer.setPivotY(mContentContainer.getMeasuredHeight());
+ public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int suggestedWidth) {
+ final int toolbarWidth = getAdjustedToolbarWidth(mContext, suggestedWidth)
+ // Reserve space for the "open overflow" button.
+ - getEstimatedOpenOverflowButtonWidth(mContext);
+
+ int availableWidth = toolbarWidth;
+ final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
+
+ mContentView.removeAllViews();
+
+ boolean isFirstItem = true;
+ while (!remainingMenuItems.isEmpty()) {
+ final MenuItem menuItem = remainingMenuItems.peek();
+ Button menuItemButton = createMenuItemButton(mContext, menuItem);
+
+ // Adding additional left padding for the first button to even out button spacing.
+ if (isFirstItem) {
+ menuItemButton.setPadding(
+ 2 * menuItemButton.getPaddingLeft(),
+ menuItemButton.getPaddingTop(),
+ menuItemButton.getPaddingRight(),
+ menuItemButton.getPaddingBottom());
+ isFirstItem = false;
+ }
+
+ // Adding additional right padding for the last button to even out button spacing.
+ if (remainingMenuItems.size() == 1) {
+ menuItemButton.setPadding(
+ menuItemButton.getPaddingLeft(),
+ menuItemButton.getPaddingTop(),
+ 2 * menuItemButton.getPaddingRight(),
+ menuItemButton.getPaddingBottom());
+ }
+
+ menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
+ if (menuItemButtonWidth <= availableWidth) {
+ menuItemButton.setTag(menuItem);
+ menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
+ mContentView.addView(menuItemButton);
+ ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
+ params.width = menuItemButtonWidth;
+ menuItemButton.setLayoutParams(params);
+ availableWidth -= menuItemButtonWidth;
+ remainingMenuItems.pop();
+ } else {
+ if (mOpenOverflowButton == null) {
+ mOpenOverflowButton = (ImageButton) LayoutInflater.from(mContext)
+ .inflate(R.layout.floating_popup_open_overflow_button, null);
+ mOpenOverflowButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOpenOverflowButton != null) {
+ mOpenOverflow.run();
+ }
+ }
+ });
+ }
+ mContentView.addView(mOpenOverflowButton);
+ break;
+ }
+ }
+ return remainingMenuItems;
+ }
+
+ public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
+ mOnMenuItemClickListener = listener;
}
- private static ViewGroup createContentContainer(Context context) {
- return (ViewGroup) LayoutInflater.from(context)
- .inflate(R.layout.floating_popup_container, null);
+ public View getView() {
+ return mContentView;
}
- private static PopupWindow createPopupWindow(View content) {
- ViewGroup popupContentHolder = new LinearLayout(content.getContext());
- PopupWindow popupWindow = new PopupWindow(popupContentHolder);
- popupWindow.setAnimationStyle(0);
- popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
- popupWindow.setWidth(getScreenWidth(content.getContext()));
- popupWindow.setHeight(getScreenHeight(content.getContext()));
- content.setLayoutParams(new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
- popupContentHolder.addView(content);
- return popupWindow;
+ public void fadeIn(boolean animate) {
+ viewFader.fadeIn(animate);
+ }
+
+ public void fadeOut(boolean animate) {
+ viewFader.fadeOut(animate);
}
/**
- * Creates a "grow and fade in from the bottom" animation for the specified view.
+ * Returns how big this panel's view should be.
+ * This method should only be called when the view has not been attached to a parent
+ * otherwise it will throw an illegal state.
+ */
+ public Size measure() throws IllegalStateException {
+ Preconditions.checkState(mContentView.getParent() == null);
+ mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
+ }
+ }
+
+
+ /**
+ * A widget that holds the overflow items in the floating toolbar.
+ */
+ private static final class FloatingToolbarOverflowPanel {
+
+ private final LinearLayout mContentView;
+ private final ViewGroup mBackButtonContainer;
+ private final View mBackButton;
+ private final ListView mListView;
+ private final ViewFader mViewFader;
+ private final Runnable mCloseOverflow;
+
+ private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
+
+ /**
+ * Initializes a floating toolbar popup overflow view panel.
*
- * @param view The view to animate
+ * @param context
+ * @param closeOverflow The code that closes the toolbar popup's overflow.
+ */
+ public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) {
+ mCloseOverflow = Preconditions.checkNotNull(closeOverflow);
+
+ mContentView = new LinearLayout(context);
+ mContentView.setOrientation(LinearLayout.VERTICAL);
+ mViewFader = new ViewFader(mContentView);
+
+ mBackButton = LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_close_overflow_button, null);
+ mBackButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCloseOverflow.run();
+ }
+ });
+ mBackButtonContainer = new LinearLayout(context);
+ mBackButtonContainer.addView(mBackButton);
+
+ mListView = createOverflowListView(context);
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position);
+ if (mOnMenuItemClickListener != null) {
+ mOnMenuItemClickListener.onMenuItemClick(menuItem);
+ }
+ }
+ });
+
+ mContentView.addView(mListView);
+ mContentView.addView(mBackButtonContainer);
+ }
+
+ /**
+ * Sets the menu items to be displayed in the overflow.
*/
- private static AnimatorSet createGrowFadeInFromBottom(View view) {
- AnimatorSet growFadeInFromBottomAnimation = new AnimatorSet();
- growFadeInFromBottomAnimation.playTogether(
- ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125),
- ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125),
- ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75));
- return growFadeInFromBottomAnimation;
+ public void setMenuItems(List<MenuItem> menuItems) {
+ ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter();
+ overflowListViewAdapter.clear();
+ overflowListViewAdapter.addAll(menuItems);
+ setListViewHeight();
+ }
+
+ public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
+ mOnMenuItemClickListener = listener;
}
/**
- * Creates a "shrink and fade out from bottom" animation for the specified view.
+ * Notifies the overflow of the current direction in which the overflow will be opened.
*
- * @param view The view to animate
- * @param listener The animation listener
+ * @param overflowDirection {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP}
+ * or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}.
*/
- private static AnimatorSet createShrinkFadeOutFromBottomAnimation(
- View view, Animator.AnimatorListener listener) {
- AnimatorSet shrinkFadeOutFromBottomAnimation = new AnimatorSet();
- shrinkFadeOutFromBottomAnimation.playTogether(
- ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125),
- ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75));
- shrinkFadeOutFromBottomAnimation.setStartDelay(150);
- shrinkFadeOutFromBottomAnimation.addListener(listener);
- return shrinkFadeOutFromBottomAnimation;
+ public void setOverflowDirection(int overflowDirection) {
+ mContentView.removeView(mBackButtonContainer);
+ int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0;
+ mContentView.addView(mBackButtonContainer, index);
}
/**
- * Returns value, restricted to the range min->max (inclusive).
- * If maximum is less than minimum, the result is undefined.
+ * Returns the content view of the overflow.
+ */
+ public View getView() {
+ return mContentView;
+ }
+
+ public void fadeIn(boolean animate) {
+ mViewFader.fadeIn(animate);
+ }
+
+ public void fadeOut(boolean animate) {
+ mViewFader.fadeOut(animate);
+ }
+
+ /**
+ * Returns how big this panel's view should be.
+ * This method should only be called when the view has not been attached to a parent.
*
- * @param value The value to clamp.
- * @param minimum The minimum value in the range.
- * @param maximum The maximum value in the range. Must not be less than minimum.
+ * @throws IllegalStateException
*/
- private static int clamp(int value, int minimum, int maximum) {
- return Math.max(minimum, Math.min(value, maximum));
+ public Size measure() {
+ Preconditions.checkState(mContentView.getParent() == null);
+ mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
+ }
+
+ private void setListViewHeight() {
+ int itemHeight = getEstimatedToolbarHeight(mContentView.getContext());
+ int height = mListView.getAdapter().getCount() * itemHeight;
+ int maxHeight = mContentView.getContext().getResources().
+ getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
+ ViewGroup.LayoutParams params = mListView.getLayoutParams();
+ params.height = Math.min(height, maxHeight);
+ mListView.setLayoutParams(params);
}
+
+ private static ListView createOverflowListView(final Context context) {
+ final ListView overflowListView = new ListView(context);
+ overflowListView.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ overflowListView.setDivider(null);
+ overflowListView.setDividerHeight(0);
+ final ArrayAdapter overflowListViewAdapter =
+ new ArrayAdapter<MenuItem>(context, 0) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView menuButton;
+ if (convertView != null) {
+ menuButton = (TextView) convertView;
+ } else {
+ menuButton = createOverflowMenuItemButton(context);
+ }
+ MenuItem menuItem = getItem(position);
+ menuButton.setText(menuItem.getTitle());
+ menuButton.setContentDescription(menuItem.getTitle());
+ return menuButton;
+ }
+ };
+ overflowListView.setAdapter(overflowListViewAdapter);
+ return overflowListView;
+ }
+ }
+
+
+ /**
+ * A helper for fading in or out a view.
+ */
+ private static final class ViewFader {
+
+ private static final int FADE_OUT_DURATION = 250;
+ private static final int FADE_IN_DURATION = 150;
+
+ private final View mView;
+ private final ObjectAnimator mFadeOutAnimation;
+ private final ObjectAnimator mFadeInAnimation;
+
+ private ViewFader(View view) {
+ mView = Preconditions.checkNotNull(view);
+ mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0)
+ .setDuration(FADE_OUT_DURATION);
+ mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1)
+ .setDuration(FADE_IN_DURATION);
+ }
+
+ public void fadeIn(boolean animate) {
+ if (animate) {
+ mFadeInAnimation.start();
+ } else {
+ mView.setAlpha(1);
+ }
+ }
+
+ public void fadeOut(boolean animate) {
+ if (animate) {
+ mFadeOutAnimation.start();
+ } else {
+ mView.setAlpha(0);
+ }
+ }
+ }
+
+
+ /**
+ * Creates and returns a menu button for the specified menu item.
+ */
+ private static Button createMenuItemButton(Context context, MenuItem menuItem) {
+ Button menuItemButton = (Button) LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_menu_button, null);
+ menuItemButton.setText(menuItem.getTitle());
+ menuItemButton.setContentDescription(menuItem.getTitle());
+ return menuItemButton;
+ }
+
+ /**
+ * Creates and returns a styled floating toolbar overflow list view item.
+ */
+ private static TextView createOverflowMenuItemButton(Context context) {
+ return (TextView) LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_overflow_list_item, null);
+ }
+
+ private static ViewGroup createContentContainer(Context context) {
+ return (ViewGroup) LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_container, null);
+ }
+
+ private static PopupWindow createPopupWindow(View content) {
+ ViewGroup popupContentHolder = new LinearLayout(content.getContext());
+ PopupWindow popupWindow = new PopupWindow(popupContentHolder);
+ popupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_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 a "grow and fade in from the bottom" animation for the specified view.
+ *
+ * @param view The view to animate
+ */
+ private static AnimatorSet createGrowFadeInFromBottom(View view) {
+ AnimatorSet growFadeInFromBottomAnimation = new AnimatorSet();
+ growFadeInFromBottomAnimation.playTogether(
+ ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125),
+ ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125),
+ ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75));
+ growFadeInFromBottomAnimation.setStartDelay(50);
+ return growFadeInFromBottomAnimation;
+ }
+
+ /**
+ * Creates a "shrink and fade out from bottom" animation for the specified view.
+ *
+ * @param view The view to animate
+ * @param listener The animation listener
+ */
+ private static AnimatorSet createShrinkFadeOutFromBottomAnimation(
+ View view, Animator.AnimatorListener listener) {
+ AnimatorSet shrinkFadeOutFromBottomAnimation = new AnimatorSet();
+ shrinkFadeOutFromBottomAnimation.playTogether(
+ ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125),
+ ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75));
+ shrinkFadeOutFromBottomAnimation.setStartDelay(150);
+ shrinkFadeOutFromBottomAnimation.addListener(listener);
+ return shrinkFadeOutFromBottomAnimation;
+ }
+
+ private static int getOverflowWidth(Context context) {
+ return context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_width);
+ }
+
+ private static int getEstimatedToolbarHeight(Context context) {
+ return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
+ }
+
+ private static int getEstimatedOpenOverflowButtonWidth(Context context) {
+ return context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
+ }
+
+ private static int getAdjustedToolbarWidth(Context context, int width) {
+ if (width <= 0 || width > getScreenWidth(context)) {
+ width = context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_default_width);
+ }
+ return width;
+ }
+
+ /**
+ * Returns the device's screen width.
+ */
+ private static int getScreenWidth(Context context) {
+ return context.getResources().getDisplayMetrics().widthPixels;
+ }
+
+ /**
+ * Returns the device's screen height.
+ */
+ private static int getScreenHeight(Context context) {
+ return context.getResources().getDisplayMetrics().heightPixels;
+ }
+
+ /**
+ * Returns value, restricted to the range min->max (inclusive).
+ * If maximum is less than minimum, the result is undefined.
+ *
+ * @param value The value to clamp.
+ * @param minimum The minimum value in the range.
+ * @param maximum The maximum value in the range. Must not be less than minimum.
+ */
+ private static int clamp(int value, int minimum, int maximum) {
+ return Math.max(minimum, Math.min(value, maximum));
}
}
diff --git a/core/res/res/layout/floating_popup_close_overflow_button.xml b/core/res/res/layout/floating_popup_close_overflow_button.xml
new file mode 100644
index 000000000000..a1d28113dad1
--- /dev/null
+++ b/core/res/res/layout/floating_popup_close_overflow_button.xml
@@ -0,0 +1,24 @@
+<!--
+/* Copyright 2015, 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.
+*/
+-->
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/floating_toolbar_menu_button_minimum_width"
+ android:layout_height="match_parent"
+ android:minWidth="@dimen/floating_toolbar_menu_button_minimum_width"
+ android:minHeight="@dimen/floating_toolbar_height"
+ android:src="?android:attr/actionModeCloseDrawable"
+ android:contentDescription="@string/floating_toolbar_close_overflow_description"
+ android:background="?attr/selectableItemBackgroundBorderless" />
diff --git a/core/res/res/layout/floating_popup_open_overflow_button.xml b/core/res/res/layout/floating_popup_open_overflow_button.xml
index 4c1176c2a4c9..dca538462cc3 100644
--- a/core/res/res/layout/floating_popup_open_overflow_button.xml
+++ b/core/res/res/layout/floating_popup_open_overflow_button.xml
@@ -21,5 +21,5 @@
android:minWidth="@dimen/floating_toolbar_menu_button_minimum_width"
android:minHeight="@dimen/floating_toolbar_height"
android:src="@drawable/ic_menu_moreoverflow_material"
- android:contentDescription="@string/action_menu_overflow_description"
+ android:contentDescription="@string/floating_toolbar_open_overflow_description"
android:background="?attr/selectableItemBackgroundBorderless" />
diff --git a/core/res/res/layout/floating_popup_overflow_list_item b/core/res/res/layout/floating_popup_overflow_list_item
new file mode 100644
index 000000000000..9294f3b1ab4b
--- /dev/null
+++ b/core/res/res/layout/floating_popup_overflow_list_item
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* Copyright 2015, 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.
+*/
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:gravity="center_vertical"
+ android:minWidth="@dimen/floating_toolbar_menu_button_side_padding"
+ android:minHeight="@dimen/floating_toolbar_height"
+ android:paddingLeft="@dimen/floating_toolbar_menu_button_side_padding"
+ android:paddingRight="@dimen/floating_toolbar_menu_button_side_padding"
+ android:paddingTop="0dp"
+ android:paddingBottom="0dp"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:fontFamily="sans-serif"
+ android:textSize="@dimen/floating_toolbar_text_size"
+ android:textAllCaps="true" />
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 7d08e7f62756..50a0d166eeb4 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -392,4 +392,6 @@
<dimen name="floating_toolbar_menu_button_minimum_width">48dp</dimen>
<dimen name="floating_toolbar_default_width">250dp</dimen>
<dimen name="floating_toolbar_minimum_overflow_height">192dp</dimen>
+ <dimen name="floating_toolbar_overflow_width">130dp</dimen>
+ <dimen name="floating_toolbar_margin">2dp</dimen>
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 7dc3ff79d273..a48e9648c7e1 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5237,4 +5237,10 @@
<!-- Model name for USB MIDI Peripheral port -->
<string name="usb_midi_peripheral_model_name">USB Peripheral Port</string>
+ <!-- Floating toolbar strings -->
+ <!-- Content description for the button that opens the floating toolbar overflow. [CHAR LIMIT=NONE] -->
+ <string name="floating_toolbar_open_overflow_description">More options</string>
+ <!-- Content description for the button that closes the floating toolbar overflow. [CHAR LIMIT=NONE] -->
+ <string name="floating_toolbar_close_overflow_description">Close overflow</string>
+
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b4ba3162a785..c2c00b5bdcc3 100755
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2214,12 +2214,16 @@
<java-symbol type="layout" name="floating_popup_container" />
<java-symbol type="layout" name="floating_popup_menu_button" />
<java-symbol type="layout" name="floating_popup_open_overflow_button" />
+ <java-symbol type="layout" name="floating_popup_close_overflow_button" />
+ <java-symbol type="layout" name="floating_popup_overflow_list_item" />
<java-symbol type="dimen" name="floating_toolbar_height" />
<java-symbol type="dimen" name="floating_toolbar_menu_button_side_padding" />
<java-symbol type="dimen" name="floating_toolbar_text_size" />
<java-symbol type="dimen" name="floating_toolbar_menu_button_minimum_width" />
<java-symbol type="dimen" name="floating_toolbar_default_width" />
<java-symbol type="dimen" name="floating_toolbar_minimum_overflow_height" />
+ <java-symbol type="dimen" name="floating_toolbar_overflow_width" />
+ <java-symbol type="dimen" name="floating_toolbar_margin" />
<java-symbol type="drawable" name="ic_chevron_left" />
<java-symbol type="drawable" name="ic_chevron_right" />