BoundsAnimationController.java revision b003364bc7e31e7f85865ef298d399ed61c372a2
1/*
2 * Copyright (C) 2016 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.server.wm;
18
19import static com.android.server.wm.AppTransition.DEFAULT_APP_TRANSITION_DURATION;
20import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
21import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
22import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
23
24import android.animation.Animator;
25import android.animation.ValueAnimator;
26import android.graphics.Rect;
27import android.os.Debug;
28import android.util.ArrayMap;
29import android.util.Slog;
30import android.view.animation.LinearInterpolator;
31
32/**
33 * Enables animating bounds of objects.
34 *
35 * In multi-window world bounds of both stack and tasks can change. When we need these bounds to
36 * change smoothly and not require the app to relaunch (e.g. because it handles resizes and
37 * relaunching it would cause poorer experience), these class provides a way to directly animate
38 * the bounds of the resized object.
39 *
40 * The object that is resized needs to implement {@link AnimateBoundsUser} interface.
41 */
42public class BoundsAnimationController {
43    private static final boolean DEBUG_LOCAL = false;
44    private static final boolean DEBUG = DEBUG_LOCAL || DEBUG_ANIM;
45    private static final String TAG = TAG_WITH_CLASS_NAME || DEBUG_LOCAL
46            ? "BoundsAnimationController" : TAG_WM;
47    private static final int DEBUG_ANIMATION_SLOW_DOWN_FACTOR = 1;
48
49    // Only accessed on UI thread.
50    private ArrayMap<AnimateBoundsUser, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
51
52    private final class BoundsAnimator extends ValueAnimator
53            implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
54        private final AnimateBoundsUser mTarget;
55        private final Rect mFrom;
56        private final Rect mTo;
57        private final Rect mTmpRect = new Rect();
58        private final Rect mTmpTaskBounds = new Rect();
59        private final boolean mMoveToFullScreen;
60        // True if this this animation was cancelled and will be replaced the another animation from
61        // the same {@link #AnimateBoundsUser} target.
62        private boolean mWillReplace;
63        // True to true if this animation replaced a previous animation of the same
64        // {@link #AnimateBoundsUser} target.
65        private final boolean mReplacement;
66
67        // Depending on whether we are animating from
68        // a smaller to a larger size
69        private final int mFrozenTaskWidth;
70        private final int mFrozenTaskHeight;
71
72        BoundsAnimator(AnimateBoundsUser target, Rect from, Rect to,
73                boolean moveToFullScreen, boolean replacement) {
74            super();
75            mTarget = target;
76            mFrom = from;
77            mTo = to;
78            mMoveToFullScreen = moveToFullScreen;
79            mReplacement = replacement;
80            addUpdateListener(this);
81            addListener(this);
82
83            // If we are animating from smaller to larger, we want to change the task bounds
84            // to their final size immediately so we can use scaling to make the window
85            // larger. Likewise if we are going from bigger to smaller, we want to wait until
86            // the end so we don't have to upscale from the smaller finished size.
87            if (animatingToLargerSize()) {
88                mFrozenTaskWidth = mTo.width();
89                mFrozenTaskHeight = mTo.height();
90            } else {
91                mFrozenTaskWidth = mFrom.width();
92                mFrozenTaskHeight = mFrom.height();
93            }
94        }
95
96        boolean animatingToLargerSize() {
97            if (mFrom.width() * mFrom.height() > mTo.width() * mTo.height()) {
98                return false;
99            }
100            return true;
101        }
102
103        @Override
104        public void onAnimationUpdate(ValueAnimator animation) {
105            final float value = (Float) animation.getAnimatedValue();
106            final float remains = 1 - value;
107            mTmpRect.left = (int) (mFrom.left * remains + mTo.left * value + 0.5f);
108            mTmpRect.top = (int) (mFrom.top * remains + mTo.top * value + 0.5f);
109            mTmpRect.right = (int) (mFrom.right * remains + mTo.right * value + 0.5f);
110            mTmpRect.bottom = (int) (mFrom.bottom * remains + mTo.bottom * value + 0.5f);
111            if (DEBUG) Slog.d(TAG, "animateUpdate: mTarget=" + mTarget + " mBounds="
112                    + mTmpRect + " from=" + mFrom + " mTo=" + mTo + " value=" + value
113                    + " remains=" + remains);
114
115            if (remains != 0) {
116                mTmpTaskBounds.set(mTmpRect.left, mTmpRect.top,
117                        mTmpRect.left + mFrozenTaskWidth, mTmpRect.top + mFrozenTaskHeight);
118            }
119
120            if (!mTarget.setPinnedStackSize(mTmpRect, remains != 0 ? mTmpTaskBounds : null)) {
121                // Whoops, the target doesn't feel like animating anymore. Let's immediately finish
122                // any further animation.
123                animation.cancel();
124            }
125        }
126
127
128        @Override
129        public void onAnimationStart(Animator animation) {
130            if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget
131                    + " mReplacement=" + mReplacement);
132            // Ensure that we have prepared the target for animation before
133            // we trigger any size changes, so it can swap surfaces
134            // in to appropriate modes, or do as it wishes otherwise.
135            if (!mReplacement) {
136                mTarget.onAnimationStart();
137            }
138
139            // Immediately update the task bounds if they have to become larger, but preserve
140            // the starting position so we don't jump at the beginning of the animation.
141            if (animatingToLargerSize()) {
142                mTmpRect.set(mFrom.left, mFrom.top,
143                        mFrom.left + mFrozenTaskWidth, mFrom.top + mFrozenTaskHeight);
144                mTarget.setPinnedStackSize(mFrom, mTmpRect);
145            }
146        }
147
148        @Override
149        public void onAnimationEnd(Animator animation) {
150            if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
151                    + " mMoveToFullScreen=" + mMoveToFullScreen + " mWillReplace=" + mWillReplace);
152
153            finishAnimation();
154            if (mMoveToFullScreen && !mWillReplace) {
155                mTarget.moveToFullscreen();
156            }
157        }
158
159        @Override
160        public void onAnimationCancel(Animator animation) {
161            finishAnimation();
162        }
163
164        @Override
165        public void cancel() {
166            mWillReplace = true;
167            if (DEBUG) Slog.d(TAG, "cancel: willReplace mTarget=" + mTarget);
168            super.cancel();
169        }
170
171        /** Returns true if the animation target is the same as the input bounds. */
172        public boolean isAnimatingTo(Rect bounds) {
173            return mTo.equals(bounds);
174        }
175
176        private void finishAnimation() {
177            if (DEBUG) Slog.d(TAG, "finishAnimation: mTarget=" + mTarget
178                    + " callers" + Debug.getCallers(2));
179            if (!mWillReplace) {
180                mTarget.onAnimationEnd();
181            }
182            removeListener(this);
183            removeUpdateListener(this);
184            mRunningAnimations.remove(mTarget);
185        }
186
187        @Override
188        public void onAnimationRepeat(Animator animation) {
189
190        }
191    }
192
193    public interface AnimateBoundsUser {
194        /**
195         * Asks the target to directly (without any intermediate steps, like scheduling animation)
196         * resize its bounds.
197         *
198         * @return Whether the target still wants to be animated and successfully finished the
199         * operation. If it returns false, the animation will immediately be cancelled. The target
200         * should return false when something abnormal happened, e.g. it was completely removed
201         * from the hierarchy and is not valid anymore.
202         */
203        boolean setSize(Rect bounds);
204        /**
205         * Behaves as setSize, but freezes the bounds of any tasks in the target at taskBounds,
206         * to allow for more flexibility during resizing. Only
207         * works for the pinned stack at the moment.
208         */
209        boolean setPinnedStackSize(Rect bounds, Rect taskBounds);
210
211        void onAnimationStart();
212
213        /**
214         * Callback for the target to inform it that the animation has ended, so it can do some
215         * necessary cleanup.
216         */
217        void onAnimationEnd();
218
219        void moveToFullscreen();
220
221        void getFullScreenBounds(Rect bounds);
222    }
223
224    void animateBounds(final AnimateBoundsUser target, Rect from, Rect to, int animationDuration) {
225        boolean moveToFullscreen = false;
226        if (to == null) {
227            to = new Rect();
228            target.getFullScreenBounds(to);
229            moveToFullscreen = true;
230        }
231
232        final BoundsAnimator existing = mRunningAnimations.get(target);
233        final boolean replacing = existing != null;
234
235        if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to
236                + " moveToFullscreen=" + moveToFullscreen + " replacing=" + replacing);
237
238        if (replacing) {
239            if (existing.isAnimatingTo(to)) {
240                // Just les the current animation complete if it has the same destination as the
241                // one we are trying to start.
242                if (DEBUG) Slog.d(TAG, "animateBounds: same destination as existing=" + existing
243                        + " ignoring...");
244                return;
245            }
246            existing.cancel();
247        }
248        final BoundsAnimator animator =
249                new BoundsAnimator(target, from, to, moveToFullscreen, replacing);
250        mRunningAnimations.put(target, animator);
251        animator.setFloatValues(0f, 1f);
252        animator.setDuration((animationDuration != -1 ? animationDuration
253                : DEFAULT_APP_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR);
254        animator.setInterpolator(new LinearInterpolator());
255        animator.start();
256    }
257}
258