/*
|
* Copyright (C) 2018 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.systemui.statusbar.notification;
|
|
import android.animation.Animator;
|
import android.animation.AnimatorListenerAdapter;
|
import android.animation.ValueAnimator;
|
import android.app.ActivityManager;
|
import android.graphics.Matrix;
|
import android.graphics.Rect;
|
import android.os.RemoteException;
|
import android.util.MathUtils;
|
import android.view.IRemoteAnimationFinishedCallback;
|
import android.view.IRemoteAnimationRunner;
|
import android.view.RemoteAnimationAdapter;
|
import android.view.RemoteAnimationTarget;
|
import android.view.SyncRtSurfaceTransactionApplier;
|
import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
|
import android.view.View;
|
|
import com.android.internal.policy.ScreenDecorationsUtils;
|
import com.android.systemui.Interpolators;
|
import com.android.systemui.shared.system.SurfaceControlCompat;
|
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
|
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
|
import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
|
import com.android.systemui.statusbar.phone.NotificationPanelView;
|
import com.android.systemui.statusbar.phone.StatusBarWindowView;
|
|
/**
|
* A class that allows activities to be launched in a seamless way where the notification
|
* transforms nicely into the starting window.
|
*/
|
public class ActivityLaunchAnimator {
|
|
private static final int ANIMATION_DURATION = 400;
|
public static final long ANIMATION_DURATION_FADE_CONTENT = 67;
|
public static final long ANIMATION_DURATION_FADE_APP = 200;
|
public static final long ANIMATION_DELAY_ICON_FADE_IN = ANIMATION_DURATION -
|
CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY
|
- 16;
|
private static final long LAUNCH_TIMEOUT = 500;
|
private final NotificationPanelView mNotificationPanel;
|
private final NotificationListContainer mNotificationContainer;
|
private final StatusBarWindowView mStatusBarWindow;
|
private final float mWindowCornerRadius;
|
private Callback mCallback;
|
private final Runnable mTimeoutRunnable = () -> {
|
setAnimationPending(false);
|
mCallback.onExpandAnimationTimedOut();
|
};
|
private boolean mAnimationPending;
|
private boolean mAnimationRunning;
|
private boolean mIsLaunchForActivity;
|
|
public ActivityLaunchAnimator(StatusBarWindowView statusBarWindow,
|
Callback callback,
|
NotificationPanelView notificationPanel,
|
NotificationListContainer container) {
|
mNotificationPanel = notificationPanel;
|
mNotificationContainer = container;
|
mStatusBarWindow = statusBarWindow;
|
mCallback = callback;
|
mWindowCornerRadius = ScreenDecorationsUtils
|
.getWindowCornerRadius(statusBarWindow.getResources());
|
}
|
|
public RemoteAnimationAdapter getLaunchAnimation(
|
View sourceView, boolean occluded) {
|
if (!(sourceView instanceof ExpandableNotificationRow) || !mCallback.areLaunchAnimationsEnabled() || occluded) {
|
return null;
|
}
|
AnimationRunner animationRunner = new AnimationRunner(
|
(ExpandableNotificationRow) sourceView);
|
return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION,
|
ANIMATION_DURATION - 150 /* statusBarTransitionDelay */);
|
}
|
|
public boolean isAnimationPending() {
|
return mAnimationPending;
|
}
|
|
/**
|
* Set the launch result the intent requested
|
*
|
* @param launchResult the launch result
|
* @param wasIntentActivity was this launch for an activity
|
*/
|
public void setLaunchResult(int launchResult, boolean wasIntentActivity) {
|
mIsLaunchForActivity = wasIntentActivity;
|
setAnimationPending((launchResult == ActivityManager.START_TASK_TO_FRONT
|
|| launchResult == ActivityManager.START_SUCCESS)
|
&& mCallback.areLaunchAnimationsEnabled());
|
}
|
|
public boolean isLaunchForActivity() {
|
return mIsLaunchForActivity;
|
}
|
|
private void setAnimationPending(boolean pending) {
|
mAnimationPending = pending;
|
mStatusBarWindow.setExpandAnimationPending(pending);
|
if (pending) {
|
mStatusBarWindow.postDelayed(mTimeoutRunnable, LAUNCH_TIMEOUT);
|
} else {
|
mStatusBarWindow.removeCallbacks(mTimeoutRunnable);
|
}
|
}
|
|
public boolean isAnimationRunning() {
|
return mAnimationRunning;
|
}
|
|
class AnimationRunner extends IRemoteAnimationRunner.Stub {
|
|
private final ExpandableNotificationRow mSourceNotification;
|
private final ExpandAnimationParameters mParams;
|
private final Rect mWindowCrop = new Rect();
|
private final float mNotificationCornerRadius;
|
private float mCornerRadius;
|
private boolean mIsFullScreenLaunch = true;
|
private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier;
|
|
public AnimationRunner(ExpandableNotificationRow sourceNofitication) {
|
mSourceNotification = sourceNofitication;
|
mParams = new ExpandAnimationParameters();
|
mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification);
|
mNotificationCornerRadius = Math.max(mSourceNotification.getCurrentTopRoundness(),
|
mSourceNotification.getCurrentBottomRoundness());
|
}
|
|
@Override
|
public void onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets,
|
IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)
|
throws RemoteException {
|
mSourceNotification.post(() -> {
|
RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget(
|
remoteAnimationTargets);
|
if (primary == null) {
|
setAnimationPending(false);
|
invokeCallback(iRemoteAnimationFinishedCallback);
|
mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */);
|
return;
|
}
|
|
setExpandAnimationRunning(true);
|
mIsFullScreenLaunch = primary.position.y == 0
|
&& primary.sourceContainerBounds.height()
|
>= mNotificationPanel.getHeight();
|
if (!mIsFullScreenLaunch) {
|
mNotificationPanel.collapseWithDuration(ANIMATION_DURATION);
|
}
|
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
mParams.startPosition = mSourceNotification.getLocationOnScreen();
|
mParams.startTranslationZ = mSourceNotification.getTranslationZ();
|
mParams.startClipTopAmount = mSourceNotification.getClipTopAmount();
|
if (mSourceNotification.isChildInGroup()) {
|
int parentClip = mSourceNotification
|
.getNotificationParent().getClipTopAmount();
|
mParams.parentStartClipTopAmount = parentClip;
|
// We need to calculate how much the child is clipped by the parent
|
// because children always have 0 clipTopAmount
|
if (parentClip != 0) {
|
float childClip = parentClip
|
- mSourceNotification.getTranslationY();
|
if (childClip > 0.0f) {
|
mParams.startClipTopAmount = (int) Math.ceil(childClip);
|
}
|
}
|
}
|
int targetWidth = primary.sourceContainerBounds.width();
|
int notificationHeight = mSourceNotification.getActualHeight()
|
- mSourceNotification.getClipBottomAmount();
|
int notificationWidth = mSourceNotification.getWidth();
|
anim.setDuration(ANIMATION_DURATION);
|
anim.setInterpolator(Interpolators.LINEAR);
|
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
@Override
|
public void onAnimationUpdate(ValueAnimator animation) {
|
mParams.linearProgress = animation.getAnimatedFraction();
|
float progress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
|
mParams.linearProgress);
|
int newWidth = (int) MathUtils.lerp(notificationWidth,
|
targetWidth, progress);
|
mParams.left = (int) ((targetWidth - newWidth) / 2.0f);
|
mParams.right = mParams.left + newWidth;
|
mParams.top = (int) MathUtils.lerp(mParams.startPosition[1],
|
primary.position.y, progress);
|
mParams.bottom = (int) MathUtils.lerp(mParams.startPosition[1]
|
+ notificationHeight,
|
primary.position.y + primary.sourceContainerBounds.bottom,
|
progress);
|
mCornerRadius = MathUtils.lerp(mNotificationCornerRadius,
|
mWindowCornerRadius, progress);
|
applyParamsToWindow(primary);
|
applyParamsToNotification(mParams);
|
applyParamsToNotificationList(mParams);
|
}
|
});
|
anim.addListener(new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
setExpandAnimationRunning(false);
|
invokeCallback(iRemoteAnimationFinishedCallback);
|
}
|
});
|
anim.start();
|
setAnimationPending(false);
|
});
|
}
|
|
private void invokeCallback(IRemoteAnimationFinishedCallback callback) {
|
try {
|
callback.onAnimationFinished();
|
} catch (RemoteException e) {
|
e.printStackTrace();
|
}
|
}
|
|
private RemoteAnimationTarget getPrimaryRemoteAnimationTarget(
|
RemoteAnimationTarget[] remoteAnimationTargets) {
|
RemoteAnimationTarget primary = null;
|
for (RemoteAnimationTarget app : remoteAnimationTargets) {
|
if (app.mode == RemoteAnimationTarget.MODE_OPENING) {
|
primary = app;
|
break;
|
}
|
}
|
return primary;
|
}
|
|
private void setExpandAnimationRunning(boolean running) {
|
mNotificationPanel.setLaunchingNotification(running);
|
mSourceNotification.setExpandAnimationRunning(running);
|
mStatusBarWindow.setExpandAnimationRunning(running);
|
mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null);
|
mAnimationRunning = running;
|
if (!running) {
|
mCallback.onExpandAnimationFinished(mIsFullScreenLaunch);
|
applyParamsToNotification(null);
|
applyParamsToNotificationList(null);
|
}
|
|
}
|
|
private void applyParamsToNotificationList(ExpandAnimationParameters params) {
|
mNotificationContainer.applyExpandAnimationParams(params);
|
mNotificationPanel.applyExpandAnimationParams(params);
|
}
|
|
private void applyParamsToNotification(ExpandAnimationParameters params) {
|
mSourceNotification.applyExpandAnimationParams(params);
|
}
|
|
private void applyParamsToWindow(RemoteAnimationTarget app) {
|
Matrix m = new Matrix();
|
m.postTranslate(0, (float) (mParams.top - app.position.y));
|
mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight());
|
SurfaceParams params = new SurfaceParams(app.leash, 1f /* alpha */, m, mWindowCrop,
|
app.prefixOrderIndex, mCornerRadius, true /* visible */);
|
mSyncRtTransactionApplier.scheduleApply(params);
|
}
|
|
@Override
|
public void onAnimationCancelled() throws RemoteException {
|
mSourceNotification.post(() -> {
|
setAnimationPending(false);
|
mCallback.onLaunchAnimationCancelled();
|
});
|
}
|
};
|
|
public static class ExpandAnimationParameters {
|
float linearProgress;
|
int[] startPosition;
|
float startTranslationZ;
|
int left;
|
int top;
|
int right;
|
int bottom;
|
int startClipTopAmount;
|
int parentStartClipTopAmount;
|
|
public ExpandAnimationParameters() {
|
}
|
|
public int getTop() {
|
return top;
|
}
|
|
public int getBottom() {
|
return bottom;
|
}
|
|
public int getWidth() {
|
return right - left;
|
}
|
|
public int getHeight() {
|
return bottom - top;
|
}
|
|
public int getTopChange() {
|
// We need this compensation to ensure that the QS moves in sync.
|
int clipTopAmountCompensation = 0;
|
if (startClipTopAmount != 0.0f) {
|
clipTopAmountCompensation = (int) MathUtils.lerp(0, startClipTopAmount,
|
Interpolators.FAST_OUT_SLOW_IN.getInterpolation(linearProgress));
|
}
|
return Math.min(top - startPosition[1] - clipTopAmountCompensation, 0);
|
}
|
|
public float getProgress() {
|
return linearProgress;
|
}
|
|
public float getProgress(long delay, long duration) {
|
return MathUtils.constrain((linearProgress * ANIMATION_DURATION - delay)
|
/ duration, 0.0f, 1.0f);
|
}
|
|
public int getStartClipTopAmount() {
|
return startClipTopAmount;
|
}
|
|
public int getParentStartClipTopAmount() {
|
return parentStartClipTopAmount;
|
}
|
|
public float getStartTranslationZ() {
|
return startTranslationZ;
|
}
|
}
|
|
public interface Callback {
|
|
/**
|
* Called when the launch animation was cancelled.
|
*/
|
void onLaunchAnimationCancelled();
|
|
/**
|
* Called when the launch animation has timed out without starting an actual animation.
|
*/
|
void onExpandAnimationTimedOut();
|
|
/**
|
* Called when the expand animation has finished.
|
*
|
* @param launchIsFullScreen True if this launch was fullscreen, such that now the window
|
* fills the whole screen
|
*/
|
void onExpandAnimationFinished(boolean launchIsFullScreen);
|
|
/**
|
* Are animations currently enabled.
|
*/
|
boolean areLaunchAnimationsEnabled();
|
}
|
}
|