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