1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.chrome.browser.infobar;
6
7import android.animation.Animator;
8import android.animation.AnimatorListenerAdapter;
9import android.animation.AnimatorSet;
10import android.animation.ObjectAnimator;
11import android.animation.PropertyValuesHolder;
12import android.os.Build;
13import android.view.View;
14import android.view.ViewTreeObserver;
15import android.view.animation.AccelerateDecelerateInterpolator;
16import android.widget.LinearLayout;
17
18import org.chromium.base.ApiCompatibilityUtils;
19
20import java.util.ArrayList;
21
22/**
23 * Sets up animations to move InfoBars around inside of the InfoBarContainer.
24 *
25 * Animations proceed in several phases:
26 * 1) Prep work is done for the InfoBar so that the View being animated in (if it exists) is
27 *    properly sized.  This involves adding the View to a FrameLayout with a visibility of
28 *    INVISIBLE and triggering a layout.
29 *
30 * 2) Once the View has an actual size, we compute all of the actions needed for the animation.
31 *    We use translations primarily to slide things in and out of the screen as things are shown,
32 *    hidden, or resized.
33 *
34 * 3) The animation is kicked off and the animations run.  During this phase, the View being shown
35 *    is added to ContentWrapperView.
36 *
37 * 4) At the end of the animation, we clean up everything and make sure all the children are in the
38 *    right places.
39 */
40public class AnimationHelper implements ViewTreeObserver.OnGlobalLayoutListener {
41    private static final long ANIMATION_DURATION_MS = 250;
42
43    public static final int ANIMATION_TYPE_SHOW = 0;
44    public static final int ANIMATION_TYPE_SWAP = 1;
45    public static final int ANIMATION_TYPE_HIDE = 2;
46    public static final int ANIMATION_TYPE_BOUNDARY = 3;
47
48    private final InfoBarContainer mContainer;
49    private final LinearLayout mLinearLayout;
50    private final InfoBar mInfoBar;
51    private final ContentWrapperView mTargetWrapperView;
52    private final AnimatorSet mAnimatorSet;
53    private final int mAnimationType;
54    private final View mToShow;
55
56    private boolean mAnimationStarted;
57
58    /**
59     * Creates and starts an animation.
60     * @param container InfoBarContainer that is having its InfoBars animated.
61     * @param target ContentWrapperView that is the focus of the animation and is being resized,
62     *               shown, or hidden.
63     * @param infoBar InfoBar that goes with the specified ContentWrapperView.
64     * @param toShow If non-null, this View will replace whatever child View the ContentWrapperView
65     *               is currently displaying.
66     * @param animationType Type of animation being performed.
67     */
68    public AnimationHelper(InfoBarContainer container, ContentWrapperView target, InfoBar infoBar,
69            View toShow, int animationType) {
70        mContainer = container;
71        mLinearLayout = container.getLinearLayout();
72        mInfoBar = infoBar;
73        mTargetWrapperView = target;
74        mAnimatorSet = new AnimatorSet();
75        mAnimationType = animationType;
76        mToShow = toShow;
77        assert mLinearLayout.indexOfChild(mTargetWrapperView) != -1;
78    }
79
80    /**
81     * Start the animation.
82     */
83    public void start() {
84        mTargetWrapperView.prepareTransition(mToShow);
85        mContainer.prepareTransition(mToShow);
86
87        if (mToShow == null) {
88            // We've got a size already; start the animation immediately.
89            continueAnimation();
90        } else {
91            // Wait for the object to be sized.
92            mTargetWrapperView.getViewTreeObserver().addOnGlobalLayoutListener(this);
93        }
94    }
95
96    /**
97     * @return the InfoBar being animated.
98     */
99    public InfoBar getInfoBar() {
100        return mInfoBar;
101    }
102
103    /**
104     * @return the ContentWrapperView being animated.
105     */
106    public ContentWrapperView getTarget() {
107        return mTargetWrapperView;
108    }
109
110    /**
111     * @return the type of animation being performed.
112     */
113    public int getAnimationType() {
114        return mAnimationType;
115    }
116
117    /**
118     * Catch when the layout occurs, which lets us know when the View has been sized properly.
119     */
120    @Override
121    public void onGlobalLayout() {
122        ApiCompatibilityUtils.removeOnGlobalLayoutListener(mTargetWrapperView, this);
123        continueAnimation();
124    }
125
126    private void continueAnimation() {
127        if (mAnimationStarted) return;
128        mAnimationStarted = true;
129
130        int indexOfWrapperView = mLinearLayout.indexOfChild(mTargetWrapperView);
131        assert indexOfWrapperView != -1;
132
133        ArrayList<Animator> animators = new ArrayList<Animator>();
134        mTargetWrapperView.getAnimationsForTransition(animators);
135
136        // Determine where the tops of each InfoBar will need to be.
137        int heightDifference = mTargetWrapperView.getTransitionHeightDifference();
138        int cumulativeTopStart = 0;
139        int cumulativeTopEnd = 0;
140        int cumulativeEndHeight = 0;
141        if (heightDifference >= 0) {
142            // The current container is smaller than the final container, so the current 0
143            // coordinate will be >= 0 in the final container.
144            cumulativeTopStart = heightDifference;
145        } else {
146            // The current container is bigger than the final container, so the current 0
147            // coordinate will be < 0 in the final container.
148            cumulativeTopEnd = -heightDifference;
149        }
150
151        for (int i = 0; i < mLinearLayout.getChildCount(); ++i) {
152            View view = mLinearLayout.getChildAt(i);
153
154            // At this point, the View being transitioned in shouldn't have been added to the
155            // visible container, yet, and shouldn't affect calculations.
156            int startHeight = view.getHeight();
157            int endHeight = startHeight + (i == indexOfWrapperView ? heightDifference : 0);
158            int topStart = cumulativeTopStart;
159            int topEnd = cumulativeTopEnd;
160            int bottomStart = topStart + startHeight;
161            int bottomEnd = topEnd + endHeight;
162
163            if (topStart == topEnd && bottomStart == bottomEnd) {
164                // The View needs to stay put.
165                view.setTop(topEnd);
166                view.setBottom(bottomEnd);
167                view.setY(topEnd);
168                view.setTranslationY(0);
169            } else {
170                // A translation is required to move the View into place.
171                int translation = heightDifference;
172
173                boolean translateDownward;
174                if (topStart < topEnd) {
175                    translateDownward = false;
176                } else if (topStart > topEnd) {
177                    translateDownward = true;
178                } else {
179                    translateDownward = bottomEnd > bottomStart;
180                }
181
182                PropertyValuesHolder viewTranslation;
183                if (translateDownward) {
184                    view.setTop(topEnd);
185                    view.setBottom(bottomEnd);
186                    view.setTranslationY(translation);
187                    view.setY(topEnd + translation);
188                    viewTranslation =
189                            PropertyValuesHolder.ofFloat("translationY", translation, 0.0f);
190                } else {
191                    viewTranslation =
192                            PropertyValuesHolder.ofFloat("translationY", 0.0f, -translation);
193                }
194
195                animators.add(ObjectAnimator.ofPropertyValuesHolder(view, viewTranslation));
196            }
197
198            // Add heights to the cumulative totals.
199            cumulativeTopStart += startHeight;
200            cumulativeTopEnd += endHeight;
201            cumulativeEndHeight += endHeight;
202        }
203
204        // Lock the InfoBarContainer's size at its largest during the animation to avoid
205        // clipping issues.
206        int oldContainerTop = mLinearLayout.getTop();
207        int newContainerTop = mLinearLayout.getBottom() - cumulativeEndHeight;
208        int biggestContainerTop = Math.min(oldContainerTop, newContainerTop);
209        mLinearLayout.setTop(biggestContainerTop);
210
211        // Set up and run all of the animations.
212        mAnimatorSet.addListener(new AnimatorListenerAdapter() {
213            @Override
214            public void onAnimationStart(Animator animation) {
215                mTargetWrapperView.startTransition();
216            }
217
218            @Override
219            public void onAnimationEnd(Animator animation) {
220                mTargetWrapperView.finishTransition();
221                mContainer.finishTransition();
222
223                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && mToShow != null &&
224                        (mAnimationType == ANIMATION_TYPE_SHOW ||
225                                mAnimationType == ANIMATION_TYPE_SWAP)) {
226                    mToShow.announceForAccessibility(mInfoBar.getMessage());
227                }
228            }
229        });
230
231        mAnimatorSet.playTogether(animators);
232        mAnimatorSet.setDuration(ANIMATION_DURATION_MS);
233        mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
234        mAnimatorSet.start();
235    }
236}
237