FloatingToolbar.java revision c0fa6bd7ac8fa8f138c62d734d276e55d600bb6b
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.content.Context;
24import android.graphics.Color;
25import android.graphics.Point;
26import android.graphics.Rect;
27import android.graphics.Region;
28import android.graphics.drawable.ColorDrawable;
29import android.util.Size;
30import android.view.Gravity;
31import android.view.LayoutInflater;
32import android.view.Menu;
33import android.view.MenuItem;
34import android.view.View;
35import android.view.View.MeasureSpec;
36import android.view.ViewGroup;
37import android.view.ViewTreeObserver;
38import android.view.Window;
39import android.view.WindowManager;
40import android.view.animation.Animation;
41import android.view.animation.AnimationSet;
42import android.view.animation.Transformation;
43import android.widget.AdapterView;
44import android.widget.ArrayAdapter;
45import android.widget.Button;
46import android.widget.ImageButton;
47import android.widget.LinearLayout;
48import android.widget.ListView;
49import android.widget.PopupWindow;
50import android.widget.TextView;
51
52import java.util.ArrayList;
53import java.util.LinkedList;
54import java.util.List;
55
56import com.android.internal.R;
57import com.android.internal.util.Preconditions;
58
59/**
60 * A floating toolbar for showing contextual menu items.
61 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
62 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
63 * The horizontal toolbar morphs into the vertical overflow view.
64 */
65public final class FloatingToolbar {
66
67    // This class is responsible for the public API of the floating toolbar.
68    // It delegates rendering operations to the FloatingToolbarPopup.
69
70    private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
71            new MenuItem.OnMenuItemClickListener() {
72                @Override
73                public boolean onMenuItemClick(MenuItem item) {
74                    return false;
75                }
76            };
77
78    private final Context mContext;
79    private final FloatingToolbarPopup mPopup;
80
81    private final Rect mContentRect = new Rect();
82    private final Point mCoordinates = new Point();
83
84    private Menu mMenu;
85    private List<CharSequence> mShowingTitles = new ArrayList<CharSequence>();
86    private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
87
88    private int mSuggestedWidth;
89    private boolean mWidthChanged = true;
90    private int mOverflowDirection;
91
92    /**
93     * Initializes a floating toolbar.
94     */
95    public FloatingToolbar(Context context, Window window) {
96        mContext = Preconditions.checkNotNull(context);
97        mPopup = new FloatingToolbarPopup(window.getDecorView());
98    }
99
100    /**
101     * Sets the menu to be shown in this floating toolbar.
102     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
103     * toolbar.
104     */
105    public FloatingToolbar setMenu(Menu menu) {
106        mMenu = Preconditions.checkNotNull(menu);
107        return this;
108    }
109
110    /**
111     * Sets the custom listener for invocation of menu items in this floating toolbar.
112     */
113    public FloatingToolbar setOnMenuItemClickListener(
114            MenuItem.OnMenuItemClickListener menuItemClickListener) {
115        if (menuItemClickListener != null) {
116            mMenuItemClickListener = menuItemClickListener;
117        } else {
118            mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
119        }
120        return this;
121    }
122
123    /**
124     * Sets the content rectangle. This is the area of the interesting content that this toolbar
125     * should avoid obstructing.
126     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
127     * toolbar.
128     */
129    public FloatingToolbar setContentRect(Rect rect) {
130        mContentRect.set(Preconditions.checkNotNull(rect));
131        return this;
132    }
133
134    /**
135     * Sets the suggested width of this floating toolbar.
136     * The actual width will be about this size but there are no guarantees that it will be exactly
137     * the suggested width.
138     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
139     * toolbar.
140     */
141    public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
142        // Check if there's been a substantial width spec change.
143        int difference = Math.abs(suggestedWidth - mSuggestedWidth);
144        mWidthChanged = difference > (mSuggestedWidth * 0.2);
145
146        mSuggestedWidth = suggestedWidth;
147        return this;
148    }
149
150    /**
151     * Shows this floating toolbar.
152     */
153    public FloatingToolbar show() {
154        List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
155        if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
156            mPopup.dismiss();
157            mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
158            mShowingTitles = getMenuItemTitles(menuItems);
159        }
160        refreshCoordinates();
161        mPopup.setOverflowDirection(mOverflowDirection);
162        mPopup.updateCoordinates(mCoordinates.x, mCoordinates.y);
163        if (!mPopup.isShowing()) {
164            mPopup.show(mCoordinates.x, mCoordinates.y);
165        }
166        mWidthChanged = false;
167        return this;
168    }
169
170    /**
171     * Updates this floating toolbar to reflect recent position and view updates.
172     * NOTE: This method is a no-op if the toolbar isn't showing.
173     */
174    public FloatingToolbar updateLayout() {
175        if (mPopup.isShowing()) {
176            // show() performs all the logic we need here.
177            show();
178        }
179        return this;
180    }
181
182    /**
183     * Dismisses this floating toolbar.
184     */
185    public void dismiss() {
186        mPopup.dismiss();
187    }
188
189    /**
190     * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
191     * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
192     */
193    public void hide() {
194        mPopup.hide();
195    }
196
197    /**
198     * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
199     */
200    public boolean isShowing() {
201        return mPopup.isShowing();
202    }
203
204    /**
205     * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
206     */
207    public boolean isHidden() {
208        return mPopup.isHidden();
209    }
210
211    /**
212     * Refreshes {@link #mCoordinates} with values based on {@link #mContentRect}.
213     */
214    private void refreshCoordinates() {
215        int x = mContentRect.centerX() - mPopup.getWidth() / 2;
216        int y;
217        if (mContentRect.top > mPopup.getHeight()) {
218            y = mContentRect.top - mPopup.getHeight();
219            mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_UP;
220        } else if (mContentRect.top > mPopup.getToolbarHeightWithVerticalMargin()) {
221            y = mContentRect.top - mPopup.getToolbarHeightWithVerticalMargin();
222            mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
223        } else {
224            y = mContentRect.bottom;
225            mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
226        }
227        mCoordinates.set(x, y);
228    }
229
230    /**
231     * Returns true if this floating toolbar is currently showing the specified menu items.
232     */
233    private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
234        return mShowingTitles.equals(getMenuItemTitles(menuItems));
235    }
236
237    /**
238     * Returns the visible and enabled menu items in the specified menu.
239     * This method is recursive.
240     */
241    private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
242        List<MenuItem> menuItems = new ArrayList<MenuItem>();
243        for (int i = 0; (menu != null) && (i < menu.size()); i++) {
244            MenuItem menuItem = menu.getItem(i);
245            if (menuItem.isVisible() && menuItem.isEnabled()) {
246                Menu subMenu = menuItem.getSubMenu();
247                if (subMenu != null) {
248                    menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
249                } else {
250                    menuItems.add(menuItem);
251                }
252            }
253        }
254        return menuItems;
255    }
256
257    private List<CharSequence> getMenuItemTitles(List<MenuItem> menuItems) {
258        List<CharSequence> titles = new ArrayList<CharSequence>();
259        for (MenuItem menuItem : menuItems) {
260            titles.add(menuItem.getTitle());
261        }
262        return titles;
263    }
264
265
266    /**
267     * A popup window used by the floating toolbar.
268     *
269     * This class is responsible for the rendering/animation of the floating toolbar.
270     * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time.
271     * It delegates specific panel functionality to the appropriate panel.
272     */
273    private static final class FloatingToolbarPopup {
274
275        public static final int OVERFLOW_DIRECTION_UP = 0;
276        public static final int OVERFLOW_DIRECTION_DOWN = 1;
277
278        private final View mParent;
279        private final PopupWindow mPopupWindow;
280        private final ViewGroup mContentContainer;
281        private final int mMarginHorizontal;
282        private final int mMarginVertical;
283
284        private final Animation.AnimationListener mOnOverflowOpened =
285                new Animation.AnimationListener() {
286                    @Override
287                    public void onAnimationStart(Animation animation) {}
288
289                    @Override
290                    public void onAnimationEnd(Animation animation) {
291                        setOverflowPanelAsContent();
292                        mOverflowPanel.fadeIn(true);
293                    }
294
295                    @Override
296                    public void onAnimationRepeat(Animation animation) {}
297                };
298        private final Animation.AnimationListener mOnOverflowClosed =
299                new Animation.AnimationListener() {
300                    @Override
301                    public void onAnimationStart(Animation animation) {}
302
303                    @Override
304                    public void onAnimationEnd(Animation animation) {
305                        setMainPanelAsContent();
306                        mMainPanel.fadeIn(true);
307                    }
308
309                    @Override
310                    public void onAnimationRepeat(Animation animation) {
311                    }
312                };
313        private final AnimatorSet mShowAnimation;
314        private final AnimatorSet mDismissAnimation;
315        private final AnimatorSet mHideAnimation;
316        private final AnimationSet mOpenOverflowAnimation = new AnimationSet(true) {
317            @Override
318            public void cancel() {
319                if (hasStarted() && !hasEnded()) {
320                    super.cancel();
321                    setOverflowPanelAsContent();
322                }
323            }
324        };
325        private final AnimationSet mCloseOverflowAnimation = new AnimationSet(true) {
326            @Override
327            public void cancel() {
328                if (hasStarted() && !hasEnded()) {
329                    super.cancel();
330                    setMainPanelAsContent();
331                }
332            }
333        };
334
335        private final Runnable mOpenOverflow = new Runnable() {
336            @Override
337            public void run() {
338                openOverflow();
339            }
340        };
341        private final Runnable mCloseOverflow = new Runnable() {
342            @Override
343            public void run() {
344                closeOverflow();
345            }
346        };
347
348        private final Region mTouchableRegion = new Region();
349        private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
350                new ViewTreeObserver.OnComputeInternalInsetsListener() {
351                    public void onComputeInternalInsets(
352                            ViewTreeObserver.InternalInsetsInfo info) {
353                        info.contentInsets.setEmpty();
354                        info.visibleInsets.setEmpty();
355                        info.touchableRegion.set(mTouchableRegion);
356                        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
357                                .TOUCHABLE_INSETS_REGION);
358                    }
359                };
360
361        private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
362        private boolean mHidden; // tracks whether this popup is hidden or hiding.
363
364        private FloatingToolbarOverflowPanel mOverflowPanel;
365        private FloatingToolbarMainPanel mMainPanel;
366        private int mOverflowDirection;
367
368        /**
369         * Initializes a new floating toolbar popup.
370         *
371         * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
372         *      from.
373         */
374        public FloatingToolbarPopup(View parent) {
375            mParent = Preconditions.checkNotNull(parent);
376            mContentContainer = createContentContainer(parent.getContext());
377            mPopupWindow = createPopupWindow(mContentContainer);
378            mShowAnimation = createGrowFadeInFromBottom(mContentContainer);
379            mDismissAnimation = createShrinkFadeOutFromBottomAnimation(
380                    mContentContainer,
381                    new AnimatorListenerAdapter() {
382                        @Override
383                        public void onAnimationEnd(Animator animation) {
384                            mPopupWindow.dismiss();
385                            mContentContainer.removeAllViews();
386                        }
387                    });
388            mHideAnimation = createShrinkFadeOutFromBottomAnimation(
389                    mContentContainer,
390                    new AnimatorListenerAdapter() {
391                        @Override
392                        public void onAnimationEnd(Animator animation) {
393                            mPopupWindow.dismiss();
394                        }
395                    });
396            mMarginHorizontal = parent.getResources()
397                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
398            mMarginVertical = parent.getResources()
399                    .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
400        }
401
402        /**
403         * Lays out buttons for the specified menu items.
404         */
405        public void layoutMenuItems(List<MenuItem> menuItems,
406                MenuItem.OnMenuItemClickListener menuItemClickListener, int suggestedWidth) {
407            mContentContainer.removeAllViews();
408            if (mMainPanel == null) {
409                mMainPanel = new FloatingToolbarMainPanel(mParent.getContext(), mOpenOverflow);
410            }
411            List<MenuItem> overflowMenuItems =
412                    mMainPanel.layoutMenuItems(menuItems, suggestedWidth);
413            mMainPanel.setOnMenuItemClickListener(menuItemClickListener);
414            if (!overflowMenuItems.isEmpty()) {
415                if (mOverflowPanel == null) {
416                    mOverflowPanel =
417                            new FloatingToolbarOverflowPanel(mParent.getContext(), mCloseOverflow);
418                }
419                mOverflowPanel.setMenuItems(overflowMenuItems);
420                mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener);
421            }
422            updatePopupSize();
423        }
424
425        /**
426         * Shows this popup at the specified coordinates.
427         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
428         */
429        public void show(int x, int y) {
430            if (isShowing()) {
431                return;
432            }
433
434            mHidden = false;
435            mDismissed = false;
436            cancelDismissAndHideAnimations();
437            cancelOverflowAnimations();
438            // Make sure a panel is set as the content.
439            if (mContentContainer.getChildCount() == 0) {
440                setMainPanelAsContent();
441                // If we're yet to show the popup, set the container visibility to zero.
442                // The "show" animation will make this visible.
443                mContentContainer.setAlpha(0);
444            }
445            preparePopupContent();
446            mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, x, y);
447            setTouchableSurfaceInsetsComputer();
448            runShowAnimation();
449        }
450
451        /**
452         * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
453         */
454        public void dismiss() {
455            if (mDismissed) {
456                return;
457            }
458
459            mHidden = false;
460            mDismissed = true;
461            mHideAnimation.cancel();
462            runDismissAnimation();
463            setZeroTouchableSurface();
464        }
465
466        /**
467         * Hides this popup. This is a no-op if this popup is not showing.
468         * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
469         */
470        public void hide() {
471            if (!isShowing()) {
472                return;
473            }
474
475            mHidden = true;
476            runHideAnimation();
477            setZeroTouchableSurface();
478        }
479
480        /**
481         * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
482         */
483        public boolean isShowing() {
484            return !mDismissed && !mHidden;
485        }
486
487        /**
488         * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
489         */
490        public boolean isHidden() {
491            return mHidden;
492        }
493
494        /**
495         * Updates the coordinates of this popup.
496         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
497         * This is a no-op if this popup is not showing.
498         */
499        public void updateCoordinates(int x, int y) {
500            if (!isShowing() || !mPopupWindow.isShowing()) {
501                return;
502            }
503
504            cancelOverflowAnimations();
505            preparePopupContent();
506            mPopupWindow.update(x, y, getWidth(), getHeight());
507        }
508
509        /**
510         * Sets the direction in which the overflow will open. i.e. up or down.
511         *
512         * @param overflowDirection Either {@link #OVERFLOW_DIRECTION_UP}
513         *   or {@link #OVERFLOW_DIRECTION_DOWN}.
514         */
515        public void setOverflowDirection(int overflowDirection) {
516            mOverflowDirection = overflowDirection;
517            if (mOverflowPanel != null) {
518                mOverflowPanel.setOverflowDirection(mOverflowDirection);
519            }
520        }
521
522        /**
523         * Returns the width of this popup.
524         */
525        public int getWidth() {
526            return mPopupWindow.getWidth();
527        }
528
529        /**
530         * Returns the height of this popup.
531         */
532        public int getHeight() {
533            return mPopupWindow.getHeight();
534        }
535
536        /**
537         * Returns the context this popup is running in.
538         */
539        public Context getContext() {
540            return mContentContainer.getContext();
541        }
542
543        int getToolbarHeightWithVerticalMargin() {
544            return getEstimatedToolbarHeight(mParent.getContext()) + mMarginVertical * 2;
545        }
546
547        /**
548         * Performs the "show" animation on the floating popup.
549         */
550        private void runShowAnimation() {
551            mShowAnimation.start();
552        }
553
554        /**
555         * Performs the "dismiss" animation on the floating popup.
556         */
557        private void runDismissAnimation() {
558            mDismissAnimation.start();
559        }
560
561        /**
562         * Performs the "hide" animation on the floating popup.
563         */
564        private void runHideAnimation() {
565            mHideAnimation.start();
566        }
567
568        private void cancelDismissAndHideAnimations() {
569            mDismissAnimation.cancel();
570            mHideAnimation.cancel();
571        }
572
573        private void cancelOverflowAnimations() {
574            mOpenOverflowAnimation.cancel();
575            mCloseOverflowAnimation.cancel();
576        }
577
578        /**
579         * Opens the floating toolbar overflow.
580         * This method should not be called if menu items have not been laid out with
581         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
582         *
583         * @throws IllegalStateException if called when menu items have not been laid out.
584         */
585        private void openOverflow() {
586            Preconditions.checkState(mMainPanel != null);
587            Preconditions.checkState(mOverflowPanel != null);
588
589            mMainPanel.fadeOut(true);
590            Size overflowPanelSize = mOverflowPanel.measure();
591            final int targetWidth = overflowPanelSize.getWidth();
592            final int targetHeight = overflowPanelSize.getHeight();
593            final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
594            final int startWidth = mContentContainer.getWidth();
595            final int startHeight = mContentContainer.getHeight();
596            final float startY = mContentContainer.getY();
597            final float right = mContentContainer.getX() + mContentContainer.getWidth();
598            Animation widthAnimation = new Animation() {
599                @Override
600                protected void applyTransformation(float interpolatedTime, Transformation t) {
601                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
602                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
603                    params.width = startWidth + deltaWidth;
604                    mContentContainer.setLayoutParams(params);
605                    mContentContainer.setX(right - mContentContainer.getWidth());
606                }
607            };
608            Animation heightAnimation = new Animation() {
609                @Override
610                protected void applyTransformation(float interpolatedTime, Transformation t) {
611                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
612                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
613                    params.height = startHeight + deltaHeight;
614                    mContentContainer.setLayoutParams(params);
615                    if (morphUpwards) {
616                        float y = startY - (mContentContainer.getHeight() - startHeight);
617                        mContentContainer.setY(y);
618                    }
619                }
620            };
621            widthAnimation.setDuration(240);
622            heightAnimation.setDuration(180);
623            heightAnimation.setStartOffset(60);
624            mOpenOverflowAnimation.getAnimations().clear();
625            mOpenOverflowAnimation.setAnimationListener(mOnOverflowOpened);
626            mOpenOverflowAnimation.addAnimation(widthAnimation);
627            mOpenOverflowAnimation.addAnimation(heightAnimation);
628            mContentContainer.startAnimation(mOpenOverflowAnimation);
629        }
630
631        /**
632         * Opens the floating toolbar overflow.
633         * This method should not be called if menu items have not been laid out with
634         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
635         *
636         * @throws IllegalStateException if called when menu items have not been laid out.
637         */
638        private void closeOverflow() {
639            Preconditions.checkState(mMainPanel != null);
640            Preconditions.checkState(mOverflowPanel != null);
641
642            mOverflowPanel.fadeOut(true);
643            Size mainPanelSize = mMainPanel.measure();
644            final int targetWidth = mainPanelSize.getWidth();
645            final int targetHeight = mainPanelSize.getHeight();
646            final int startWidth = mContentContainer.getWidth();
647            final int startHeight = mContentContainer.getHeight();
648            final float right = mContentContainer.getX() + mContentContainer.getWidth();
649            final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
650            final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
651            Animation widthAnimation = new Animation() {
652                @Override
653                protected void applyTransformation(float interpolatedTime, Transformation t) {
654                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
655                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
656                    params.width = startWidth + deltaWidth;
657                    mContentContainer.setLayoutParams(params);
658                    mContentContainer.setX(right - mContentContainer.getWidth());
659                }
660            };
661            Animation heightAnimation = new Animation() {
662                @Override
663                protected void applyTransformation(float interpolatedTime, Transformation t) {
664                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
665                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
666                    params.height = startHeight + deltaHeight;
667                    mContentContainer.setLayoutParams(params);
668                    if (morphedUpwards) {
669                        mContentContainer.setY(bottom - mContentContainer.getHeight());
670                    }
671                }
672            };
673            widthAnimation.setDuration(150);
674            widthAnimation.setStartOffset(150);
675            heightAnimation.setDuration(210);
676            mCloseOverflowAnimation.getAnimations().clear();
677            mCloseOverflowAnimation.setAnimationListener(mOnOverflowClosed);
678            mCloseOverflowAnimation.addAnimation(widthAnimation);
679            mCloseOverflowAnimation.addAnimation(heightAnimation);
680            mContentContainer.startAnimation(mCloseOverflowAnimation);
681        }
682
683        /**
684         * Prepares the content container for show and update calls.
685         */
686        private void preparePopupContent() {
687            // Reset visibility.
688            if (mMainPanel != null) {
689                mMainPanel.fadeIn(false);
690            }
691            if (mOverflowPanel != null) {
692                mOverflowPanel.fadeIn(false);
693            }
694
695            // Reset position.
696            if (mMainPanel != null
697                    && mContentContainer.getChildAt(0) == mMainPanel.getView()) {
698                positionMainPanel();
699            }
700            if (mOverflowPanel != null
701                    && mContentContainer.getChildAt(0) == mOverflowPanel.getView()) {
702                positionOverflowPanel();
703            }
704        }
705
706        /**
707         * Sets the current content to be the main view panel.
708         */
709        private void setMainPanelAsContent() {
710            // This should never be called if the main panel has not been initialized.
711            Preconditions.checkNotNull(mMainPanel);
712            mContentContainer.removeAllViews();
713            Size mainPanelSize = mMainPanel.measure();
714            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
715            params.width = mainPanelSize.getWidth();
716            params.height = mainPanelSize.getHeight();
717            mContentContainer.setLayoutParams(params);
718            mContentContainer.addView(mMainPanel.getView());
719            setContentAreaAsTouchableSurface();
720        }
721
722        /**
723         * Sets the current content to be the overflow view panel.
724         */
725        private void setOverflowPanelAsContent() {
726            // This should never be called if the overflow panel has not been initialized.
727            Preconditions.checkNotNull(mOverflowPanel);
728            mContentContainer.removeAllViews();
729            Size overflowPanelSize = mOverflowPanel.measure();
730            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
731            params.width = overflowPanelSize.getWidth();
732            params.height = overflowPanelSize.getHeight();
733            mContentContainer.setLayoutParams(params);
734            mContentContainer.addView(mOverflowPanel.getView());
735            setContentAreaAsTouchableSurface();
736        }
737
738        /**
739         * Places the main view panel at the appropriate resting coordinates.
740         */
741        private void positionMainPanel() {
742            Preconditions.checkNotNull(mMainPanel);
743            float x = mPopupWindow.getWidth()
744                    - (mMainPanel.getView().getMeasuredWidth() + mMarginHorizontal);
745            mContentContainer.setX(x);
746
747            float y = mMarginVertical;
748            if  (mOverflowDirection == OVERFLOW_DIRECTION_UP) {
749                y = getHeight()
750                        - (mMainPanel.getView().getMeasuredHeight() + mMarginVertical);
751            }
752            mContentContainer.setY(y);
753            setContentAreaAsTouchableSurface();
754        }
755
756        /**
757         * Places the main view panel at the appropriate resting coordinates.
758         */
759        private void positionOverflowPanel() {
760            Preconditions.checkNotNull(mOverflowPanel);
761            float x = mPopupWindow.getWidth()
762                    - (mOverflowPanel.getView().getMeasuredWidth() + mMarginHorizontal);
763            mContentContainer.setX(x);
764            mContentContainer.setY(mMarginVertical);
765            setContentAreaAsTouchableSurface();
766        }
767
768        private void updatePopupSize() {
769            int width = 0;
770            int height = 0;
771            if (mMainPanel != null) {
772                Size mainPanelSize = mMainPanel.measure();
773                width = mainPanelSize.getWidth();
774                height = mainPanelSize.getHeight();
775            }
776            if (mOverflowPanel != null) {
777                Size overflowPanelSize = mOverflowPanel.measure();
778                width = Math.max(width, overflowPanelSize.getWidth());
779                height = Math.max(height, overflowPanelSize.getHeight());
780            }
781            mPopupWindow.setWidth(width + mMarginHorizontal * 2);
782            mPopupWindow.setHeight(height + mMarginVertical * 2);
783        }
784
785        /**
786         * Sets the touchable region of this popup to be zero. This means that all touch events on
787         * this popup will go through to the surface behind it.
788         */
789        private void setZeroTouchableSurface() {
790            mTouchableRegion.setEmpty();
791        }
792
793        /**
794         * Sets the touchable region of this popup to be the area occupied by its content.
795         */
796        private void setContentAreaAsTouchableSurface() {
797            if (!mPopupWindow.isShowing()) {
798                mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
799            }
800            int width = mContentContainer.getMeasuredWidth();
801            int height = mContentContainer.getMeasuredHeight();
802            mTouchableRegion.set(
803                    (int) mContentContainer.getX(),
804                    (int) mContentContainer.getY(),
805                    (int) mContentContainer.getX() + width,
806                    (int) mContentContainer.getY() + height);
807        }
808
809        /**
810         * Make the touchable area of this popup be the area specified by mTouchableRegion.
811         * This should be called after the popup window has been dismissed (dismiss/hide)
812         * and is probably being re-shown with a new content root view.
813         */
814        private void setTouchableSurfaceInsetsComputer() {
815            ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
816                    .getRootView()
817                    .getViewTreeObserver();
818            viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
819            viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
820        }
821    }
822
823    /**
824     * A widget that holds the primary menu items in the floating toolbar.
825     */
826    private static final class FloatingToolbarMainPanel {
827
828        private final Context mContext;
829        private final ViewGroup mContentView;
830        private final View.OnClickListener mMenuItemButtonOnClickListener =
831                new View.OnClickListener() {
832                    @Override
833                    public void onClick(View v) {
834                        if (v.getTag() instanceof MenuItem) {
835                            if (mOnMenuItemClickListener != null) {
836                                mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
837                            }
838                        }
839                    }
840                };
841        private final ViewFader viewFader;
842        private final Runnable mOpenOverflow;
843
844        private View mOpenOverflowButton;
845        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
846
847        /**
848         * Initializes a floating toolbar popup main view panel.
849         *
850         * @param context
851         * @param openOverflow  The code that opens the toolbar popup overflow.
852         */
853        public FloatingToolbarMainPanel(Context context, Runnable openOverflow) {
854            mContext = Preconditions.checkNotNull(context);
855            mContentView = new LinearLayout(context);
856            viewFader = new ViewFader(mContentView);
857            mOpenOverflow = Preconditions.checkNotNull(openOverflow);
858        }
859
860        /**
861         * Fits as many menu items in the main panel and returns a list of the menu items that
862         * were not fit in.
863         *
864         * @return The menu items that are not included in this main panel.
865         */
866        public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int suggestedWidth) {
867            final int toolbarWidth = getAdjustedToolbarWidth(mContext, suggestedWidth)
868                    // Reserve space for the "open overflow" button.
869                    - getEstimatedOpenOverflowButtonWidth(mContext);
870
871            int availableWidth = toolbarWidth;
872            final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
873
874            mContentView.removeAllViews();
875
876            boolean isFirstItem = true;
877            while (!remainingMenuItems.isEmpty()) {
878                final MenuItem menuItem = remainingMenuItems.peek();
879                Button menuItemButton = createMenuItemButton(mContext, menuItem);
880
881                // Adding additional start padding for the first button to even out button spacing.
882                if (isFirstItem) {
883                    menuItemButton.setPaddingRelative(
884                            (int) (1.5 * menuItemButton.getPaddingStart()),
885                            menuItemButton.getPaddingTop(),
886                            menuItemButton.getPaddingEnd(),
887                            menuItemButton.getPaddingBottom());
888                    isFirstItem = false;
889                }
890
891                // Adding additional end padding for the last button to even out button spacing.
892                if (remainingMenuItems.size() == 1) {
893                    menuItemButton.setPaddingRelative(
894                            menuItemButton.getPaddingStart(),
895                            menuItemButton.getPaddingTop(),
896                            (int) (1.5 * menuItemButton.getPaddingEnd()),
897                            menuItemButton.getPaddingBottom());
898                }
899
900                menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
901                int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
902                if (menuItemButtonWidth <= availableWidth) {
903                    menuItemButton.setTag(menuItem);
904                    menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
905                    mContentView.addView(menuItemButton);
906                    ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
907                    params.width = menuItemButtonWidth;
908                    menuItemButton.setLayoutParams(params);
909                    availableWidth -= menuItemButtonWidth;
910                    remainingMenuItems.pop();
911                } else {
912                    if (mOpenOverflowButton == null) {
913                        mOpenOverflowButton = (ImageButton) LayoutInflater.from(mContext)
914                                .inflate(R.layout.floating_popup_open_overflow_button, null);
915                        mOpenOverflowButton.setOnClickListener(new View.OnClickListener() {
916                            @Override
917                            public void onClick(View v) {
918                                if (mOpenOverflowButton != null) {
919                                    mOpenOverflow.run();
920                                }
921                            }
922                        });
923                    }
924                    mContentView.addView(mOpenOverflowButton);
925                    break;
926                }
927            }
928            return remainingMenuItems;
929        }
930
931        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
932            mOnMenuItemClickListener = listener;
933        }
934
935        public View getView() {
936            return mContentView;
937        }
938
939        public void fadeIn(boolean animate) {
940            viewFader.fadeIn(animate);
941        }
942
943        public void fadeOut(boolean animate) {
944            viewFader.fadeOut(animate);
945        }
946
947        /**
948         * Returns how big this panel's view should be.
949         * This method should only be called when the view has not been attached to a parent
950         * otherwise it will throw an illegal state.
951         */
952        public Size measure() throws IllegalStateException {
953            Preconditions.checkState(mContentView.getParent() == null);
954            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
955            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
956        }
957    }
958
959
960    /**
961     * A widget that holds the overflow items in the floating toolbar.
962     */
963    private static final class FloatingToolbarOverflowPanel {
964
965        private final LinearLayout mContentView;
966        private final ViewGroup mBackButtonContainer;
967        private final View mBackButton;
968        private final ListView mListView;
969        private final TextView mListViewItemWidthCalculator;
970        private final ViewFader mViewFader;
971        private final Runnable mCloseOverflow;
972
973        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
974        private int mOverflowWidth = 0;
975
976        /**
977         * Initializes a floating toolbar popup overflow view panel.
978         *
979         * @param context
980         * @param closeOverflow  The code that closes the toolbar popup's overflow.
981         */
982        public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) {
983            mCloseOverflow = Preconditions.checkNotNull(closeOverflow);
984
985            mContentView = new LinearLayout(context);
986            mContentView.setOrientation(LinearLayout.VERTICAL);
987            mViewFader = new ViewFader(mContentView);
988
989            mBackButton = LayoutInflater.from(context)
990                    .inflate(R.layout.floating_popup_close_overflow_button, null);
991            mBackButton.setOnClickListener(new View.OnClickListener() {
992                @Override
993                public void onClick(View v) {
994                    mCloseOverflow.run();
995                }
996            });
997            mBackButtonContainer = new LinearLayout(context);
998            mBackButtonContainer.addView(mBackButton);
999
1000            mListView = createOverflowListView();
1001            mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1002                @Override
1003                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1004                    MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position);
1005                    if (mOnMenuItemClickListener != null) {
1006                        mOnMenuItemClickListener.onMenuItemClick(menuItem);
1007                    }
1008                }
1009            });
1010
1011            mContentView.addView(mListView);
1012            mContentView.addView(mBackButtonContainer);
1013
1014            mListViewItemWidthCalculator = createOverflowMenuItemButton(context);
1015            mListViewItemWidthCalculator.setLayoutParams(new ViewGroup.LayoutParams(
1016                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1017        }
1018
1019        /**
1020         * Sets the menu items to be displayed in the overflow.
1021         */
1022        public void setMenuItems(List<MenuItem> menuItems) {
1023            ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter();
1024            overflowListViewAdapter.clear();
1025            overflowListViewAdapter.addAll(menuItems);
1026            setListViewHeight();
1027            setOverflowWidth();
1028        }
1029
1030        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1031            mOnMenuItemClickListener = listener;
1032        }
1033
1034        /**
1035         * Notifies the overflow of the current direction in which the overflow will be opened.
1036         *
1037         * @param overflowDirection  {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP}
1038         *   or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}.
1039         */
1040        public void setOverflowDirection(int overflowDirection) {
1041            mContentView.removeView(mBackButtonContainer);
1042            int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0;
1043            mContentView.addView(mBackButtonContainer, index);
1044        }
1045
1046        /**
1047         * Returns the content view of the overflow.
1048         */
1049        public View getView() {
1050            return mContentView;
1051        }
1052
1053        public void fadeIn(boolean animate) {
1054            mViewFader.fadeIn(animate);
1055        }
1056
1057        public void fadeOut(boolean animate) {
1058            mViewFader.fadeOut(animate);
1059        }
1060
1061        /**
1062         * Returns how big this panel's view should be.
1063         * This method should only be called when the view has not been attached to a parent.
1064         *
1065         * @throws IllegalStateException
1066         */
1067        public Size measure() {
1068            Preconditions.checkState(mContentView.getParent() == null);
1069            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1070            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1071        }
1072
1073        private void setListViewHeight() {
1074            int itemHeight = getEstimatedToolbarHeight(mContentView.getContext());
1075            int height = mListView.getAdapter().getCount() * itemHeight;
1076            int maxHeight = mContentView.getContext().getResources().
1077                    getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
1078            ViewGroup.LayoutParams params = mListView.getLayoutParams();
1079            params.height = Math.min(height, maxHeight);
1080            mListView.setLayoutParams(params);
1081        }
1082
1083        private int setOverflowWidth() {
1084            for (int i = 0; i < mListView.getAdapter().getCount(); i++) {
1085                MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(i);
1086                Preconditions.checkNotNull(menuItem);
1087                mListViewItemWidthCalculator.setText(menuItem.getTitle());
1088                mListViewItemWidthCalculator.measure(
1089                        MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1090                mOverflowWidth = Math.max(
1091                        mListViewItemWidthCalculator.getMeasuredWidth(), mOverflowWidth);
1092            }
1093            return mOverflowWidth;
1094        }
1095
1096        private ListView createOverflowListView() {
1097            final Context context = mContentView.getContext();
1098            final ListView overflowListView = new ListView(context);
1099            overflowListView.setLayoutParams(new ViewGroup.LayoutParams(
1100                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1101            overflowListView.setDivider(null);
1102            overflowListView.setDividerHeight(0);
1103            final ArrayAdapter overflowListViewAdapter =
1104                    new ArrayAdapter<MenuItem>(context, 0) {
1105                        @Override
1106                        public View getView(int position, View convertView, ViewGroup parent) {
1107                            TextView menuButton;
1108                            if (convertView != null) {
1109                                menuButton = (TextView) convertView;
1110                            } else {
1111                                menuButton = createOverflowMenuItemButton(context);
1112                            }
1113                            MenuItem menuItem = getItem(position);
1114                            menuButton.setText(menuItem.getTitle());
1115                            menuButton.setContentDescription(menuItem.getTitle());
1116                            menuButton.setMinimumWidth(mOverflowWidth);
1117                            return menuButton;
1118                        }
1119                    };
1120            overflowListView.setAdapter(overflowListViewAdapter);
1121            return overflowListView;
1122        }
1123    }
1124
1125
1126    /**
1127     * A helper for fading in or out a view.
1128     */
1129    private static final class ViewFader {
1130
1131        private static final int FADE_OUT_DURATION = 250;
1132        private static final int FADE_IN_DURATION = 150;
1133
1134        private final View mView;
1135        private final ObjectAnimator mFadeOutAnimation;
1136        private final ObjectAnimator mFadeInAnimation;
1137
1138        private ViewFader(View view) {
1139            mView = Preconditions.checkNotNull(view);
1140            mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0)
1141                    .setDuration(FADE_OUT_DURATION);
1142            mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1)
1143                    .setDuration(FADE_IN_DURATION);
1144        }
1145
1146        public void fadeIn(boolean animate) {
1147            cancelFadeAnimations();
1148            if (animate) {
1149                mFadeInAnimation.start();
1150            } else {
1151                mView.setAlpha(1);
1152            }
1153        }
1154
1155        public void fadeOut(boolean animate) {
1156            cancelFadeAnimations();
1157            if (animate) {
1158                mFadeOutAnimation.start();
1159            } else {
1160                mView.setAlpha(0);
1161            }
1162        }
1163
1164        private void cancelFadeAnimations() {
1165            mFadeInAnimation.cancel();
1166            mFadeOutAnimation.cancel();
1167        }
1168    }
1169
1170
1171    /**
1172     * Creates and returns a menu button for the specified menu item.
1173     */
1174    private static Button createMenuItemButton(Context context, MenuItem menuItem) {
1175        Button menuItemButton = (Button) LayoutInflater.from(context)
1176                .inflate(R.layout.floating_popup_menu_button, null);
1177        menuItemButton.setText(menuItem.getTitle());
1178        menuItemButton.setContentDescription(menuItem.getTitle());
1179        return menuItemButton;
1180    }
1181
1182    /**
1183     * Creates and returns a styled floating toolbar overflow list view item.
1184     */
1185    private static TextView createOverflowMenuItemButton(Context context) {
1186        return (TextView) LayoutInflater.from(context)
1187                .inflate(R.layout.floating_popup_overflow_list_item, null);
1188    }
1189
1190    private static ViewGroup createContentContainer(Context context) {
1191        return (ViewGroup) LayoutInflater.from(context)
1192                .inflate(R.layout.floating_popup_container, null);
1193    }
1194
1195    private static PopupWindow createPopupWindow(View content) {
1196        ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1197        PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1198        popupWindow.setWindowLayoutType(
1199                WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1200        popupWindow.setAnimationStyle(0);
1201        popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1202        content.setLayoutParams(new ViewGroup.LayoutParams(
1203                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1204        popupContentHolder.addView(content);
1205        return popupWindow;
1206    }
1207
1208    /**
1209     * Creates a "grow and fade in from the bottom" animation for the specified view.
1210     *
1211     * @param view  The view to animate
1212     */
1213    private static AnimatorSet createGrowFadeInFromBottom(View view) {
1214        AnimatorSet growFadeInFromBottomAnimation =  new AnimatorSet();
1215        growFadeInFromBottomAnimation.playTogether(
1216                ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125),
1217                ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125),
1218                ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75));
1219        growFadeInFromBottomAnimation.setStartDelay(50);
1220        return growFadeInFromBottomAnimation;
1221    }
1222
1223    /**
1224     * Creates a "shrink and fade out from bottom" animation for the specified view.
1225     *
1226     * @param view  The view to animate
1227     * @param listener  The animation listener
1228     */
1229    private static AnimatorSet createShrinkFadeOutFromBottomAnimation(
1230            View view, Animator.AnimatorListener listener) {
1231        AnimatorSet shrinkFadeOutFromBottomAnimation =  new AnimatorSet();
1232        shrinkFadeOutFromBottomAnimation.playTogether(
1233                ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125),
1234                ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75));
1235        shrinkFadeOutFromBottomAnimation.setStartDelay(150);
1236        shrinkFadeOutFromBottomAnimation.addListener(listener);
1237        return shrinkFadeOutFromBottomAnimation;
1238    }
1239
1240    private static int getEstimatedToolbarHeight(Context context) {
1241        return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1242    }
1243
1244    private static int getEstimatedOpenOverflowButtonWidth(Context context) {
1245        return context.getResources()
1246                .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
1247    }
1248
1249    private static int getAdjustedToolbarWidth(Context context, int width) {
1250        int maximumWidth = getScreenWidth(context) - 2 * context.getResources()
1251                .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1252
1253        if (width <= 0 || width > maximumWidth) {
1254            int defaultWidth = context.getResources()
1255                    .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1256            width = Math.min(defaultWidth, maximumWidth);
1257        }
1258        return width;
1259    }
1260
1261    /**
1262     * Returns the device's screen width.
1263     */
1264    private static int getScreenWidth(Context context) {
1265        return context.getResources().getDisplayMetrics().widthPixels;
1266    }
1267
1268    /**
1269     * Returns the device's screen height.
1270     */
1271    private static int getScreenHeight(Context context) {
1272        return context.getResources().getDisplayMetrics().heightPixels;
1273    }
1274
1275    /**
1276     * Returns value, restricted to the range min->max (inclusive).
1277     * If maximum is less than minimum, the result is undefined.
1278     *
1279     * @param value  The value to clamp.
1280     * @param minimum  The minimum value in the range.
1281     * @param maximum  The maximum value in the range. Must not be less than minimum.
1282     */
1283    private static int clamp(int value, int minimum, int maximum) {
1284        return Math.max(minimum, Math.min(value, maximum));
1285    }
1286}
1287