blob: 2ce5a0be44e03e34372bdc3c5a01a2262a044f28 [file] [log] [blame]
/*
* Copyright (C) 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.
*/
package com.android.internal.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
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.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewConfiguration;
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.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.util.Preconditions;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
* 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
* the remaining menu items in a vertical overflow view when the overflow button is clicked.
* The horizontal toolbar morphs into the vertical overflow view.
*/
public final class FloatingToolbar {
// This class is responsible for the public API of the floating toolbar.
// It delegates rendering operations to the FloatingToolbarPopup.
public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
item -> false;
private final Context mContext;
private final Window mWindow;
private final FloatingToolbarPopup mPopup;
private final Rect mContentRect = new Rect();
private final Rect mPreviousContentRect = new Rect();
private Menu mMenu;
private List<MenuItem> mShowingMenuItems = new ArrayList<>();
private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
private int mSuggestedWidth;
private boolean mWidthChanged = true;
private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
private final Rect mNewRect = new Rect();
private final Rect mOldRect = new Rect();
@Override
public void onLayoutChange(
View view,
int newLeft, int newRight, int newTop, int newBottom,
int oldLeft, int oldRight, int oldTop, int oldBottom) {
mNewRect.set(newLeft, newRight, newTop, newBottom);
mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
mWidthChanged = true;
updateLayout();
}
}
};
/**
* Sorts the list of menu items to conform to certain requirements.
*/
private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> {
// Ensure the assist menu item is always the first item:
if (menuItem1.getItemId() == android.R.id.textAssist) {
return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1;
}
if (menuItem2.getItemId() == android.R.id.textAssist) {
return 1;
}
// Order by SHOW_AS_ACTION type:
if (menuItem1.requiresActionButton()) {
return menuItem2.requiresActionButton() ? 0 : -1;
}
if (menuItem2.requiresActionButton()) {
return 1;
}
if (menuItem1.requiresOverflow()) {
return menuItem2.requiresOverflow() ? 0 : 1;
}
if (menuItem2.requiresOverflow()) {
return -1;
}
// Order by order value:
return menuItem1.getOrder() - menuItem2.getOrder();
};
/**
* Initializes a floating toolbar.
*/
public FloatingToolbar(Window window) {
// TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
// supports multi-display.
mContext = applyDefaultTheme(window.getContext());
mWindow = Preconditions.checkNotNull(window);
mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
}
/**
* Sets the menu to be shown in this floating toolbar.
* NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
* toolbar.
*/
public FloatingToolbar setMenu(Menu menu) {
mMenu = Preconditions.checkNotNull(menu);
return this;
}
/**
* Sets the custom listener for invocation of menu items in this floating toolbar.
*/
public FloatingToolbar setOnMenuItemClickListener(
MenuItem.OnMenuItemClickListener menuItemClickListener) {
if (menuItemClickListener != null) {
mMenuItemClickListener = menuItemClickListener;
} else {
mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
}
return this;
}
/**
* Sets the content rectangle. This is the area of the interesting content that this toolbar
* should avoid obstructing.
* NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
* toolbar.
*/
public FloatingToolbar setContentRect(Rect rect) {
mContentRect.set(Preconditions.checkNotNull(rect));
return this;
}
/**
* Sets the suggested width of this floating toolbar.
* The actual width will be about this size but there are no guarantees that it will be exactly
* the suggested width.
* NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
* 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;
}
/**
* Shows this floating toolbar.
*/
public FloatingToolbar show() {
registerOrientationHandler();
doShow();
return this;
}
/**
* Updates this floating toolbar to reflect recent position and view updates.
* NOTE: This method is a no-op if the toolbar isn't showing.
*/
public FloatingToolbar updateLayout() {
if (mPopup.isShowing()) {
doShow();
}
return this;
}
/**
* Dismisses this floating toolbar.
*/
public void dismiss() {
unregisterOrientationHandler();
mPopup.dismiss();
}
/**
* Hides this floating toolbar. This is a no-op if the toolbar is not showing.
* Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
*/
public void hide() {
mPopup.hide();
}
/**
* Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
*/
public boolean isShowing() {
return mPopup.isShowing();
}
/**
* Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
*/
public boolean isHidden() {
return mPopup.isHidden();
}
private void doShow() {
List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
menuItems.sort(mMenuItemComparator);
if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
mPopup.dismiss();
mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
mShowingMenuItems = menuItems;
}
if (!mPopup.isShowing()) {
mPopup.show(mContentRect);
} else if (!mPreviousContentRect.equals(mContentRect)) {
mPopup.updateCoordinates(mContentRect);
}
mWidthChanged = false;
mPreviousContentRect.set(mContentRect);
}
/**
* Returns true if this floating toolbar is currently showing the specified menu items.
*/
private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) {
return false;
}
final int size = menuItems.size();
for (int i = 0; i < size; i++) {
final MenuItem menuItem = menuItems.get(i);
final MenuItem showingItem = mShowingMenuItems.get(i);
if (menuItem.getItemId() != showingItem.getItemId()
|| !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle())
|| !Objects.equals(menuItem.getIcon(), showingItem.getIcon())
|| menuItem.getGroupId() != showingItem.getGroupId()) {
return false;
}
}
return true;
}
/**
* Returns the visible and enabled menu items in the specified menu.
* This method is recursive.
*/
private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
List<MenuItem> menuItems = new ArrayList<>();
for (int i = 0; (menu != null) && (i < menu.size()); i++) {
MenuItem menuItem = menu.getItem(i);
if (menuItem.isVisible() && menuItem.isEnabled()) {
Menu subMenu = menuItem.getSubMenu();
if (subMenu != null) {
menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
} else {
menuItems.add(menuItem);
}
}
}
return menuItems;
}
private void registerOrientationHandler() {
unregisterOrientationHandler();
mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
}
private void unregisterOrientationHandler() {
mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
}
/**
* A popup window used by the floating toolbar.
*
* 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.
*/
private static final class 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 it's 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.
private final OverflowPanel mOverflowPanel; // holds menu items hidden in the overflow.
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;
/* Item click listeners */
private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
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 boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards.
private boolean mIsOverflowOpen;
private int mTransitionDurationScale; // Used to scale the toolbar transition duration.
/**
* Initializes a new floating toolbar popup.
*
* @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
* from.
*/
public FloatingToolbarPopup(Context context, View parent) {
mParent = Preconditions.checkNotNull(parent);
mContext = Preconditions.checkNotNull(context);
mContentContainer = createContentContainer(context);
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_menu_button_side_padding);
// 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);
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();
}
});
}
/**
* Lays out buttons for the specified menu items.
* Requires a subsequent call to {@link #show()} to show the items.
*/
public void layoutMenuItems(
List<MenuItem> menuItems,
MenuItem.OnMenuItemClickListener menuItemClickListener,
int suggestedWidth) {
mOnMenuItemClickListener = menuItemClickListener;
cancelOverflowAnimations();
clearPanels();
menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
if (!menuItems.isEmpty()) {
// Add remaining items to the overflow.
layoutOverflowPanelItems(menuItems);
}
updatePopupSize();
}
/**
* Shows this popup at the specified coordinates.
* The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
*/
public void show(Rect contentRectOnScreen) {
Preconditions.checkNotNull(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.setLayoutInScreenEnabled(true) so that we can
// specify the popup position in screen coordinates.
mPopupWindow.showAtLocation(
mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
setTouchableSurfaceInsetsComputer();
runShowAnimation();
}
/**
* Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
*/
public void dismiss() {
if (mDismissed) {
return;
}
mHidden = false;
mDismissed = true;
mHideAnimation.cancel();
runDismissAnimation();
setZeroTouchableSurface();
}
/**
* Hides this popup. This is a no-op if this popup is not showing.
* Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
*/
public void hide() {
if (!isShowing()) {
return;
}
mHidden = true;
runHideAnimation();
setZeroTouchableSurface();
}
/**
* Returns {@code true} if this popup is currently showing. {@code false} otherwise.
*/
public boolean isShowing() {
return !mDismissed && !mHidden;
}
/**
* Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
*/
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.
*/
public void updateCoordinates(Rect contentRectOnScreen) {
Preconditions.checkNotNull(contentRectOnScreen);
if (!isShowing() || !mPopupWindow.isShowing()) {
return;
}
cancelOverflowAnimations();
refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
preparePopupContent();
// We need to specify the position in window coordinates.
// TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(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 coordiantes, 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.setLayoutInScreenEnabled(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() {
Preconditions.checkNotNull(mMainPanelSize);
final int width;
final int height;
if (mIsOverflowOpen) {
Preconditions.checkNotNull(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) {
Preconditions.checkNotNull(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);
// 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);
final boolean isNewGroup = !isFirstItem && lastGroupId != menuItem.getGroupId();
final int extraPadding = isNewGroup ? menuItemButton.getPaddingEnd() * 2 : 0;
// Check if we can fit an item while reserving space for the overflowButton.
final boolean canFitWithOverflow =
menuItemButtonWidth <=
availableWidth - mOverflowButtonSize.getWidth() - extraPadding;
final boolean canFitNoOverflow =
isLastItem && menuItemButtonWidth <= availableWidth - extraPadding;
if (canFitWithOverflow || canFitNoOverflow) {
if (isNewGroup) {
final View divider = createDivider(mContext);
final int dividerWidth = divider.getLayoutParams().width;
// Add extra padding to the end of the previous button.
// Half of the extra padding (less borderWidth) goes to the previous button.
final View previousButton = mMainPanel.getChildAt(
mMainPanel.getChildCount() - 1);
final int prevPaddingEnd = previousButton.getPaddingEnd()
+ extraPadding / 2 - dividerWidth;
previousButton.setPaddingRelative(
previousButton.getPaddingStart(),
previousButton.getPaddingTop(),
prevPaddingEnd,
previousButton.getPaddingBottom());
final ViewGroup.LayoutParams prevParams = previousButton.getLayoutParams();
prevParams.width += extraPadding / 2 - dividerWidth;
previousButton.setLayoutParams(prevParams);
// Add extra padding to the start of this button.
// Other half of the extra padding goes to this button.
final int paddingStart = menuItemButton.getPaddingStart()
+ extraPadding / 2;
menuItemButton.setPaddingRelative(
paddingStart,
menuItemButton.getPaddingTop(),
menuItemButton.getPaddingEnd(),
menuItemButton.getPaddingBottom());
// Include a divider.
mMainPanel.addView(divider);
}
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 + extraPadding / 2;
menuItemButton.setLayoutParams(params);
availableWidth -= menuItemButtonWidth + extraPadding;
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(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 it's coordinates with respect to it's 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 FloatingToolbarPopup mPopup;
OverflowPanel(FloatingToolbarPopup popup) {
super(Preconditions.checkNotNull(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 it's 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;
public OverflowPanelViewHelper(Context context) {
mContext = Preconditions.checkNotNull(context);
mIconTextSpacing = context.getResources()
.getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_side_padding);
mSidePadding = context.getResources()
.getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
mCalculator = createMenuButton(null);
}
public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
Preconditions.checkNotNull(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_TAG);
contentContainer.setClipToOutline(true);
return contentContainer;
}
private static PopupWindow createPopupWindow(ViewGroup content) {
ViewGroup popupContentHolder = new LinearLayout(content.getContext());
PopupWindow popupWindow = new PopupWindow(popupContentHolder);
// TODO: Use .setLayoutInScreenEnabled(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;
}
private static View createDivider(Context context) {
// TODO: Inflate this instead.
View divider = new View(context);
int _1dp = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
_1dp, ViewGroup.LayoutParams.MATCH_PARENT);
params.setMarginsRelative(0, _1dp * 10, 0, _1dp * 10);
divider.setLayoutParams(params);
TypedArray a = context.obtainStyledAttributes(
new TypedValue().data, new int[] { R.attr.floatingToolbarDividerColor });
divider.setBackgroundColor(a.getColor(0, 0));
a.recycle();
divider.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
divider.setEnabled(false);
divider.setFocusable(false);
divider.setContentDescription(null);
return divider;
}
/**
* 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);
}
}