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.Handler; 28import android.os.IBinder; 29import android.os.Debug; 30import android.util.ArrayMap; 31import android.util.Slog; 32import android.view.animation.LinearInterpolator; 33import android.view.WindowManagerInternal; 34 35/** 36 * Enables animating bounds of objects. 37 * 38 * In multi-window world bounds of both stack and tasks can change. When we need these bounds to 39 * change smoothly and not require the app to relaunch (e.g. because it handles resizes and 40 * relaunching it would cause poorer experience), these class provides a way to directly animate 41 * the bounds of the resized object. 42 * 43 * The object that is resized needs to implement {@link AnimateBoundsUser} interface. 44 * 45 * NOTE: All calls to methods in this class should be done on the UI thread 46 */ 47public class BoundsAnimationController { 48 private static final boolean DEBUG_LOCAL = false; 49 private static final boolean DEBUG = DEBUG_LOCAL || DEBUG_ANIM; 50 private static final String TAG = TAG_WITH_CLASS_NAME || DEBUG_LOCAL 51 ? "BoundsAnimationController" : TAG_WM; 52 private static final int DEBUG_ANIMATION_SLOW_DOWN_FACTOR = 1; 53 54 // Only accessed on UI thread. 55 private ArrayMap<AnimateBoundsUser, BoundsAnimator> mRunningAnimations = new ArrayMap<>(); 56 57 private final class AppTransitionNotifier 58 extends WindowManagerInternal.AppTransitionListener implements Runnable { 59 60 public void onAppTransitionCancelledLocked() { 61 animationFinished(); 62 } 63 public void onAppTransitionFinishedLocked(IBinder token) { 64 animationFinished(); 65 } 66 private void animationFinished() { 67 if (mFinishAnimationAfterTransition) { 68 mHandler.removeCallbacks(this); 69 // This might end up calling into activity manager which will be bad since we have the 70 // window manager lock held at this point. Post a message to take care of the processing 71 // so we don't deadlock. 72 mHandler.post(this); 73 } 74 } 75 76 @Override 77 public void run() { 78 for (int i = 0; i < mRunningAnimations.size(); i++) { 79 final BoundsAnimator b = mRunningAnimations.valueAt(i); 80 b.onAnimationEnd(null); 81 } 82 } 83 } 84 85 private final Handler mHandler; 86 private final AppTransition mAppTransition; 87 private final AppTransitionNotifier mAppTransitionNotifier = new AppTransitionNotifier(); 88 private boolean mFinishAnimationAfterTransition = false; 89 90 BoundsAnimationController(AppTransition transition, Handler handler) { 91 mHandler = handler; 92 mAppTransition = transition; 93 mAppTransition.registerListenerLocked(mAppTransitionNotifier); 94 } 95 96 private final class BoundsAnimator extends ValueAnimator 97 implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener { 98 private final AnimateBoundsUser mTarget; 99 private final Rect mFrom; 100 private final Rect mTo; 101 private final Rect mTmpRect = new Rect(); 102 private final Rect mTmpTaskBounds = new Rect(); 103 private final boolean mMoveToFullScreen; 104 // True if this this animation was cancelled and will be replaced the another animation from 105 // the same {@link #AnimateBoundsUser} target. 106 private boolean mWillReplace; 107 // True to true if this animation replaced a previous animation of the same 108 // {@link #AnimateBoundsUser} target. 109 private final boolean mReplacement; 110 111 // Depending on whether we are animating from 112 // a smaller to a larger size 113 private final int mFrozenTaskWidth; 114 private final int mFrozenTaskHeight; 115 116 BoundsAnimator(AnimateBoundsUser target, Rect from, Rect to, 117 boolean moveToFullScreen, boolean replacement) { 118 super(); 119 mTarget = target; 120 mFrom = from; 121 mTo = to; 122 mMoveToFullScreen = moveToFullScreen; 123 mReplacement = replacement; 124 addUpdateListener(this); 125 addListener(this); 126 127 // If we are animating from smaller to larger, we want to change the task bounds 128 // to their final size immediately so we can use scaling to make the window 129 // larger. Likewise if we are going from bigger to smaller, we want to wait until 130 // the end so we don't have to upscale from the smaller finished size. 131 if (animatingToLargerSize()) { 132 mFrozenTaskWidth = mTo.width(); 133 mFrozenTaskHeight = mTo.height(); 134 } else { 135 mFrozenTaskWidth = mFrom.width(); 136 mFrozenTaskHeight = mFrom.height(); 137 } 138 } 139 140 boolean animatingToLargerSize() { 141 if (mFrom.width() * mFrom.height() > mTo.width() * mTo.height()) { 142 return false; 143 } 144 return true; 145 } 146 147 @Override 148 public void onAnimationUpdate(ValueAnimator animation) { 149 final float value = (Float) animation.getAnimatedValue(); 150 final float remains = 1 - value; 151 mTmpRect.left = (int) (mFrom.left * remains + mTo.left * value + 0.5f); 152 mTmpRect.top = (int) (mFrom.top * remains + mTo.top * value + 0.5f); 153 mTmpRect.right = (int) (mFrom.right * remains + mTo.right * value + 0.5f); 154 mTmpRect.bottom = (int) (mFrom.bottom * remains + mTo.bottom * value + 0.5f); 155 if (DEBUG) Slog.d(TAG, "animateUpdate: mTarget=" + mTarget + " mBounds=" 156 + mTmpRect + " from=" + mFrom + " mTo=" + mTo + " value=" + value 157 + " remains=" + remains); 158 159 mTmpTaskBounds.set(mTmpRect.left, mTmpRect.top, 160 mTmpRect.left + mFrozenTaskWidth, mTmpRect.top + mFrozenTaskHeight); 161 162 if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) { 163 // Whoops, the target doesn't feel like animating anymore. Let's immediately finish 164 // any further animation. 165 animation.cancel(); 166 } 167 } 168 169 170 @Override 171 public void onAnimationStart(Animator animation) { 172 if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget 173 + " mReplacement=" + mReplacement); 174 mFinishAnimationAfterTransition = false; 175 // Ensure that we have prepared the target for animation before 176 // we trigger any size changes, so it can swap surfaces 177 // in to appropriate modes, or do as it wishes otherwise. 178 if (!mReplacement) { 179 mTarget.onAnimationStart(); 180 } 181 182 // Immediately update the task bounds if they have to become larger, but preserve 183 // the starting position so we don't jump at the beginning of the animation. 184 if (animatingToLargerSize()) { 185 mTmpRect.set(mFrom.left, mFrom.top, 186 mFrom.left + mFrozenTaskWidth, mFrom.top + mFrozenTaskHeight); 187 mTarget.setPinnedStackSize(mFrom, mTmpRect); 188 } 189 } 190 191 @Override 192 public void onAnimationEnd(Animator animation) { 193 if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget 194 + " mMoveToFullScreen=" + mMoveToFullScreen + " mWillReplace=" + mWillReplace); 195 196 // There could be another animation running. For example in the 197 // move to fullscreen case, recents will also be closing while the 198 // previous task will be taking its place in the fullscreen stack. 199 // we have to ensure this is completed before we finish the animation 200 // and take our place in the fullscreen stack. 201 if (mAppTransition.isRunning() && !mFinishAnimationAfterTransition) { 202 mFinishAnimationAfterTransition = true; 203 return; 204 } 205 206 finishAnimation(); 207 208 mTarget.setPinnedStackSize(mTo, null); 209 if (mMoveToFullScreen && !mWillReplace) { 210 mTarget.moveToFullscreen(); 211 } 212 } 213 214 @Override 215 public void onAnimationCancel(Animator animation) { 216 finishAnimation(); 217 } 218 219 @Override 220 public void cancel() { 221 mWillReplace = true; 222 if (DEBUG) Slog.d(TAG, "cancel: willReplace mTarget=" + mTarget); 223 super.cancel(); 224 } 225 226 /** Returns true if the animation target is the same as the input bounds. */ 227 public boolean isAnimatingTo(Rect bounds) { 228 return mTo.equals(bounds); 229 } 230 231 private void finishAnimation() { 232 if (DEBUG) Slog.d(TAG, "finishAnimation: mTarget=" + mTarget 233 + " callers" + Debug.getCallers(2)); 234 if (!mWillReplace) { 235 mTarget.onAnimationEnd(); 236 } 237 removeListener(this); 238 removeUpdateListener(this); 239 mRunningAnimations.remove(mTarget); 240 } 241 242 @Override 243 public void onAnimationRepeat(Animator animation) { 244 245 } 246 } 247 248 public interface AnimateBoundsUser { 249 /** 250 * Asks the target to directly (without any intermediate steps, like scheduling animation) 251 * resize its bounds. 252 * 253 * @return Whether the target still wants to be animated and successfully finished the 254 * operation. If it returns false, the animation will immediately be cancelled. The target 255 * should return false when something abnormal happened, e.g. it was completely removed 256 * from the hierarchy and is not valid anymore. 257 */ 258 boolean setSize(Rect bounds); 259 /** 260 * Behaves as setSize, but freezes the bounds of any tasks in the target at taskBounds, 261 * to allow for more flexibility during resizing. Only 262 * works for the pinned stack at the moment. 263 */ 264 boolean setPinnedStackSize(Rect bounds, Rect taskBounds); 265 266 void onAnimationStart(); 267 268 /** 269 * Callback for the target to inform it that the animation has ended, so it can do some 270 * necessary cleanup. 271 */ 272 void onAnimationEnd(); 273 274 void moveToFullscreen(); 275 276 void getFullScreenBounds(Rect bounds); 277 } 278 279 void animateBounds(final AnimateBoundsUser target, Rect from, Rect to, int animationDuration) { 280 boolean moveToFullscreen = false; 281 if (to == null) { 282 to = new Rect(); 283 target.getFullScreenBounds(to); 284 moveToFullscreen = true; 285 } 286 287 final BoundsAnimator existing = mRunningAnimations.get(target); 288 final boolean replacing = existing != null; 289 290 if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to 291 + " moveToFullscreen=" + moveToFullscreen + " replacing=" + replacing); 292 293 if (replacing) { 294 if (existing.isAnimatingTo(to)) { 295 // Just les the current animation complete if it has the same destination as the 296 // one we are trying to start. 297 if (DEBUG) Slog.d(TAG, "animateBounds: same destination as existing=" + existing 298 + " ignoring..."); 299 return; 300 } 301 existing.cancel(); 302 } 303 final BoundsAnimator animator = 304 new BoundsAnimator(target, from, to, moveToFullscreen, replacing); 305 mRunningAnimations.put(target, animator); 306 animator.setFloatValues(0f, 1f); 307 animator.setDuration((animationDuration != -1 ? animationDuration 308 : DEFAULT_APP_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR); 309 animator.setInterpolator(new LinearInterpolator()); 310 animator.start(); 311 } 312} 313