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