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