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