/*
|
* Copyright (C) 2016 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.server.wm;
|
|
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
|
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
|
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
|
|
import android.animation.AnimationHandler;
|
import android.animation.Animator;
|
import android.animation.ValueAnimator;
|
import android.annotation.IntDef;
|
import android.content.Context;
|
import android.graphics.Rect;
|
import android.os.Debug;
|
import android.os.Handler;
|
import android.os.IBinder;
|
import android.util.ArrayMap;
|
import android.util.Slog;
|
import android.view.Choreographer;
|
import android.view.animation.AnimationUtils;
|
import android.view.animation.Interpolator;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
|
|
import java.lang.annotation.Retention;
|
import java.lang.annotation.RetentionPolicy;
|
|
/**
|
* Enables animating bounds of objects.
|
*
|
* In multi-window world bounds of both stack and tasks can change. When we need these bounds to
|
* change smoothly and not require the app to relaunch (e.g. because it handles resizes and
|
* relaunching it would cause poorer experience), these class provides a way to directly animate
|
* the bounds of the resized object.
|
*
|
* The object that is resized needs to implement {@link BoundsAnimationTarget} interface.
|
*
|
* NOTE: All calls to methods in this class should be done on the Animation thread
|
*/
|
public class BoundsAnimationController {
|
private static final boolean DEBUG_LOCAL = false;
|
private static final boolean DEBUG = DEBUG_LOCAL || DEBUG_ANIM;
|
private static final String TAG = TAG_WITH_CLASS_NAME || DEBUG_LOCAL
|
? "BoundsAnimationController" : TAG_WM;
|
private static final int DEBUG_ANIMATION_SLOW_DOWN_FACTOR = 1;
|
|
private static final int DEFAULT_TRANSITION_DURATION = 425;
|
|
@Retention(RetentionPolicy.SOURCE)
|
@IntDef({NO_PIP_MODE_CHANGED_CALLBACKS, SCHEDULE_PIP_MODE_CHANGED_ON_START,
|
SCHEDULE_PIP_MODE_CHANGED_ON_END})
|
public @interface SchedulePipModeChangedState {}
|
/** Do not schedule any PiP mode changed callbacks as a part of this animation. */
|
public static final int NO_PIP_MODE_CHANGED_CALLBACKS = 0;
|
/** Schedule a PiP mode changed callback when this animation starts. */
|
public static final int SCHEDULE_PIP_MODE_CHANGED_ON_START = 1;
|
/** Schedule a PiP mode changed callback when this animation ends. */
|
public static final int SCHEDULE_PIP_MODE_CHANGED_ON_END = 2;
|
|
public static final int BOUNDS = 0;
|
public static final int FADE_IN = 1;
|
|
@IntDef({BOUNDS, FADE_IN}) public @interface AnimationType {}
|
|
private static final int FADE_IN_DURATION = 500;
|
|
// Only accessed on UI thread.
|
private ArrayMap<BoundsAnimationTarget, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
|
|
private final class AppTransitionNotifier
|
extends WindowManagerInternal.AppTransitionListener implements Runnable {
|
|
public void onAppTransitionCancelledLocked() {
|
if (DEBUG) Slog.d(TAG, "onAppTransitionCancelledLocked:"
|
+ " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition);
|
animationFinished();
|
}
|
public void onAppTransitionFinishedLocked(IBinder token) {
|
if (DEBUG) Slog.d(TAG, "onAppTransitionFinishedLocked:"
|
+ " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition);
|
animationFinished();
|
}
|
private void animationFinished() {
|
if (mFinishAnimationAfterTransition) {
|
mHandler.removeCallbacks(this);
|
// This might end up calling into activity manager which will be bad since we have
|
// the window manager lock held at this point. Post a message to take care of the
|
// processing so we don't deadlock.
|
mHandler.post(this);
|
}
|
}
|
|
@Override
|
public void run() {
|
for (int i = 0; i < mRunningAnimations.size(); i++) {
|
final BoundsAnimator b = mRunningAnimations.valueAt(i);
|
b.onAnimationEnd(null);
|
}
|
}
|
}
|
|
private final Handler mHandler;
|
private final AppTransition mAppTransition;
|
private final AppTransitionNotifier mAppTransitionNotifier = new AppTransitionNotifier();
|
private final Interpolator mFastOutSlowInInterpolator;
|
private boolean mFinishAnimationAfterTransition = false;
|
private final AnimationHandler mAnimationHandler;
|
private Choreographer mChoreographer;
|
private @AnimationType int mAnimationType;
|
|
private static final int WAIT_FOR_DRAW_TIMEOUT_MS = 3000;
|
|
BoundsAnimationController(Context context, AppTransition transition, Handler handler,
|
AnimationHandler animationHandler) {
|
mHandler = handler;
|
mAppTransition = transition;
|
mAppTransition.registerListenerLocked(mAppTransitionNotifier);
|
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
|
com.android.internal.R.interpolator.fast_out_slow_in);
|
mAnimationHandler = animationHandler;
|
if (animationHandler != null) {
|
// If an animation handler is provided, then ensure that it runs on the sf vsync tick
|
handler.post(() -> {
|
mChoreographer = Choreographer.getSfInstance();
|
animationHandler.setProvider(new SfVsyncFrameCallbackProvider(mChoreographer));
|
});
|
}
|
}
|
|
@VisibleForTesting
|
final class BoundsAnimator extends ValueAnimator
|
implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
|
|
private final BoundsAnimationTarget mTarget;
|
private final @AnimationType int mAnimationType;
|
private final Rect mFrom = new Rect();
|
private final Rect mTo = new Rect();
|
private final Rect mTmpRect = new Rect();
|
private final Rect mTmpTaskBounds = new Rect();
|
|
// True if this this animation was canceled and will be replaced the another animation from
|
// the same {@link #BoundsAnimationTarget} target.
|
private boolean mSkipFinalResize;
|
// True if this animation was canceled by the user, not as a part of a replacing animation
|
private boolean mSkipAnimationEnd;
|
|
// True if the animation target is animating from the fullscreen. Only one of
|
// {@link mMoveToFullscreen} or {@link mMoveFromFullscreen} can be true at any time in the
|
// animation.
|
private boolean mMoveFromFullscreen;
|
// True if the animation target should be moved to the fullscreen stack at the end of this
|
// animation. Only one of {@link mMoveToFullscreen} or {@link mMoveFromFullscreen} can be
|
// true at any time in the animation.
|
private boolean mMoveToFullscreen;
|
|
// Whether to schedule PiP mode changes on animation start/end
|
private @SchedulePipModeChangedState int mSchedulePipModeChangedState;
|
private @SchedulePipModeChangedState int mPrevSchedulePipModeChangedState;
|
|
// Depending on whether we are animating from
|
// a smaller to a larger size
|
private int mFrozenTaskWidth;
|
private int mFrozenTaskHeight;
|
|
// Timeout callback to ensure we continue the animation if waiting for resuming or app
|
// windows drawn fails
|
private final Runnable mResumeRunnable = () -> {
|
if (DEBUG) Slog.d(TAG, "pause: timed out waiting for windows drawn");
|
resume();
|
};
|
|
BoundsAnimator(BoundsAnimationTarget target, @AnimationType int animationType, Rect from,
|
Rect to, @SchedulePipModeChangedState int schedulePipModeChangedState,
|
@SchedulePipModeChangedState int prevShedulePipModeChangedState,
|
boolean moveFromFullscreen, boolean moveToFullscreen, Rect frozenTask) {
|
super();
|
mTarget = target;
|
mAnimationType = animationType;
|
mFrom.set(from);
|
mTo.set(to);
|
mSchedulePipModeChangedState = schedulePipModeChangedState;
|
mPrevSchedulePipModeChangedState = prevShedulePipModeChangedState;
|
mMoveFromFullscreen = moveFromFullscreen;
|
mMoveToFullscreen = moveToFullscreen;
|
addUpdateListener(this);
|
addListener(this);
|
|
// If we are animating from smaller to larger, we want to change the task bounds
|
// to their final size immediately so we can use scaling to make the window
|
// larger. Likewise if we are going from bigger to smaller, we want to wait until
|
// the end so we don't have to upscale from the smaller finished size.
|
if (mAnimationType == BOUNDS) {
|
if (animatingToLargerSize()) {
|
mFrozenTaskWidth = mTo.width();
|
mFrozenTaskHeight = mTo.height();
|
} else {
|
mFrozenTaskWidth = frozenTask.isEmpty() ? mFrom.width() : frozenTask.width();
|
mFrozenTaskHeight = frozenTask.isEmpty() ? mFrom.height() : frozenTask.height();
|
}
|
}
|
}
|
|
@Override
|
public void onAnimationStart(Animator animation) {
|
if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget
|
+ " mPrevSchedulePipModeChangedState=" + mPrevSchedulePipModeChangedState
|
+ " mSchedulePipModeChangedState=" + mSchedulePipModeChangedState);
|
mFinishAnimationAfterTransition = false;
|
mTmpRect.set(mFrom.left, mFrom.top, mFrom.left + mFrozenTaskWidth,
|
mFrom.top + mFrozenTaskHeight);
|
|
// Boost the thread priority of the animation thread while the bounds animation is
|
// running
|
updateBooster();
|
|
// Ensure that we have prepared the target for animation before we trigger any size
|
// changes, so it can swap surfaces in to appropriate modes, or do as it wishes
|
// otherwise.
|
boolean continueAnimation;
|
if (mPrevSchedulePipModeChangedState == NO_PIP_MODE_CHANGED_CALLBACKS) {
|
continueAnimation = mTarget.onAnimationStart(
|
mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START,
|
false /* forceUpdate */, mAnimationType);
|
|
// When starting an animation from fullscreen, pause here and wait for the
|
// windows-drawn signal before we start the rest of the transition down into PiP.
|
if (continueAnimation && mMoveFromFullscreen
|
&& mTarget.shouldDeferStartOnMoveToFullscreen()) {
|
pause();
|
}
|
} else if (mPrevSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END &&
|
mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
|
// We are replacing a running animation into PiP, but since it hasn't completed, the
|
// client will not currently receive any picture-in-picture mode change callbacks.
|
// However, we still need to report to them that they are leaving PiP, so this will
|
// force an update via a mode changed callback.
|
continueAnimation = mTarget.onAnimationStart(
|
true /* schedulePipModeChangedCallback */, true /* forceUpdate */,
|
mAnimationType);
|
} else {
|
// The animation is already running, but we should check that the TaskStack is still
|
// valid before continuing with the animation
|
continueAnimation = mTarget.isAttached();
|
}
|
|
if (!continueAnimation) {
|
// No point of trying to animate something that isn't attached to the hierarchy
|
// anymore.
|
cancel();
|
return;
|
}
|
|
// Immediately update the task bounds if they have to become larger, but preserve
|
// the starting position so we don't jump at the beginning of the animation.
|
if (animatingToLargerSize()) {
|
mTarget.setPinnedStackSize(mFrom, mTmpRect);
|
|
// We pause the animation until the app has drawn at the new size.
|
// The target will notify us via BoundsAnimationController#resume.
|
// We do this here and pause the animation, rather than just defer starting it
|
// so we can enter the animating state and have WindowStateAnimator apply the
|
// correct logic to make this resize seamless.
|
if (mMoveToFullscreen) {
|
pause();
|
}
|
}
|
}
|
|
@Override
|
public void pause() {
|
if (DEBUG) Slog.d(TAG, "pause: waiting for windows drawn");
|
super.pause();
|
mHandler.postDelayed(mResumeRunnable, WAIT_FOR_DRAW_TIMEOUT_MS);
|
}
|
|
@Override
|
public void resume() {
|
if (DEBUG) Slog.d(TAG, "resume:");
|
mHandler.removeCallbacks(mResumeRunnable);
|
super.resume();
|
}
|
|
@Override
|
public void onAnimationUpdate(ValueAnimator animation) {
|
final float value = (Float) animation.getAnimatedValue();
|
if (mAnimationType == FADE_IN) {
|
if (!mTarget.setPinnedStackAlpha(value)) {
|
cancelAndCallAnimationEnd();
|
}
|
return;
|
}
|
|
final float remains = 1 - value;
|
mTmpRect.left = (int) (mFrom.left * remains + mTo.left * value + 0.5f);
|
mTmpRect.top = (int) (mFrom.top * remains + mTo.top * value + 0.5f);
|
mTmpRect.right = (int) (mFrom.right * remains + mTo.right * value + 0.5f);
|
mTmpRect.bottom = (int) (mFrom.bottom * remains + mTo.bottom * value + 0.5f);
|
if (DEBUG) Slog.d(TAG, "animateUpdate: mTarget=" + mTarget + " mBounds="
|
+ mTmpRect + " from=" + mFrom + " mTo=" + mTo + " value=" + value
|
+ " remains=" + remains);
|
|
mTmpTaskBounds.set(mTmpRect.left, mTmpRect.top,
|
mTmpRect.left + mFrozenTaskWidth, mTmpRect.top + mFrozenTaskHeight);
|
|
if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) {
|
// Whoops, the target doesn't feel like animating anymore. Let's immediately finish
|
// any further animation.
|
if (DEBUG) Slog.d(TAG, "animateUpdate: cancelled");
|
|
// If we have already scheduled a PiP mode changed at the start of the animation,
|
// then we need to clean up and schedule one at the end, since we have canceled the
|
// animation to the final state.
|
if (mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
|
mSchedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
|
}
|
|
// Since we are cancelling immediately without a replacement animation, send the
|
// animation end to maintain callback parity, but also skip any further resizes
|
cancelAndCallAnimationEnd();
|
}
|
}
|
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
|
+ " mSkipFinalResize=" + mSkipFinalResize
|
+ " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition
|
+ " mAppTransitionIsRunning=" + mAppTransition.isRunning()
|
+ " callers=" + Debug.getCallers(2));
|
|
// There could be another animation running. For example in the
|
// move to fullscreen case, recents will also be closing while the
|
// previous task will be taking its place in the fullscreen stack.
|
// we have to ensure this is completed before we finish the animation
|
// and take our place in the fullscreen stack.
|
if (mAppTransition.isRunning() && !mFinishAnimationAfterTransition) {
|
mFinishAnimationAfterTransition = true;
|
return;
|
}
|
|
if (!mSkipAnimationEnd) {
|
// If this animation has already scheduled the picture-in-picture mode on start, and
|
// we are not skipping the final resize due to being canceled, then move the PiP to
|
// fullscreen once the animation ends
|
if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
|
+ " moveToFullscreen=" + mMoveToFullscreen);
|
mTarget.onAnimationEnd(mSchedulePipModeChangedState ==
|
SCHEDULE_PIP_MODE_CHANGED_ON_END, !mSkipFinalResize ? mTo : null,
|
mMoveToFullscreen);
|
}
|
|
// Clean up this animation
|
removeListener(this);
|
removeUpdateListener(this);
|
mRunningAnimations.remove(mTarget);
|
|
// Reset the thread priority of the animation thread after the bounds animation is done
|
updateBooster();
|
}
|
|
@Override
|
public void onAnimationCancel(Animator animation) {
|
// Always skip the final resize when the animation is canceled
|
mSkipFinalResize = true;
|
mMoveToFullscreen = false;
|
}
|
|
private void cancelAndCallAnimationEnd() {
|
if (DEBUG) Slog.d(TAG, "cancelAndCallAnimationEnd: mTarget=" + mTarget);
|
mSkipAnimationEnd = false;
|
super.cancel();
|
}
|
|
@Override
|
public void cancel() {
|
if (DEBUG) Slog.d(TAG, "cancel: mTarget=" + mTarget);
|
mSkipAnimationEnd = true;
|
super.cancel();
|
|
// Reset the thread priority of the animation thread if the bounds animation is canceled
|
updateBooster();
|
}
|
|
/**
|
* @return true if the animation target is the same as the input bounds.
|
*/
|
boolean isAnimatingTo(Rect bounds) {
|
return mTo.equals(bounds);
|
}
|
|
/**
|
* @return true if we are animating to a larger surface size
|
*/
|
@VisibleForTesting
|
boolean animatingToLargerSize() {
|
// TODO: Fix this check for aspect ratio changes
|
return (mFrom.width() * mFrom.height() < mTo.width() * mTo.height());
|
}
|
|
@Override
|
public void onAnimationRepeat(Animator animation) {
|
// Do nothing
|
}
|
|
@Override
|
public AnimationHandler getAnimationHandler() {
|
if (mAnimationHandler != null) {
|
return mAnimationHandler;
|
}
|
return super.getAnimationHandler();
|
}
|
}
|
|
public void animateBounds(final BoundsAnimationTarget target, Rect from, Rect to,
|
int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
|
boolean moveFromFullscreen, boolean moveToFullscreen,
|
@AnimationType int animationType) {
|
animateBoundsImpl(target, from, to, animationDuration, schedulePipModeChangedState,
|
moveFromFullscreen, moveToFullscreen, animationType);
|
}
|
|
/**
|
* Cancel existing animation if the destination was modified.
|
*/
|
void cancel(final BoundsAnimationTarget target) {
|
final BoundsAnimator existing = mRunningAnimations.get(target);
|
if (existing != null) {
|
// Cancel animation. Since its already started, send animation end to client.
|
if (DEBUG) Slog.d(TAG, "cancel: mTarget= " + target);
|
existing.cancelAndCallAnimationEnd();
|
}
|
}
|
|
@VisibleForTesting
|
BoundsAnimator animateBoundsImpl(final BoundsAnimationTarget target, Rect from, Rect to,
|
int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
|
boolean moveFromFullscreen, boolean moveToFullscreen,
|
@AnimationType int animationType) {
|
final BoundsAnimator existing = mRunningAnimations.get(target);
|
|
if (isRunningFadeInAnimation(target) && from.width() == to.width()
|
&& from.height() == to.height()) {
|
animationType = FADE_IN;
|
}
|
final boolean replacing = existing != null;
|
@SchedulePipModeChangedState int prevSchedulePipModeChangedState =
|
NO_PIP_MODE_CHANGED_CALLBACKS;
|
|
if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to
|
+ " schedulePipModeChangedState=" + schedulePipModeChangedState
|
+ " replacing=" + replacing);
|
|
Rect frozenTask = new Rect();
|
if (replacing) {
|
if (existing.isAnimatingTo(to) && (!moveToFullscreen || existing.mMoveToFullscreen)
|
&& (!moveFromFullscreen || existing.mMoveFromFullscreen)) {
|
// Just let the current animation complete if it has the same destination as the
|
// one we are trying to start, and, if moveTo/FromFullscreen was requested, already
|
// has that flag set.
|
if (DEBUG) Slog.d(TAG, "animateBounds: same destination and moveTo/From flags as "
|
+ "existing=" + existing + ", ignoring...");
|
return existing;
|
}
|
|
// Save the previous state
|
prevSchedulePipModeChangedState = existing.mSchedulePipModeChangedState;
|
|
// Update the PiP callback states if we are replacing the animation
|
if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
|
if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
|
if (DEBUG) Slog.d(TAG, "animateBounds: still animating to fullscreen, keep"
|
+ " existing deferred state");
|
} else {
|
if (DEBUG) Slog.d(TAG, "animateBounds: fullscreen animation canceled, callback"
|
+ " on start already processed, schedule deferred update on end");
|
schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
|
}
|
} else if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END) {
|
if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
|
if (DEBUG) Slog.d(TAG, "animateBounds: non-fullscreen animation canceled,"
|
+ " callback on start will be processed");
|
} else {
|
if (DEBUG) Slog.d(TAG, "animateBounds: still animating from fullscreen, keep"
|
+ " existing deferred state");
|
schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
|
}
|
}
|
|
// We need to keep the previous moveTo/FromFullscreen flag, unless the new animation
|
// specifies a direction.
|
if (!moveFromFullscreen && !moveToFullscreen) {
|
moveToFullscreen = existing.mMoveToFullscreen;
|
moveFromFullscreen = existing.mMoveFromFullscreen;
|
}
|
|
// We are in the middle of an existing animation, so that this new animation may
|
// start from an interpolated bounds. We should keep using the existing frozen task
|
// width/height for consistent configurations.
|
frozenTask.set(0, 0, existing.mFrozenTaskWidth, existing.mFrozenTaskHeight);
|
|
// Since we are replacing, we skip both animation start and end callbacks
|
existing.cancel();
|
}
|
if (animationType == FADE_IN) {
|
target.setPinnedStackSize(to, null);
|
}
|
|
final BoundsAnimator animator = new BoundsAnimator(target, animationType, from, to,
|
schedulePipModeChangedState, prevSchedulePipModeChangedState,
|
moveFromFullscreen, moveToFullscreen, frozenTask);
|
mRunningAnimations.put(target, animator);
|
animator.setFloatValues(0f, 1f);
|
animator.setDuration(animationType == FADE_IN ? FADE_IN_DURATION
|
: (animationDuration != -1 ? animationDuration : DEFAULT_TRANSITION_DURATION)
|
* DEBUG_ANIMATION_SLOW_DOWN_FACTOR);
|
animator.setInterpolator(mFastOutSlowInInterpolator);
|
animator.start();
|
return animator;
|
}
|
|
public void setAnimationType(@AnimationType int animationType) {
|
mAnimationType = animationType;
|
}
|
|
/** return the current animation type. */
|
public @AnimationType int getAnimationType() {
|
@AnimationType int animationType = mAnimationType;
|
// Default to BOUNDS.
|
mAnimationType = BOUNDS;
|
return animationType;
|
}
|
|
public Handler getHandler() {
|
return mHandler;
|
}
|
|
public void onAllWindowsDrawn() {
|
if (DEBUG) Slog.d(TAG, "onAllWindowsDrawn:");
|
mHandler.post(this::resume);
|
}
|
|
private boolean isRunningFadeInAnimation(final BoundsAnimationTarget target) {
|
final BoundsAnimator existing = mRunningAnimations.get(target);
|
return existing != null && existing.mAnimationType == FADE_IN && existing.isStarted();
|
}
|
|
private void resume() {
|
for (int i = 0; i < mRunningAnimations.size(); i++) {
|
final BoundsAnimator b = mRunningAnimations.valueAt(i);
|
b.resume();
|
}
|
}
|
|
private void updateBooster() {
|
WindowManagerService.sThreadPriorityBooster.setBoundsAnimationRunning(
|
!mRunningAnimations.isEmpty());
|
}
|
}
|