1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.widget;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.ValueAnimator;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.res.TypedArray;
27 import android.graphics.Color;
28 import android.graphics.Point;
29 import android.graphics.Rect;
30 import android.graphics.Region;
31 import android.graphics.drawable.AnimatedVectorDrawable;
32 import android.graphics.drawable.ColorDrawable;
33 import android.graphics.drawable.Drawable;
34 import android.text.TextUtils;
35 import android.util.Size;
36 import android.view.ContextThemeWrapper;
37 import android.view.Gravity;
38 import android.view.LayoutInflater;
39 import android.view.Menu;
40 import android.view.MenuItem;
41 import android.view.MotionEvent;
42 import android.view.View;
43 import android.view.View.MeasureSpec;
44 import android.view.View.OnLayoutChangeListener;
45 import android.view.ViewConfiguration;
46 import android.view.ViewGroup;
47 import android.view.ViewTreeObserver;
48 import android.view.Window;
49 import android.view.WindowManager;
50 import android.view.animation.Animation;
51 import android.view.animation.AnimationSet;
52 import android.view.animation.AnimationUtils;
53 import android.view.animation.Interpolator;
54 import android.view.animation.Transformation;
55 import android.widget.ArrayAdapter;
56 import android.widget.ImageButton;
57 import android.widget.ImageView;
58 import android.widget.LinearLayout;
59 import android.widget.ListView;
60 import android.widget.PopupWindow;
61 import android.widget.TextView;
62 
63 import com.android.internal.R;
64 import com.android.internal.annotations.VisibleForTesting;
65 import com.android.internal.util.Preconditions;
66 
67 import java.util.ArrayList;
68 import java.util.Collection;
69 import java.util.Comparator;
70 import java.util.Iterator;
71 import java.util.LinkedHashMap;
72 import java.util.LinkedList;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.Objects;
76 
77 /**
78  * A floating toolbar for showing contextual menu items.
79  * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
80  * the remaining menu items in a vertical overflow view when the overflow button is clicked.
81  * The horizontal toolbar morphs into the vertical overflow view.
82  */
83 public final class FloatingToolbar {
84 
85     // This class is responsible for the public API of the floating toolbar.
86     // It delegates rendering operations to the FloatingToolbarPopup.
87 
88     public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
89 
90     private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
91             item -> false;
92 
93     private final Context mContext;
94     private final Window mWindow;
95     private final FloatingToolbarPopup mPopup;
96 
97     private final Rect mContentRect = new Rect();
98     private final Rect mPreviousContentRect = new Rect();
99 
100     private Menu mMenu;
101     private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
102 
103     private int mSuggestedWidth;
104     private boolean mWidthChanged = true;
105 
106     private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
107 
108         private final Rect mNewRect = new Rect();
109         private final Rect mOldRect = new Rect();
110 
111         @Override
112         public void onLayoutChange(
113                 View view,
114                 int newLeft, int newRight, int newTop, int newBottom,
115                 int oldLeft, int oldRight, int oldTop, int oldBottom) {
116             mNewRect.set(newLeft, newRight, newTop, newBottom);
117             mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
118             if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
119                 mWidthChanged = true;
120                 updateLayout();
121             }
122         }
123     };
124 
125     /**
126      * Sorts the list of menu items to conform to certain requirements.
127      */
128     private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> {
129         // Ensure the assist menu item is always the first item:
130         if (menuItem1.getItemId() == android.R.id.textAssist) {
131             return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1;
132         }
133         if (menuItem2.getItemId() == android.R.id.textAssist) {
134             return 1;
135         }
136 
137         // Order by SHOW_AS_ACTION type:
138         if (menuItem1.requiresActionButton()) {
139             return menuItem2.requiresActionButton() ? 0 : -1;
140         }
141         if (menuItem2.requiresActionButton()) {
142             return 1;
143         }
144         if (menuItem1.requiresOverflow()) {
145             return menuItem2.requiresOverflow() ? 0 : 1;
146         }
147         if (menuItem2.requiresOverflow()) {
148             return -1;
149         }
150 
151         // Order by order value:
152         return menuItem1.getOrder() - menuItem2.getOrder();
153     };
154 
155     /**
156      * Initializes a floating toolbar.
157      */
FloatingToolbar(Window window)158     public FloatingToolbar(Window window) {
159         // TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
160         // supports multi-display.
161         mContext = applyDefaultTheme(window.getContext());
162         mWindow = Objects.requireNonNull(window);
163         mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
164     }
165 
166     /**
167      * Sets the menu to be shown in this floating toolbar.
168      * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
169      * toolbar.
170      */
setMenu(Menu menu)171     public FloatingToolbar setMenu(Menu menu) {
172         mMenu = Objects.requireNonNull(menu);
173         return this;
174     }
175 
176     /**
177      * Sets the custom listener for invocation of menu items in this floating toolbar.
178      */
setOnMenuItemClickListener( MenuItem.OnMenuItemClickListener menuItemClickListener)179     public FloatingToolbar setOnMenuItemClickListener(
180             MenuItem.OnMenuItemClickListener menuItemClickListener) {
181         if (menuItemClickListener != null) {
182             mMenuItemClickListener = menuItemClickListener;
183         } else {
184             mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
185         }
186         return this;
187     }
188 
189     /**
190      * Sets the content rectangle. This is the area of the interesting content that this toolbar
191      * should avoid obstructing.
192      * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
193      * toolbar.
194      */
setContentRect(Rect rect)195     public FloatingToolbar setContentRect(Rect rect) {
196         mContentRect.set(Objects.requireNonNull(rect));
197         return this;
198     }
199 
200     /**
201      * Sets the suggested width of this floating toolbar.
202      * The actual width will be about this size but there are no guarantees that it will be exactly
203      * the suggested width.
204      * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
205      * toolbar.
206      */
setSuggestedWidth(int suggestedWidth)207     public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
208         // Check if there's been a substantial width spec change.
209         int difference = Math.abs(suggestedWidth - mSuggestedWidth);
210         mWidthChanged = difference > (mSuggestedWidth * 0.2);
211 
212         mSuggestedWidth = suggestedWidth;
213         return this;
214     }
215 
216     /**
217      * Shows this floating toolbar.
218      */
show()219     public FloatingToolbar show() {
220         registerOrientationHandler();
221         doShow();
222         return this;
223     }
224 
225     /**
226      * Updates this floating toolbar to reflect recent position and view updates.
227      * NOTE: This method is a no-op if the toolbar isn't showing.
228      */
updateLayout()229     public FloatingToolbar updateLayout() {
230         if (mPopup.isShowing()) {
231             doShow();
232         }
233         return this;
234     }
235 
236     /**
237      * Dismisses this floating toolbar.
238      */
dismiss()239     public void dismiss() {
240         unregisterOrientationHandler();
241         mPopup.dismiss();
242     }
243 
244     /**
245      * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
246      * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
247      */
hide()248     public void hide() {
249         mPopup.hide();
250     }
251 
252     /**
253      * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
254      */
isShowing()255     public boolean isShowing() {
256         return mPopup.isShowing();
257     }
258 
259     /**
260      * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
261      */
isHidden()262     public boolean isHidden() {
263         return mPopup.isHidden();
264     }
265 
266     /**
267      * If this is set to true, the action mode view will dismiss itself on touch events outside of
268      * its window. The setting takes effect immediately.
269      *
270      * @param outsideTouchable whether or not this action mode is "outside touchable"
271      * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
272      */
setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)273     public void setOutsideTouchable(
274             boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
275         mPopup.setOutsideTouchable(outsideTouchable, onDismiss);
276     }
277 
doShow()278     private void doShow() {
279         List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
280         menuItems.sort(mMenuItemComparator);
281         if (mPopup.isLayoutRequired(menuItems) || mWidthChanged) {
282             mPopup.dismiss();
283             mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
284         } else {
285             mPopup.updateMenuItems(menuItems, mMenuItemClickListener);
286         }
287         if (!mPopup.isShowing()) {
288             mPopup.show(mContentRect);
289         } else if (!mPreviousContentRect.equals(mContentRect)) {
290             mPopup.updateCoordinates(mContentRect);
291         }
292         mWidthChanged = false;
293         mPreviousContentRect.set(mContentRect);
294     }
295 
296     /**
297      * Returns the visible and enabled menu items in the specified menu.
298      * This method is recursive.
299      */
getVisibleAndEnabledMenuItems(Menu menu)300     private static List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
301         List<MenuItem> menuItems = new ArrayList<>();
302         for (int i = 0; (menu != null) && (i < menu.size()); i++) {
303             MenuItem menuItem = menu.getItem(i);
304             if (menuItem.isVisible() && menuItem.isEnabled()) {
305                 Menu subMenu = menuItem.getSubMenu();
306                 if (subMenu != null) {
307                     menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
308                 } else {
309                     menuItems.add(menuItem);
310                 }
311             }
312         }
313         return menuItems;
314     }
315 
registerOrientationHandler()316     private void registerOrientationHandler() {
317         unregisterOrientationHandler();
318         mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
319     }
320 
unregisterOrientationHandler()321     private void unregisterOrientationHandler() {
322         mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
323     }
324 
325 
326     /**
327      * A popup window used by the floating toolbar.
328      *
329      * This class is responsible for the rendering/animation of the floating toolbar.
330      * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
331      * to transition between panels.
332      */
333     private static final class FloatingToolbarPopup {
334 
335         /* Minimum and maximum number of items allowed in the overflow. */
336         private static final int MIN_OVERFLOW_SIZE = 2;
337         private static final int MAX_OVERFLOW_SIZE = 4;
338 
339         private final Context mContext;
340         private final View mParent;  // Parent for the popup window.
341         private final PopupWindow mPopupWindow;
342 
343         /* Margins between the popup window and it's content. */
344         private final int mMarginHorizontal;
345         private final int mMarginVertical;
346 
347         /* View components */
348         private final ViewGroup mContentContainer;  // holds all contents.
349         private final ViewGroup mMainPanel;  // holds menu items that are initially displayed.
350         private final OverflowPanel mOverflowPanel;  // holds menu items hidden in the overflow.
351         private final ImageButton mOverflowButton;  // opens/closes the overflow.
352         /* overflow button drawables. */
353         private final Drawable mArrow;
354         private final Drawable mOverflow;
355         private final AnimatedVectorDrawable mToArrow;
356         private final AnimatedVectorDrawable mToOverflow;
357 
358         private final OverflowPanelViewHelper mOverflowPanelViewHelper;
359 
360         /* Animation interpolators. */
361         private final Interpolator mLogAccelerateInterpolator;
362         private final Interpolator mFastOutSlowInInterpolator;
363         private final Interpolator mLinearOutSlowInInterpolator;
364         private final Interpolator mFastOutLinearInInterpolator;
365 
366         /* Animations. */
367         private final AnimatorSet mShowAnimation;
368         private final AnimatorSet mDismissAnimation;
369         private final AnimatorSet mHideAnimation;
370         private final AnimationSet mOpenOverflowAnimation;
371         private final AnimationSet mCloseOverflowAnimation;
372         private final Animation.AnimationListener mOverflowAnimationListener;
373 
374         private final Rect mViewPortOnScreen = new Rect();  // portion of screen we can draw in.
375         private final Point mCoordsOnWindow = new Point();  // popup window coordinates.
376         /* Temporary data holders. Reset values before using. */
377         private final int[] mTmpCoords = new int[2];
378 
379         private final Region mTouchableRegion = new Region();
380         private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
381                 info -> {
382                     info.contentInsets.setEmpty();
383                     info.visibleInsets.setEmpty();
384                     info.touchableRegion.set(mTouchableRegion);
385                     info.setTouchableInsets(
386                             ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
387                 };
388 
389         private final int mLineHeight;
390         private final int mIconTextSpacing;
391 
392         /**
393          * @see OverflowPanelViewHelper#preparePopupContent().
394          */
395         private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
396             @Override
397             public void run() {
398                 setPanelsStatesAtRestingPosition();
399                 setContentAreaAsTouchableSurface();
400                 mContentContainer.setAlpha(1);
401             }
402         };
403 
404         private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
405         private boolean mHidden; // tracks whether this popup is hidden or hiding.
406 
407         /* Calculated sizes for panels and overflow button. */
408         private final Size mOverflowButtonSize;
409         private Size mOverflowPanelSize;  // Should be null when there is no overflow.
410         private Size mMainPanelSize;
411 
412         /* Menu items and click listeners */
413         private final Map<MenuItemRepr, MenuItem> mMenuItems = new LinkedHashMap<>();
414         private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
415         private final View.OnClickListener mMenuItemButtonOnClickListener =
416                 new View.OnClickListener() {
417                     @Override
418                     public void onClick(View v) {
419                         if (mOnMenuItemClickListener == null) {
420                             return;
421                         }
422                         final Object tag = v.getTag();
423                         if (!(tag instanceof MenuItemRepr)) {
424                             return;
425                         }
426                         final MenuItem menuItem = mMenuItems.get((MenuItemRepr) tag);
427                         if (menuItem == null) {
428                             return;
429                         }
430                         mOnMenuItemClickListener.onMenuItemClick(menuItem);
431                     }
432                 };
433 
434         private boolean mOpenOverflowUpwards;  // Whether the overflow opens upwards or downwards.
435         private boolean mIsOverflowOpen;
436 
437         private int mTransitionDurationScale;  // Used to scale the toolbar transition duration.
438 
439         /**
440          * Initializes a new floating toolbar popup.
441          *
442          * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
443          *      from.
444          */
FloatingToolbarPopup(Context context, View parent)445         public FloatingToolbarPopup(Context context, View parent) {
446             mParent = Objects.requireNonNull(parent);
447             mContext = Objects.requireNonNull(context);
448             mContentContainer = createContentContainer(context);
449             mPopupWindow = createPopupWindow(mContentContainer);
450             mMarginHorizontal = parent.getResources()
451                     .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
452             mMarginVertical = parent.getResources()
453                     .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
454             mLineHeight = context.getResources()
455                     .getDimensionPixelSize(R.dimen.floating_toolbar_height);
456             mIconTextSpacing = context.getResources()
457                     .getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
458 
459             // Interpolators
460             mLogAccelerateInterpolator = new LogAccelerateInterpolator();
461             mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
462                     mContext, android.R.interpolator.fast_out_slow_in);
463             mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
464                     mContext, android.R.interpolator.linear_out_slow_in);
465             mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
466                     mContext, android.R.interpolator.fast_out_linear_in);
467 
468             // Drawables. Needed for views.
469             mArrow = mContext.getResources()
470                     .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
471             mArrow.setAutoMirrored(true);
472             mOverflow = mContext.getResources()
473                     .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
474             mOverflow.setAutoMirrored(true);
475             mToArrow = (AnimatedVectorDrawable) mContext.getResources()
476                     .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
477             mToArrow.setAutoMirrored(true);
478             mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
479                     .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
480             mToOverflow.setAutoMirrored(true);
481 
482             // Views
483             mOverflowButton = createOverflowButton();
484             mOverflowButtonSize = measure(mOverflowButton);
485             mMainPanel = createMainPanel();
486             mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext, mIconTextSpacing);
487             mOverflowPanel = createOverflowPanel();
488 
489             // Animation. Need views.
490             mOverflowAnimationListener = createOverflowAnimationListener();
491             mOpenOverflowAnimation = new AnimationSet(true);
492             mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
493             mCloseOverflowAnimation = new AnimationSet(true);
494             mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
495             mShowAnimation = createEnterAnimation(mContentContainer);
496             mDismissAnimation = createExitAnimation(
497                     mContentContainer,
498                     150,  // startDelay
499                     new AnimatorListenerAdapter() {
500                         @Override
501                         public void onAnimationEnd(Animator animation) {
502                             mPopupWindow.dismiss();
503                             mContentContainer.removeAllViews();
504                         }
505                     });
506             mHideAnimation = createExitAnimation(
507                     mContentContainer,
508                     0,  // startDelay
509                     new AnimatorListenerAdapter() {
510                         @Override
511                         public void onAnimationEnd(Animator animation) {
512                             mPopupWindow.dismiss();
513                         }
514                     });
515         }
516 
517         /**
518          * Makes this toolbar "outside touchable" and sets the onDismissListener.
519          *
520          * @param outsideTouchable if true, the popup will be made "outside touchable" and
521          *      "non focusable". The reverse will happen if false.
522          * @param onDismiss
523          *
524          * @return true if the "outsideTouchable" setting was modified. Otherwise returns false
525          *
526          * @see PopupWindow#setOutsideTouchable(boolean)
527          * @see PopupWindow#setFocusable(boolean)
528          * @see PopupWindow.OnDismissListener
529          */
setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)530         public boolean setOutsideTouchable(
531                 boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
532             boolean ret = false;
533             if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) {
534                 mPopupWindow.setOutsideTouchable(outsideTouchable);
535                 mPopupWindow.setFocusable(!outsideTouchable);
536                 mPopupWindow.update();
537                 ret = true;
538             }
539             mPopupWindow.setOnDismissListener(onDismiss);
540             return ret;
541         }
542 
543         /**
544          * Lays out buttons for the specified menu items.
545          * Requires a subsequent call to {@link #show()} to show the items.
546          */
layoutMenuItems( List<MenuItem> menuItems, MenuItem.OnMenuItemClickListener menuItemClickListener, int suggestedWidth)547         public void layoutMenuItems(
548                 List<MenuItem> menuItems,
549                 MenuItem.OnMenuItemClickListener menuItemClickListener,
550                 int suggestedWidth) {
551             cancelOverflowAnimations();
552             clearPanels();
553             updateMenuItems(menuItems, menuItemClickListener);
554             menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
555             if (!menuItems.isEmpty()) {
556                 // Add remaining items to the overflow.
557                 layoutOverflowPanelItems(menuItems);
558             }
559             updatePopupSize();
560         }
561 
562         /**
563          * Updates the popup's menu items without rebuilding the widget.
564          * Use in place of layoutMenuItems() when the popup's views need not be reconstructed.
565          *
566          * @see isLayoutRequired(List<MenuItem>)
567          */
updateMenuItems( List<MenuItem> menuItems, MenuItem.OnMenuItemClickListener menuItemClickListener)568         public void updateMenuItems(
569                 List<MenuItem> menuItems, MenuItem.OnMenuItemClickListener menuItemClickListener) {
570             mMenuItems.clear();
571             for (MenuItem menuItem : menuItems) {
572                 mMenuItems.put(MenuItemRepr.of(menuItem), menuItem);
573             }
574             mOnMenuItemClickListener = menuItemClickListener;
575         }
576 
577         /**
578          * Returns true if this popup needs a relayout to properly render the specified menu items.
579          */
isLayoutRequired(List<MenuItem> menuItems)580         public boolean isLayoutRequired(List<MenuItem> menuItems) {
581             return !MenuItemRepr.reprEquals(menuItems, mMenuItems.values());
582         }
583 
584         /**
585          * Shows this popup at the specified coordinates.
586          * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
587          */
show(Rect contentRectOnScreen)588         public void show(Rect contentRectOnScreen) {
589             Objects.requireNonNull(contentRectOnScreen);
590 
591             if (isShowing()) {
592                 return;
593             }
594 
595             mHidden = false;
596             mDismissed = false;
597             cancelDismissAndHideAnimations();
598             cancelOverflowAnimations();
599 
600             refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
601             preparePopupContent();
602             // We need to specify the position in window coordinates.
603             // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
604             // specify the popup position in screen coordinates.
605             mPopupWindow.showAtLocation(
606                     mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
607             setTouchableSurfaceInsetsComputer();
608             runShowAnimation();
609         }
610 
611         /**
612          * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
613          */
dismiss()614         public void dismiss() {
615             if (mDismissed) {
616                 return;
617             }
618 
619             mHidden = false;
620             mDismissed = true;
621             mHideAnimation.cancel();
622 
623             runDismissAnimation();
624             setZeroTouchableSurface();
625         }
626 
627         /**
628          * Hides this popup. This is a no-op if this popup is not showing.
629          * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
630          */
hide()631         public void hide() {
632             if (!isShowing()) {
633                 return;
634             }
635 
636             mHidden = true;
637             runHideAnimation();
638             setZeroTouchableSurface();
639         }
640 
641         /**
642          * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
643          */
isShowing()644         public boolean isShowing() {
645             return !mDismissed && !mHidden;
646         }
647 
648         /**
649          * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
650          */
isHidden()651         public boolean isHidden() {
652             return mHidden;
653         }
654 
655         /**
656          * Updates the coordinates of this popup.
657          * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
658          * This is a no-op if this popup is not showing.
659          */
updateCoordinates(Rect contentRectOnScreen)660         public void updateCoordinates(Rect contentRectOnScreen) {
661             Objects.requireNonNull(contentRectOnScreen);
662 
663             if (!isShowing() || !mPopupWindow.isShowing()) {
664                 return;
665             }
666 
667             cancelOverflowAnimations();
668             refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
669             preparePopupContent();
670             // We need to specify the position in window coordinates.
671             // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
672             // specify the popup position in screen coordinates.
673             mPopupWindow.update(
674                     mCoordsOnWindow.x, mCoordsOnWindow.y,
675                     mPopupWindow.getWidth(), mPopupWindow.getHeight());
676         }
677 
refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen)678         private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
679             refreshViewPort();
680 
681             // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
682             // landscape.
683             final int x = Math.min(
684                     contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
685                     mViewPortOnScreen.right - mPopupWindow.getWidth());
686 
687             final int y;
688 
689             final int availableHeightAboveContent =
690                     contentRectOnScreen.top - mViewPortOnScreen.top;
691             final int availableHeightBelowContent =
692                     mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
693 
694             final int margin = 2 * mMarginVertical;
695             final int toolbarHeightWithVerticalMargin = mLineHeight + margin;
696 
697             if (!hasOverflow()) {
698                 if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
699                     // There is enough space at the top of the content.
700                     y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
701                 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
702                     // There is enough space at the bottom of the content.
703                     y = contentRectOnScreen.bottom;
704                 } else if (availableHeightBelowContent >= mLineHeight) {
705                     // Just enough space to fit the toolbar with no vertical margins.
706                     y = contentRectOnScreen.bottom - mMarginVertical;
707                 } else {
708                     // Not enough space. Prefer to position as high as possible.
709                     y = Math.max(
710                             mViewPortOnScreen.top,
711                             contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
712                 }
713             } else {
714                 // Has an overflow.
715                 final int minimumOverflowHeightWithMargin =
716                         calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
717                 final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
718                         contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
719                 final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
720                         mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
721 
722                 if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
723                     // There is enough space at the top of the content rect for the overflow.
724                     // Position above and open upwards.
725                     updateOverflowHeight(availableHeightAboveContent - margin);
726                     y = contentRectOnScreen.top - mPopupWindow.getHeight();
727                     mOpenOverflowUpwards = true;
728                 } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
729                         && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
730                     // There is enough space at the top of the content rect for the main panel
731                     // but not the overflow.
732                     // Position above but open downwards.
733                     updateOverflowHeight(availableHeightThroughContentDown - margin);
734                     y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
735                     mOpenOverflowUpwards = false;
736                 } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
737                     // There is enough space at the bottom of the content rect for the overflow.
738                     // Position below and open downwards.
739                     updateOverflowHeight(availableHeightBelowContent - margin);
740                     y = contentRectOnScreen.bottom;
741                     mOpenOverflowUpwards = false;
742                 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
743                         && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
744                     // There is enough space at the bottom of the content rect for the main panel
745                     // but not the overflow.
746                     // Position below but open upwards.
747                     updateOverflowHeight(availableHeightThroughContentUp - margin);
748                     y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
749                             mPopupWindow.getHeight();
750                     mOpenOverflowUpwards = true;
751                 } else {
752                     // Not enough space.
753                     // Position at the top of the view port and open downwards.
754                     updateOverflowHeight(mViewPortOnScreen.height() - margin);
755                     y = mViewPortOnScreen.top;
756                     mOpenOverflowUpwards = false;
757                 }
758             }
759 
760             // We later specify the location of PopupWindow relative to the attached window.
761             // The idea here is that 1) we can get the location of a View in both window coordinates
762             // and screen coordiantes, where the offset between them should be equal to the window
763             // origin, and 2) we can use an arbitrary for this calculation while calculating the
764             // location of the rootview is supposed to be least expensive.
765             // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can avoid
766             // the following calculation.
767             mParent.getRootView().getLocationOnScreen(mTmpCoords);
768             int rootViewLeftOnScreen = mTmpCoords[0];
769             int rootViewTopOnScreen = mTmpCoords[1];
770             mParent.getRootView().getLocationInWindow(mTmpCoords);
771             int rootViewLeftOnWindow = mTmpCoords[0];
772             int rootViewTopOnWindow = mTmpCoords[1];
773             int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
774             int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
775             mCoordsOnWindow.set(
776                     Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
777         }
778 
779         /**
780          * Performs the "show" animation on the floating popup.
781          */
runShowAnimation()782         private void runShowAnimation() {
783             mShowAnimation.start();
784         }
785 
786         /**
787          * Performs the "dismiss" animation on the floating popup.
788          */
runDismissAnimation()789         private void runDismissAnimation() {
790             mDismissAnimation.start();
791         }
792 
793         /**
794          * Performs the "hide" animation on the floating popup.
795          */
runHideAnimation()796         private void runHideAnimation() {
797             mHideAnimation.start();
798         }
799 
cancelDismissAndHideAnimations()800         private void cancelDismissAndHideAnimations() {
801             mDismissAnimation.cancel();
802             mHideAnimation.cancel();
803         }
804 
cancelOverflowAnimations()805         private void cancelOverflowAnimations() {
806             mContentContainer.clearAnimation();
807             mMainPanel.animate().cancel();
808             mOverflowPanel.animate().cancel();
809             mToArrow.stop();
810             mToOverflow.stop();
811         }
812 
openOverflow()813         private void openOverflow() {
814             final int targetWidth = mOverflowPanelSize.getWidth();
815             final int targetHeight = mOverflowPanelSize.getHeight();
816             final int startWidth = mContentContainer.getWidth();
817             final int startHeight = mContentContainer.getHeight();
818             final float startY = mContentContainer.getY();
819             final float left = mContentContainer.getX();
820             final float right = left + mContentContainer.getWidth();
821             Animation widthAnimation = new Animation() {
822                 @Override
823                 protected void applyTransformation(float interpolatedTime, Transformation t) {
824                     int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
825                     setWidth(mContentContainer, startWidth + deltaWidth);
826                     if (isInRTLMode()) {
827                         mContentContainer.setX(left);
828 
829                         // Lock the panels in place.
830                         mMainPanel.setX(0);
831                         mOverflowPanel.setX(0);
832                     } else {
833                         mContentContainer.setX(right - mContentContainer.getWidth());
834 
835                         // Offset the panels' positions so they look like they're locked in place
836                         // on the screen.
837                         mMainPanel.setX(mContentContainer.getWidth() - startWidth);
838                         mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
839                     }
840                 }
841             };
842             Animation heightAnimation = new Animation() {
843                 @Override
844                 protected void applyTransformation(float interpolatedTime, Transformation t) {
845                     int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
846                     setHeight(mContentContainer, startHeight + deltaHeight);
847                     if (mOpenOverflowUpwards) {
848                         mContentContainer.setY(
849                                 startY - (mContentContainer.getHeight() - startHeight));
850                         positionContentYCoordinatesIfOpeningOverflowUpwards();
851                     }
852                 }
853             };
854             final float overflowButtonStartX = mOverflowButton.getX();
855             final float overflowButtonTargetX = isInRTLMode() ?
856                     overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
857                     overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
858             Animation overflowButtonAnimation = new Animation() {
859                 @Override
860                 protected void applyTransformation(float interpolatedTime, Transformation t) {
861                     float overflowButtonX = overflowButtonStartX
862                             + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
863                     float deltaContainerWidth = isInRTLMode() ?
864                             0 :
865                             mContentContainer.getWidth() - startWidth;
866                     float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
867                     mOverflowButton.setX(actualOverflowButtonX);
868                 }
869             };
870             widthAnimation.setInterpolator(mLogAccelerateInterpolator);
871             widthAnimation.setDuration(getAdjustedDuration(250));
872             heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
873             heightAnimation.setDuration(getAdjustedDuration(250));
874             overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
875             overflowButtonAnimation.setDuration(getAdjustedDuration(250));
876             mOpenOverflowAnimation.getAnimations().clear();
877             mOpenOverflowAnimation.getAnimations().clear();
878             mOpenOverflowAnimation.addAnimation(widthAnimation);
879             mOpenOverflowAnimation.addAnimation(heightAnimation);
880             mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
881             mContentContainer.startAnimation(mOpenOverflowAnimation);
882             mIsOverflowOpen = true;
883             mMainPanel.animate()
884                     .alpha(0).withLayer()
885                     .setInterpolator(mLinearOutSlowInInterpolator)
886                     .setDuration(250)
887                     .start();
888             mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
889         }
890 
closeOverflow()891         private void closeOverflow() {
892             final int targetWidth = mMainPanelSize.getWidth();
893             final int startWidth = mContentContainer.getWidth();
894             final float left = mContentContainer.getX();
895             final float right = left + mContentContainer.getWidth();
896             Animation widthAnimation = new Animation() {
897                 @Override
898                 protected void applyTransformation(float interpolatedTime, Transformation t) {
899                     int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
900                     setWidth(mContentContainer, startWidth + deltaWidth);
901                     if (isInRTLMode()) {
902                         mContentContainer.setX(left);
903 
904                         // Lock the panels in place.
905                         mMainPanel.setX(0);
906                         mOverflowPanel.setX(0);
907                     } else {
908                         mContentContainer.setX(right - mContentContainer.getWidth());
909 
910                         // Offset the panels' positions so they look like they're locked in place
911                         // on the screen.
912                         mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
913                         mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
914                     }
915                 }
916             };
917             final int targetHeight = mMainPanelSize.getHeight();
918             final int startHeight = mContentContainer.getHeight();
919             final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
920             Animation heightAnimation = new Animation() {
921                 @Override
922                 protected void applyTransformation(float interpolatedTime, Transformation t) {
923                     int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
924                     setHeight(mContentContainer, startHeight + deltaHeight);
925                     if (mOpenOverflowUpwards) {
926                         mContentContainer.setY(bottom - mContentContainer.getHeight());
927                         positionContentYCoordinatesIfOpeningOverflowUpwards();
928                     }
929                 }
930             };
931             final float overflowButtonStartX = mOverflowButton.getX();
932             final float overflowButtonTargetX = isInRTLMode() ?
933                     overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
934                     overflowButtonStartX + startWidth - mOverflowButton.getWidth();
935             Animation overflowButtonAnimation = new Animation() {
936                 @Override
937                 protected void applyTransformation(float interpolatedTime, Transformation t) {
938                     float overflowButtonX = overflowButtonStartX
939                             + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
940                     float deltaContainerWidth = isInRTLMode() ?
941                             0 :
942                             mContentContainer.getWidth() - startWidth;
943                     float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
944                     mOverflowButton.setX(actualOverflowButtonX);
945                 }
946             };
947             widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
948             widthAnimation.setDuration(getAdjustedDuration(250));
949             heightAnimation.setInterpolator(mLogAccelerateInterpolator);
950             heightAnimation.setDuration(getAdjustedDuration(250));
951             overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
952             overflowButtonAnimation.setDuration(getAdjustedDuration(250));
953             mCloseOverflowAnimation.getAnimations().clear();
954             mCloseOverflowAnimation.addAnimation(widthAnimation);
955             mCloseOverflowAnimation.addAnimation(heightAnimation);
956             mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
957             mContentContainer.startAnimation(mCloseOverflowAnimation);
958             mIsOverflowOpen = false;
959             mMainPanel.animate()
960                     .alpha(1).withLayer()
961                     .setInterpolator(mFastOutLinearInInterpolator)
962                     .setDuration(100)
963                     .start();
964             mOverflowPanel.animate()
965                     .alpha(0).withLayer()
966                     .setInterpolator(mLinearOutSlowInInterpolator)
967                     .setDuration(150)
968                     .start();
969         }
970 
971         /**
972          * Defines the position of the floating toolbar popup panels when transition animation has
973          * stopped.
974          */
setPanelsStatesAtRestingPosition()975         private void setPanelsStatesAtRestingPosition() {
976             mOverflowButton.setEnabled(true);
977             mOverflowPanel.awakenScrollBars();
978 
979             if (mIsOverflowOpen) {
980                 // Set open state.
981                 final Size containerSize = mOverflowPanelSize;
982                 setSize(mContentContainer, containerSize);
983                 mMainPanel.setAlpha(0);
984                 mMainPanel.setVisibility(View.INVISIBLE);
985                 mOverflowPanel.setAlpha(1);
986                 mOverflowPanel.setVisibility(View.VISIBLE);
987                 mOverflowButton.setImageDrawable(mArrow);
988                 mOverflowButton.setContentDescription(mContext.getString(
989                         R.string.floating_toolbar_close_overflow_description));
990 
991                 // Update x-coordinates depending on RTL state.
992                 if (isInRTLMode()) {
993                     mContentContainer.setX(mMarginHorizontal);  // align left
994                     mMainPanel.setX(0);  // align left
995                     mOverflowButton.setX(  // align right
996                             containerSize.getWidth() - mOverflowButtonSize.getWidth());
997                     mOverflowPanel.setX(0);  // align left
998                 } else {
999                     mContentContainer.setX(  // align right
1000                             mPopupWindow.getWidth() -
1001                                     containerSize.getWidth() - mMarginHorizontal);
1002                     mMainPanel.setX(-mContentContainer.getX());  // align right
1003                     mOverflowButton.setX(0);  // align left
1004                     mOverflowPanel.setX(0);  // align left
1005                 }
1006 
1007                 // Update y-coordinates depending on overflow's open direction.
1008                 if (mOpenOverflowUpwards) {
1009                     mContentContainer.setY(mMarginVertical);  // align top
1010                     mMainPanel.setY(  // align bottom
1011                             containerSize.getHeight() - mContentContainer.getHeight());
1012                     mOverflowButton.setY(  // align bottom
1013                             containerSize.getHeight() - mOverflowButtonSize.getHeight());
1014                     mOverflowPanel.setY(0);  // align top
1015                 } else {
1016                     // opens downwards.
1017                     mContentContainer.setY(mMarginVertical);  // align top
1018                     mMainPanel.setY(0);  // align top
1019                     mOverflowButton.setY(0);  // align top
1020                     mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
1021                 }
1022             } else {
1023                 // Overflow not open. Set closed state.
1024                 final Size containerSize = mMainPanelSize;
1025                 setSize(mContentContainer, containerSize);
1026                 mMainPanel.setAlpha(1);
1027                 mMainPanel.setVisibility(View.VISIBLE);
1028                 mOverflowPanel.setAlpha(0);
1029                 mOverflowPanel.setVisibility(View.INVISIBLE);
1030                 mOverflowButton.setImageDrawable(mOverflow);
1031                 mOverflowButton.setContentDescription(mContext.getString(
1032                         R.string.floating_toolbar_open_overflow_description));
1033 
1034                 if (hasOverflow()) {
1035                     // Update x-coordinates depending on RTL state.
1036                     if (isInRTLMode()) {
1037                         mContentContainer.setX(mMarginHorizontal);  // align left
1038                         mMainPanel.setX(0);  // align left
1039                         mOverflowButton.setX(0);  // align left
1040                         mOverflowPanel.setX(0);  // align left
1041                     } else {
1042                         mContentContainer.setX(  // align right
1043                                 mPopupWindow.getWidth() -
1044                                         containerSize.getWidth() - mMarginHorizontal);
1045                         mMainPanel.setX(0);  // align left
1046                         mOverflowButton.setX(  // align right
1047                                 containerSize.getWidth() - mOverflowButtonSize.getWidth());
1048                         mOverflowPanel.setX(  // align right
1049                                 containerSize.getWidth() - mOverflowPanelSize.getWidth());
1050                     }
1051 
1052                     // Update y-coordinates depending on overflow's open direction.
1053                     if (mOpenOverflowUpwards) {
1054                         mContentContainer.setY(  // align bottom
1055                                 mMarginVertical +
1056                                         mOverflowPanelSize.getHeight() - containerSize.getHeight());
1057                         mMainPanel.setY(0);  // align top
1058                         mOverflowButton.setY(0);  // align top
1059                         mOverflowPanel.setY(  // align bottom
1060                                 containerSize.getHeight() - mOverflowPanelSize.getHeight());
1061                     } else {
1062                         // opens downwards.
1063                         mContentContainer.setY(mMarginVertical);  // align top
1064                         mMainPanel.setY(0);  // align top
1065                         mOverflowButton.setY(0);  // align top
1066                         mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
1067                     }
1068                 } else {
1069                     // No overflow.
1070                     mContentContainer.setX(mMarginHorizontal);  // align left
1071                     mContentContainer.setY(mMarginVertical);  // align top
1072                     mMainPanel.setX(0);  // align left
1073                     mMainPanel.setY(0);  // align top
1074                 }
1075             }
1076         }
1077 
updateOverflowHeight(int suggestedHeight)1078         private void updateOverflowHeight(int suggestedHeight) {
1079             if (hasOverflow()) {
1080                 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
1081                         mLineHeight;
1082                 final int newHeight = calculateOverflowHeight(maxItemSize);
1083                 if (mOverflowPanelSize.getHeight() != newHeight) {
1084                     mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
1085                 }
1086                 setSize(mOverflowPanel, mOverflowPanelSize);
1087                 if (mIsOverflowOpen) {
1088                     setSize(mContentContainer, mOverflowPanelSize);
1089                     if (mOpenOverflowUpwards) {
1090                         final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
1091                         mContentContainer.setY(mContentContainer.getY() + deltaHeight);
1092                         mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
1093                     }
1094                 } else {
1095                     setSize(mContentContainer, mMainPanelSize);
1096                 }
1097                 updatePopupSize();
1098             }
1099         }
1100 
updatePopupSize()1101         private void updatePopupSize() {
1102             int width = 0;
1103             int height = 0;
1104             if (mMainPanelSize != null) {
1105                 width = Math.max(width, mMainPanelSize.getWidth());
1106                 height = Math.max(height, mMainPanelSize.getHeight());
1107             }
1108             if (mOverflowPanelSize != null) {
1109                 width = Math.max(width, mOverflowPanelSize.getWidth());
1110                 height = Math.max(height, mOverflowPanelSize.getHeight());
1111             }
1112             mPopupWindow.setWidth(width + mMarginHorizontal * 2);
1113             mPopupWindow.setHeight(height + mMarginVertical * 2);
1114             maybeComputeTransitionDurationScale();
1115         }
1116 
refreshViewPort()1117         private void refreshViewPort() {
1118             mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
1119         }
1120 
getAdjustedToolbarWidth(int suggestedWidth)1121         private int getAdjustedToolbarWidth(int suggestedWidth) {
1122             int width = suggestedWidth;
1123             refreshViewPort();
1124             int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
1125                     .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1126             if (width <= 0) {
1127                 width = mParent.getResources()
1128                         .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1129             }
1130             return Math.min(width, maximumWidth);
1131         }
1132 
1133         /**
1134          * Sets the touchable region of this popup to be zero. This means that all touch events on
1135          * this popup will go through to the surface behind it.
1136          */
setZeroTouchableSurface()1137         private void setZeroTouchableSurface() {
1138             mTouchableRegion.setEmpty();
1139         }
1140 
1141         /**
1142          * Sets the touchable region of this popup to be the area occupied by its content.
1143          */
setContentAreaAsTouchableSurface()1144         private void setContentAreaAsTouchableSurface() {
1145             Objects.requireNonNull(mMainPanelSize);
1146             final int width;
1147             final int height;
1148             if (mIsOverflowOpen) {
1149                 Objects.requireNonNull(mOverflowPanelSize);
1150                 width = mOverflowPanelSize.getWidth();
1151                 height = mOverflowPanelSize.getHeight();
1152             } else {
1153                 width = mMainPanelSize.getWidth();
1154                 height = mMainPanelSize.getHeight();
1155             }
1156             mTouchableRegion.set(
1157                     (int) mContentContainer.getX(),
1158                     (int) mContentContainer.getY(),
1159                     (int) mContentContainer.getX() + width,
1160                     (int) mContentContainer.getY() + height);
1161         }
1162 
1163         /**
1164          * Make the touchable area of this popup be the area specified by mTouchableRegion.
1165          * This should be called after the popup window has been dismissed (dismiss/hide)
1166          * and is probably being re-shown with a new content root view.
1167          */
setTouchableSurfaceInsetsComputer()1168         private void setTouchableSurfaceInsetsComputer() {
1169             ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
1170                     .getRootView()
1171                     .getViewTreeObserver();
1172             viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
1173             viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
1174         }
1175 
isInRTLMode()1176         private boolean isInRTLMode() {
1177             return mContext.getApplicationInfo().hasRtlSupport()
1178                     && mContext.getResources().getConfiguration().getLayoutDirection()
1179                             == View.LAYOUT_DIRECTION_RTL;
1180         }
1181 
hasOverflow()1182         private boolean hasOverflow() {
1183             return mOverflowPanelSize != null;
1184         }
1185 
1186         /**
1187          * Fits as many menu items in the main panel and returns a list of the menu items that
1188          * were not fit in.
1189          *
1190          * @return The menu items that are not included in this main panel.
1191          */
layoutMainPanelItems( List<MenuItem> menuItems, final int toolbarWidth)1192         public List<MenuItem> layoutMainPanelItems(
1193                 List<MenuItem> menuItems, final int toolbarWidth) {
1194             Objects.requireNonNull(menuItems);
1195 
1196             int availableWidth = toolbarWidth;
1197 
1198             final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>();
1199             // add the overflow menu items to the end of the remainingMenuItems list.
1200             final LinkedList<MenuItem> overflowMenuItems = new LinkedList();
1201             for (MenuItem menuItem : menuItems) {
1202                 if (menuItem.getItemId() != android.R.id.textAssist
1203                         && menuItem.requiresOverflow()) {
1204                     overflowMenuItems.add(menuItem);
1205                 } else {
1206                     remainingMenuItems.add(menuItem);
1207                 }
1208             }
1209             remainingMenuItems.addAll(overflowMenuItems);
1210 
1211             mMainPanel.removeAllViews();
1212             mMainPanel.setPaddingRelative(0, 0, 0, 0);
1213 
1214             int lastGroupId = -1;
1215             boolean isFirstItem = true;
1216             while (!remainingMenuItems.isEmpty()) {
1217                 final MenuItem menuItem = remainingMenuItems.peek();
1218 
1219                 // if this is the first item, regardless of requiresOverflow(), it should be
1220                 // displayed on the main panel. Otherwise all items including this one will be
1221                 // overflow items, and should be displayed in overflow panel.
1222                 if(!isFirstItem && menuItem.requiresOverflow()) {
1223                     break;
1224                 }
1225 
1226                 final boolean showIcon = isFirstItem && menuItem.getItemId() == R.id.textAssist;
1227                 final View menuItemButton = createMenuItemButton(
1228                         mContext, menuItem, mIconTextSpacing, showIcon);
1229                 if (!showIcon && menuItemButton instanceof LinearLayout) {
1230                     ((LinearLayout) menuItemButton).setGravity(Gravity.CENTER);
1231                 }
1232 
1233                 // Adding additional start padding for the first button to even out button spacing.
1234                 if (isFirstItem) {
1235                     menuItemButton.setPaddingRelative(
1236                             (int) (1.5 * menuItemButton.getPaddingStart()),
1237                             menuItemButton.getPaddingTop(),
1238                             menuItemButton.getPaddingEnd(),
1239                             menuItemButton.getPaddingBottom());
1240                 }
1241 
1242                 // Adding additional end padding for the last button to even out button spacing.
1243                 boolean isLastItem = remainingMenuItems.size() == 1;
1244                 if (isLastItem) {
1245                     menuItemButton.setPaddingRelative(
1246                             menuItemButton.getPaddingStart(),
1247                             menuItemButton.getPaddingTop(),
1248                             (int) (1.5 * menuItemButton.getPaddingEnd()),
1249                             menuItemButton.getPaddingBottom());
1250                 }
1251 
1252                 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1253                 final int menuItemButtonWidth = Math.min(
1254                         menuItemButton.getMeasuredWidth(), toolbarWidth);
1255 
1256                 // Check if we can fit an item while reserving space for the overflowButton.
1257                 final boolean canFitWithOverflow =
1258                         menuItemButtonWidth <=
1259                                 availableWidth - mOverflowButtonSize.getWidth();
1260                 final boolean canFitNoOverflow =
1261                         isLastItem && menuItemButtonWidth <= availableWidth;
1262                 if (canFitWithOverflow || canFitNoOverflow) {
1263                     setButtonTagAndClickListener(menuItemButton, menuItem);
1264                     // Set tooltips for main panel items, but not overflow items (b/35726766).
1265                     menuItemButton.setTooltipText(menuItem.getTooltipText());
1266                     mMainPanel.addView(menuItemButton);
1267                     final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1268                     params.width = menuItemButtonWidth;
1269                     menuItemButton.setLayoutParams(params);
1270                     availableWidth -= menuItemButtonWidth;
1271                     remainingMenuItems.pop();
1272                 } else {
1273                     break;
1274                 }
1275                 lastGroupId = menuItem.getGroupId();
1276                 isFirstItem = false;
1277             }
1278 
1279             if (!remainingMenuItems.isEmpty()) {
1280                 // Reserve space for overflowButton.
1281                 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1282             }
1283 
1284             mMainPanelSize = measure(mMainPanel);
1285             return remainingMenuItems;
1286         }
1287 
layoutOverflowPanelItems(List<MenuItem> menuItems)1288         private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
1289             ArrayAdapter<MenuItem> overflowPanelAdapter =
1290                     (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1291             overflowPanelAdapter.clear();
1292             final int size = menuItems.size();
1293             for (int i = 0; i < size; i++) {
1294                 overflowPanelAdapter.add(menuItems.get(i));
1295             }
1296             mOverflowPanel.setAdapter(overflowPanelAdapter);
1297             if (mOpenOverflowUpwards) {
1298                 mOverflowPanel.setY(0);
1299             } else {
1300                 mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1301             }
1302 
1303             int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
1304             int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
1305             mOverflowPanelSize = new Size(width, height);
1306             setSize(mOverflowPanel, mOverflowPanelSize);
1307         }
1308 
1309         /**
1310          * Resets the content container and appropriately position it's panels.
1311          */
preparePopupContent()1312         private void preparePopupContent() {
1313             mContentContainer.removeAllViews();
1314 
1315             // Add views in the specified order so they stack up as expected.
1316             // Order: overflowPanel, mainPanel, overflowButton.
1317             if (hasOverflow()) {
1318                 mContentContainer.addView(mOverflowPanel);
1319             }
1320             mContentContainer.addView(mMainPanel);
1321             if (hasOverflow()) {
1322                 mContentContainer.addView(mOverflowButton);
1323             }
1324             setPanelsStatesAtRestingPosition();
1325             setContentAreaAsTouchableSurface();
1326 
1327             // The positioning of contents in RTL is wrong when the view is first rendered.
1328             // Hide the view and post a runnable to recalculate positions and render the view.
1329             // TODO: Investigate why this happens and fix.
1330             if (isInRTLMode()) {
1331                 mContentContainer.setAlpha(0);
1332                 mContentContainer.post(mPreparePopupContentRTLHelper);
1333             }
1334         }
1335 
1336         /**
1337          * Clears out the panels and their container. Resets their calculated sizes.
1338          */
clearPanels()1339         private void clearPanels() {
1340             mOverflowPanelSize = null;
1341             mMainPanelSize = null;
1342             mIsOverflowOpen = false;
1343             mMainPanel.removeAllViews();
1344             ArrayAdapter<MenuItem> overflowPanelAdapter =
1345                     (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1346             overflowPanelAdapter.clear();
1347             mOverflowPanel.setAdapter(overflowPanelAdapter);
1348             mContentContainer.removeAllViews();
1349         }
1350 
positionContentYCoordinatesIfOpeningOverflowUpwards()1351         private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
1352             if (mOpenOverflowUpwards) {
1353                 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
1354                 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
1355                 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
1356             }
1357         }
1358 
getOverflowWidth()1359         private int getOverflowWidth() {
1360             int overflowWidth = 0;
1361             final int count = mOverflowPanel.getAdapter().getCount();
1362             for (int i = 0; i < count; i++) {
1363                 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
1364                 overflowWidth =
1365                         Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1366             }
1367             return overflowWidth;
1368         }
1369 
calculateOverflowHeight(int maxItemSize)1370         private int calculateOverflowHeight(int maxItemSize) {
1371             // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
1372             int actualSize = Math.min(
1373                     MAX_OVERFLOW_SIZE,
1374                     Math.min(
1375                             Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1376                             mOverflowPanel.getCount()));
1377             int extension = 0;
1378             if (actualSize < mOverflowPanel.getCount()) {
1379                 // The overflow will require scrolling to get to all the items.
1380                 // Extend the height so that part of the hidden items is displayed.
1381                 extension = (int) (mLineHeight * 0.5f);
1382             }
1383             return actualSize * mLineHeight
1384                     + mOverflowButtonSize.getHeight()
1385                     + extension;
1386         }
1387 
setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem)1388         private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1389             menuItemButton.setTag(MenuItemRepr.of(menuItem));
1390             menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
1391         }
1392 
1393         /**
1394          * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1395          * animations. See comment about this in the code.
1396          */
getAdjustedDuration(int originalDuration)1397         private int getAdjustedDuration(int originalDuration) {
1398             if (mTransitionDurationScale < 150) {
1399                 // For smaller transition, decrease the time.
1400                 return Math.max(originalDuration - 50, 0);
1401             } else if (mTransitionDurationScale > 300) {
1402                 // For bigger transition, increase the time.
1403                 return originalDuration + 50;
1404             }
1405 
1406             // Scale the animation duration with getDurationScale(). This allows
1407             // android.view.animation.* animations to scale just like android.animation.* animations
1408             // when  animator duration scale is adjusted in "Developer Options".
1409             // For this reason, do not use this method for android.animation.* animations.
1410             return (int) (originalDuration * ValueAnimator.getDurationScale());
1411         }
1412 
maybeComputeTransitionDurationScale()1413         private void maybeComputeTransitionDurationScale() {
1414             if (mMainPanelSize != null && mOverflowPanelSize != null) {
1415                 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
1416                 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
1417                 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
1418                         mContentContainer.getContext().getResources().getDisplayMetrics().density);
1419             }
1420         }
1421 
createMainPanel()1422         private ViewGroup createMainPanel() {
1423             ViewGroup mainPanel = new LinearLayout(mContext) {
1424                 @Override
1425                 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1426                     if (isOverflowAnimating()) {
1427                         // Update widthMeasureSpec to make sure that this view is not clipped
1428                         // as we offset it's coordinates with respect to it's parent.
1429                         widthMeasureSpec = MeasureSpec.makeMeasureSpec(
1430                                 mMainPanelSize.getWidth(),
1431                                 MeasureSpec.EXACTLY);
1432                     }
1433                     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1434                 }
1435 
1436                 @Override
1437                 public boolean onInterceptTouchEvent(MotionEvent ev) {
1438                     // Intercept the touch event while the overflow is animating.
1439                     return isOverflowAnimating();
1440                 }
1441             };
1442             return mainPanel;
1443         }
1444 
createOverflowButton()1445         private ImageButton createOverflowButton() {
1446             final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
1447                     .inflate(R.layout.floating_popup_overflow_button, null);
1448             overflowButton.setImageDrawable(mOverflow);
1449             overflowButton.setOnClickListener(v -> {
1450                 if (mIsOverflowOpen) {
1451                     overflowButton.setImageDrawable(mToOverflow);
1452                     mToOverflow.start();
1453                     closeOverflow();
1454                 } else {
1455                     overflowButton.setImageDrawable(mToArrow);
1456                     mToArrow.start();
1457                     openOverflow();
1458                 }
1459             });
1460             return overflowButton;
1461         }
1462 
createOverflowPanel()1463         private OverflowPanel createOverflowPanel() {
1464             final OverflowPanel overflowPanel = new OverflowPanel(this);
1465             overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
1466                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1467             overflowPanel.setDivider(null);
1468             overflowPanel.setDividerHeight(0);
1469 
1470             final ArrayAdapter adapter =
1471                     new ArrayAdapter<MenuItem>(mContext, 0) {
1472                         @Override
1473                         public View getView(int position, View convertView, ViewGroup parent) {
1474                             return mOverflowPanelViewHelper.getView(
1475                                     getItem(position), mOverflowPanelSize.getWidth(), convertView);
1476                         }
1477                     };
1478             overflowPanel.setAdapter(adapter);
1479 
1480             overflowPanel.setOnItemClickListener((parent, view, position, id) -> {
1481                 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
1482                 if (mOnMenuItemClickListener != null) {
1483                     mOnMenuItemClickListener.onMenuItemClick(menuItem);
1484                 }
1485             });
1486 
1487             return overflowPanel;
1488         }
1489 
isOverflowAnimating()1490         private boolean isOverflowAnimating() {
1491             final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
1492                     && !mOpenOverflowAnimation.hasEnded();
1493             final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
1494                     && !mCloseOverflowAnimation.hasEnded();
1495             return overflowOpening || overflowClosing;
1496         }
1497 
createOverflowAnimationListener()1498         private Animation.AnimationListener createOverflowAnimationListener() {
1499             Animation.AnimationListener listener = new Animation.AnimationListener() {
1500                 @Override
1501                 public void onAnimationStart(Animation animation) {
1502                     // Disable the overflow button while it's animating.
1503                     // It will be re-enabled when the animation stops.
1504                     mOverflowButton.setEnabled(false);
1505                     // Ensure both panels have visibility turned on when the overflow animation
1506                     // starts.
1507                     mMainPanel.setVisibility(View.VISIBLE);
1508                     mOverflowPanel.setVisibility(View.VISIBLE);
1509                 }
1510 
1511                 @Override
1512                 public void onAnimationEnd(Animation animation) {
1513                     // Posting this because it seems like this is called before the animation
1514                     // actually ends.
1515                     mContentContainer.post(() -> {
1516                         setPanelsStatesAtRestingPosition();
1517                         setContentAreaAsTouchableSurface();
1518                     });
1519                 }
1520 
1521                 @Override
1522                 public void onAnimationRepeat(Animation animation) {
1523                 }
1524             };
1525             return listener;
1526         }
1527 
measure(View view)1528         private static Size measure(View view) {
1529             Preconditions.checkState(view.getParent() == null);
1530             view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1531             return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
1532         }
1533 
setSize(View view, int width, int height)1534         private static void setSize(View view, int width, int height) {
1535             view.setMinimumWidth(width);
1536             view.setMinimumHeight(height);
1537             ViewGroup.LayoutParams params = view.getLayoutParams();
1538             params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
1539             params.width = width;
1540             params.height = height;
1541             view.setLayoutParams(params);
1542         }
1543 
setSize(View view, Size size)1544         private static void setSize(View view, Size size) {
1545             setSize(view, size.getWidth(), size.getHeight());
1546         }
1547 
setWidth(View view, int width)1548         private static void setWidth(View view, int width) {
1549             ViewGroup.LayoutParams params = view.getLayoutParams();
1550             setSize(view, width, params.height);
1551         }
1552 
setHeight(View view, int height)1553         private static void setHeight(View view, int height) {
1554             ViewGroup.LayoutParams params = view.getLayoutParams();
1555             setSize(view, params.width, height);
1556         }
1557 
1558         /**
1559          * A custom ListView for the overflow panel.
1560          */
1561         private static final class OverflowPanel extends ListView {
1562 
1563             private final FloatingToolbarPopup mPopup;
1564 
OverflowPanel(FloatingToolbarPopup popup)1565             OverflowPanel(FloatingToolbarPopup popup) {
1566                 super(Objects.requireNonNull(popup).mContext);
1567                 this.mPopup = popup;
1568                 setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
1569                 setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
1570             }
1571 
1572             @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)1573             protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1574                 // Update heightMeasureSpec to make sure that this view is not clipped
1575                 // as we offset it's coordinates with respect to it's parent.
1576                 int height = mPopup.mOverflowPanelSize.getHeight()
1577                         - mPopup.mOverflowButtonSize.getHeight();
1578                 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1579                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1580             }
1581 
1582             @Override
dispatchTouchEvent(MotionEvent ev)1583             public boolean dispatchTouchEvent(MotionEvent ev) {
1584                 if (mPopup.isOverflowAnimating()) {
1585                     // Eat the touch event.
1586                     return true;
1587                 }
1588                 return super.dispatchTouchEvent(ev);
1589             }
1590 
1591             @Override
awakenScrollBars()1592             protected boolean awakenScrollBars() {
1593                 return super.awakenScrollBars();
1594             }
1595         }
1596 
1597         /**
1598          * A custom interpolator used for various floating toolbar animations.
1599          */
1600         private static final class LogAccelerateInterpolator implements Interpolator {
1601 
1602             private static final int BASE = 100;
1603             private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
1604 
computeLog(float t, int base)1605             private static float computeLog(float t, int base) {
1606                 return (float) (1 - Math.pow(base, -t));
1607             }
1608 
1609             @Override
getInterpolation(float t)1610             public float getInterpolation(float t) {
1611                 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1612             }
1613         }
1614 
1615         /**
1616          * A helper for generating views for the overflow panel.
1617          */
1618         private static final class OverflowPanelViewHelper {
1619 
1620             private final View mCalculator;
1621             private final int mIconTextSpacing;
1622             private final int mSidePadding;
1623 
1624             private final Context mContext;
1625 
OverflowPanelViewHelper(Context context, int iconTextSpacing)1626             public OverflowPanelViewHelper(Context context, int iconTextSpacing) {
1627                 mContext = Objects.requireNonNull(context);
1628                 mIconTextSpacing = iconTextSpacing;
1629                 mSidePadding = context.getResources()
1630                         .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
1631                 mCalculator = createMenuButton(null);
1632             }
1633 
getView(MenuItem menuItem, int minimumWidth, View convertView)1634             public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
1635                 Objects.requireNonNull(menuItem);
1636                 if (convertView != null) {
1637                     updateMenuItemButton(
1638                             convertView, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
1639                 } else {
1640                     convertView = createMenuButton(menuItem);
1641                 }
1642                 convertView.setMinimumWidth(minimumWidth);
1643                 return convertView;
1644             }
1645 
calculateWidth(MenuItem menuItem)1646             public int calculateWidth(MenuItem menuItem) {
1647                 updateMenuItemButton(
1648                         mCalculator, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
1649                 mCalculator.measure(
1650                         View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1651                 return mCalculator.getMeasuredWidth();
1652             }
1653 
createMenuButton(MenuItem menuItem)1654             private View createMenuButton(MenuItem menuItem) {
1655                 View button = createMenuItemButton(
1656                         mContext, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
1657                 button.setPadding(mSidePadding, 0, mSidePadding, 0);
1658                 return button;
1659             }
1660 
shouldShowIcon(MenuItem menuItem)1661             private boolean shouldShowIcon(MenuItem menuItem) {
1662                 if (menuItem != null) {
1663                     return menuItem.getGroupId() == android.R.id.textAssist;
1664                 }
1665                 return false;
1666             }
1667         }
1668     }
1669 
1670     /**
1671      * Represents the identity of a MenuItem that is rendered in a FloatingToolbarPopup.
1672      */
1673     @VisibleForTesting
1674     public static final class MenuItemRepr {
1675 
1676         public final int itemId;
1677         public final int groupId;
1678         @Nullable public final String title;
1679         @Nullable private final Drawable mIcon;
1680 
MenuItemRepr( int itemId, int groupId, @Nullable CharSequence title, @Nullable Drawable icon)1681         private MenuItemRepr(
1682                 int itemId, int groupId, @Nullable CharSequence title, @Nullable Drawable icon) {
1683             this.itemId = itemId;
1684             this.groupId = groupId;
1685             this.title = (title == null) ? null : title.toString();
1686             mIcon = icon;
1687         }
1688 
1689         /**
1690          * Creates an instance of MenuItemRepr for the specified menu item.
1691          */
of(MenuItem menuItem)1692         public static MenuItemRepr of(MenuItem menuItem) {
1693             return new MenuItemRepr(
1694                     menuItem.getItemId(),
1695                     menuItem.getGroupId(),
1696                     menuItem.getTitle(),
1697                     menuItem.getIcon());
1698         }
1699 
1700         /**
1701          * Returns this object's hashcode.
1702          */
1703         @Override
hashCode()1704         public int hashCode() {
1705             return Objects.hash(itemId, groupId, title, mIcon);
1706         }
1707 
1708         /**
1709          * Returns true if this object is the same as the specified object.
1710          */
1711         @Override
equals(Object o)1712         public boolean equals(Object o) {
1713             if (o == this) {
1714                 return true;
1715             }
1716             if (!(o instanceof MenuItemRepr)) {
1717                 return false;
1718             }
1719             final MenuItemRepr other = (MenuItemRepr) o;
1720             return itemId == other.itemId
1721                     && groupId == other.groupId
1722                     && TextUtils.equals(title, other.title)
1723                     // Many Drawables (icons) do not implement equals(). Using equals() here instead
1724                     // of reference comparisons in case a Drawable subclass implements equals().
1725                     && Objects.equals(mIcon, other.mIcon);
1726         }
1727 
1728         /**
1729          * Returns true if the two menu item collections are the same based on MenuItemRepr.
1730          */
reprEquals( Collection<MenuItem> menuItems1, Collection<MenuItem> menuItems2)1731         public static boolean reprEquals(
1732                 Collection<MenuItem> menuItems1, Collection<MenuItem> menuItems2) {
1733             if (menuItems1.size() != menuItems2.size()) {
1734                 return false;
1735             }
1736 
1737             final Iterator<MenuItem> menuItems2Iter = menuItems2.iterator();
1738             for (MenuItem menuItem1 : menuItems1) {
1739                 final MenuItem menuItem2 = menuItems2Iter.next();
1740                 if (!MenuItemRepr.of(menuItem1).equals(MenuItemRepr.of(menuItem2))) {
1741                     return false;
1742                 }
1743             }
1744 
1745             return true;
1746         }
1747     }
1748 
1749     /**
1750      * Creates and returns a menu button for the specified menu item.
1751      */
createMenuItemButton( Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon)1752     private static View createMenuItemButton(
1753             Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
1754         final View menuItemButton = LayoutInflater.from(context)
1755                 .inflate(R.layout.floating_popup_menu_button, null);
1756         if (menuItem != null) {
1757             updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon);
1758         }
1759         return menuItemButton;
1760     }
1761 
1762     /**
1763      * Updates the specified menu item button with the specified menu item data.
1764      */
updateMenuItemButton( View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon)1765     private static void updateMenuItemButton(
1766             View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
1767         final TextView buttonText = menuItemButton.findViewById(
1768                 R.id.floating_toolbar_menu_item_text);
1769         buttonText.setEllipsize(null);
1770         if (TextUtils.isEmpty(menuItem.getTitle())) {
1771             buttonText.setVisibility(View.GONE);
1772         } else {
1773             buttonText.setVisibility(View.VISIBLE);
1774             buttonText.setText(menuItem.getTitle());
1775         }
1776         final ImageView buttonIcon = menuItemButton.findViewById(
1777                 R.id.floating_toolbar_menu_item_image);
1778         if (menuItem.getIcon() == null || !showIcon) {
1779             buttonIcon.setVisibility(View.GONE);
1780             if (buttonText != null) {
1781                 buttonText.setPaddingRelative(0, 0, 0, 0);
1782             }
1783         } else {
1784             buttonIcon.setVisibility(View.VISIBLE);
1785             buttonIcon.setImageDrawable(menuItem.getIcon());
1786             if (buttonText != null) {
1787                 buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
1788             }
1789         }
1790         final CharSequence contentDescription = menuItem.getContentDescription();
1791         if (TextUtils.isEmpty(contentDescription)) {
1792             menuItemButton.setContentDescription(menuItem.getTitle());
1793         } else {
1794             menuItemButton.setContentDescription(contentDescription);
1795         }
1796     }
1797 
createContentContainer(Context context)1798     private static ViewGroup createContentContainer(Context context) {
1799         ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
1800                 .inflate(R.layout.floating_popup_container, null);
1801         contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
1802                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1803         contentContainer.setTag(FLOATING_TOOLBAR_TAG);
1804         contentContainer.setClipToOutline(true);
1805         return contentContainer;
1806     }
1807 
createPopupWindow(ViewGroup content)1808     private static PopupWindow createPopupWindow(ViewGroup content) {
1809         ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1810         PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1811         // TODO: Use .setIsLaidOutInScreen(true) instead of .setClippingEnabled(false)
1812         // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1813         popupWindow.setClippingEnabled(false);
1814         popupWindow.setWindowLayoutType(
1815                 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1816         popupWindow.setAnimationStyle(0);
1817         popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1818         content.setLayoutParams(new ViewGroup.LayoutParams(
1819                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1820         popupContentHolder.addView(content);
1821         return popupWindow;
1822     }
1823 
1824     /**
1825      * Creates an "appear" animation for the specified view.
1826      *
1827      * @param view  The view to animate
1828      */
createEnterAnimation(View view)1829     private static AnimatorSet createEnterAnimation(View view) {
1830         AnimatorSet animation = new AnimatorSet();
1831         animation.playTogether(
1832                 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
1833         return animation;
1834     }
1835 
1836     /**
1837      * Creates a "disappear" animation for the specified view.
1838      *
1839      * @param view  The view to animate
1840      * @param startDelay  The start delay of the animation
1841      * @param listener  The animation listener
1842      */
createExitAnimation( View view, int startDelay, Animator.AnimatorListener listener)1843     private static AnimatorSet createExitAnimation(
1844             View view, int startDelay, Animator.AnimatorListener listener) {
1845         AnimatorSet animation =  new AnimatorSet();
1846         animation.playTogether(
1847                 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
1848         animation.setStartDelay(startDelay);
1849         animation.addListener(listener);
1850         return animation;
1851     }
1852 
1853     /**
1854      * Returns a re-themed context with controlled look and feel for views.
1855      */
applyDefaultTheme(Context originalContext)1856     private static Context applyDefaultTheme(Context originalContext) {
1857         TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1858         boolean isLightTheme = a.getBoolean(0, true);
1859         int themeId
1860                 = isLightTheme ? R.style.Theme_DeviceDefault_Light : R.style.Theme_DeviceDefault;
1861         a.recycle();
1862         return new ContextThemeWrapper(originalContext, themeId);
1863     }
1864 }
1865