| /* |
| * 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.Transformation; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.Interpolator; |
| 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 java.util.ArrayList; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| import com.android.internal.R; |
| import com.android.internal.util.Preconditions; |
| 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(); |
| } |
| } |
| }; |
| |
| /** |
| * Initializes a floating toolbar. |
| */ |
| public FloatingToolbar(Context context, Window window) { |
| mContext = applyDefaultTheme(Preconditions.checkNotNull(context)); |
| 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); |
| tidy(menuItems); |
| 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; |
| } |
| |
| /** |
| * Update the list of menu items to conform to certain requirements. |
| */ |
| private void tidy(List<MenuItem> menuItems) { |
| int assistItemIndex = -1; |
| Drawable assistItemDrawable = null; |
| |
| final int size = menuItems.size(); |
| for (int i = 0; i < size; i++) { |
| final MenuItem menuItem = menuItems.get(i); |
| |
| if (menuItem.getItemId() == android.R.id.textAssist) { |
| assistItemIndex = i; |
| assistItemDrawable = menuItem.getIcon(); |
| } |
| |
| // Remove icons for all menu items with text. |
| if (!TextUtils.isEmpty(menuItem.getTitle())) { |
| menuItem.setIcon(null); |
| } |
| } |
| if (assistItemIndex > -1) { |
| final MenuItem assistMenuItem = menuItems.remove(assistItemIndex); |
| // Ensure the assist menu item preserves its icon. |
| assistMenuItem.setIcon(assistItemDrawable); |
| // Ensure the assist menu item is always the first item. |
| menuItems.add(0, assistMenuItem); |
| } |
| } |
| |
| 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<MenuItem>(menuItems); |
| |
| mMainPanel.removeAllViews(); |
| mMainPanel.setPaddingRelative(0, 0, 0, 0); |
| |
| int lastGroupId = -1; |
| boolean isFirstItem = true; |
| while (!remainingMenuItems.isEmpty()) { |
| final MenuItem menuItem = remainingMenuItems.peek(); |
| View menuItemButton = createMenuItemButton(mContext, menuItem, mIconTextSpacing); |
| |
| // 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. |
| boolean canFitWithOverflow = |
| menuItemButtonWidth <= |
| availableWidth - mOverflowButtonSize.getWidth() - extraPadding; |
| 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. |
| 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 { |
| // Reserve space for overflowButton. |
| mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0); |
| break; |
| } |
| lastGroupId = menuItem.getGroupId(); |
| isFirstItem = false; |
| } |
| 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); |
| } else { |
| convertView = createMenuButton(menuItem); |
| } |
| convertView.setMinimumWidth(minimumWidth); |
| return convertView; |
| } |
| |
| public int calculateWidth(MenuItem menuItem) { |
| updateMenuItemButton(mCalculator, menuItem, mIconTextSpacing); |
| mCalculator.measure( |
| View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); |
| return mCalculator.getMeasuredWidth(); |
| } |
| |
| private View createMenuButton(MenuItem menuItem) { |
| View button = createMenuItemButton(mContext, menuItem, mIconTextSpacing); |
| button.setPadding(mSidePadding, 0, mSidePadding, 0); |
| return button; |
| } |
| } |
| } |
| |
| /** |
| * Creates and returns a menu button for the specified menu item. |
| */ |
| private static View createMenuItemButton( |
| Context context, MenuItem menuItem, int iconTextSpacing) { |
| final View menuItemButton = LayoutInflater.from(context) |
| .inflate(R.layout.floating_popup_menu_button, null); |
| if (menuItem != null) { |
| updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing); |
| } |
| return menuItemButton; |
| } |
| |
| /** |
| * Updates the specified menu item button with the specified menu item data. |
| */ |
| private static void updateMenuItemButton( |
| View menuItemButton, MenuItem menuItem, int iconTextSpacing) { |
| final TextView buttonText = (TextView) menuItemButton.findViewById( |
| R.id.floating_toolbar_menu_item_text); |
| if (TextUtils.isEmpty(menuItem.getTitle())) { |
| buttonText.setVisibility(View.GONE); |
| } else { |
| buttonText.setVisibility(View.VISIBLE); |
| buttonText.setText(menuItem.getTitle()); |
| } |
| final ImageView buttonIcon = (ImageView) menuItemButton |
| .findViewById(R.id.floating_toolbar_menu_item_image); |
| if (menuItem.getIcon() == null) { |
| 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); |
| 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_Material_Light : R.style.Theme_Material; |
| a.recycle(); |
| return new ContextThemeWrapper(originalContext, themeId); |
| } |
| } |