Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2018 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 
     17 package com.android.systemui.statusbar.notification;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.app.ActivityManager;
     23 import android.graphics.Matrix;
     24 import android.graphics.Rect;
     25 import android.os.RemoteException;
     26 import android.util.MathUtils;
     27 import android.view.IRemoteAnimationFinishedCallback;
     28 import android.view.IRemoteAnimationRunner;
     29 import android.view.RemoteAnimationAdapter;
     30 import android.view.RemoteAnimationTarget;
     31 
     32 import com.android.systemui.Interpolators;
     33 import com.android.systemui.shared.system.SurfaceControlCompat;
     34 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier;
     35 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams;
     36 import com.android.systemui.statusbar.ExpandableNotificationRow;
     37 import com.android.systemui.statusbar.NotificationListContainer;
     38 import com.android.systemui.statusbar.StatusBarState;
     39 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
     40 import com.android.systemui.statusbar.phone.NotificationPanelView;
     41 import com.android.systemui.statusbar.phone.StatusBar;
     42 import com.android.systemui.statusbar.phone.StatusBarWindowView;
     43 
     44 import java.util.ArrayList;
     45 
     46 /**
     47  * A class that allows activities to be launched in a seamless way where the notification
     48  * transforms nicely into the starting window.
     49  */
     50 public class ActivityLaunchAnimator {
     51 
     52     private static final int ANIMATION_DURATION = 400;
     53     public static final long ANIMATION_DURATION_FADE_CONTENT = 67;
     54     public static final long ANIMATION_DURATION_FADE_APP = 200;
     55     public static final long ANIMATION_DELAY_ICON_FADE_IN = ANIMATION_DURATION -
     56             CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY
     57             - 16;
     58     private static final long LAUNCH_TIMEOUT = 500;
     59     private final NotificationPanelView mNotificationPanel;
     60     private final NotificationListContainer mNotificationContainer;
     61     private final StatusBarWindowView mStatusBarWindow;
     62     private StatusBar mStatusBar;
     63     private final Runnable mTimeoutRunnable = () -> {
     64         setAnimationPending(false);
     65         mStatusBar.collapsePanel(true /* animate */);
     66     };
     67     private boolean mAnimationPending;
     68 
     69     public ActivityLaunchAnimator(StatusBarWindowView statusBarWindow,
     70             StatusBar statusBar,
     71             NotificationPanelView notificationPanel,
     72             NotificationListContainer container) {
     73         mNotificationPanel = notificationPanel;
     74         mNotificationContainer = container;
     75         mStatusBarWindow = statusBarWindow;
     76         mStatusBar = statusBar;
     77     }
     78 
     79     public RemoteAnimationAdapter getLaunchAnimation(
     80             ExpandableNotificationRow sourceNotification, boolean occluded) {
     81         if (mStatusBar.getBarState() != StatusBarState.SHADE || occluded) {
     82             return null;
     83         }
     84         AnimationRunner animationRunner = new AnimationRunner(sourceNotification);
     85         return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION,
     86                 ANIMATION_DURATION - 150 /* statusBarTransitionDelay */);
     87     }
     88 
     89     public boolean isAnimationPending() {
     90         return mAnimationPending;
     91     }
     92 
     93     public void setLaunchResult(int launchResult) {
     94         setAnimationPending((launchResult == ActivityManager.START_TASK_TO_FRONT
     95                 || launchResult == ActivityManager.START_SUCCESS)
     96                         && mStatusBar.getBarState() == StatusBarState.SHADE);
     97     }
     98 
     99     private void setAnimationPending(boolean pending) {
    100         mAnimationPending = pending;
    101         mStatusBarWindow.setExpandAnimationPending(pending);
    102         if (pending) {
    103             mStatusBarWindow.postDelayed(mTimeoutRunnable, LAUNCH_TIMEOUT);
    104         } else {
    105             mStatusBarWindow.removeCallbacks(mTimeoutRunnable);
    106         }
    107     }
    108 
    109     class AnimationRunner extends IRemoteAnimationRunner.Stub {
    110 
    111         private final ExpandableNotificationRow mSourceNotification;
    112         private final ExpandAnimationParameters mParams;
    113         private final Rect mWindowCrop = new Rect();
    114         private boolean mInstantCollapsePanel = true;
    115         private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier;
    116 
    117         public AnimationRunner(ExpandableNotificationRow sourceNofitication) {
    118             mSourceNotification = sourceNofitication;
    119             mParams = new ExpandAnimationParameters();
    120             mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification);
    121         }
    122 
    123         @Override
    124         public void onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets,
    125                 IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)
    126                     throws RemoteException {
    127             mSourceNotification.post(() -> {
    128                 RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget(
    129                         remoteAnimationTargets);
    130                 if (primary == null) {
    131                     setAnimationPending(false);
    132                     invokeCallback(iRemoteAnimationFinishedCallback);
    133                     return;
    134                 }
    135 
    136                 setExpandAnimationRunning(true);
    137                 mInstantCollapsePanel = primary.position.y == 0
    138                         && primary.sourceContainerBounds.height()
    139                                 >= mNotificationPanel.getHeight();
    140                 if (!mInstantCollapsePanel) {
    141                     mNotificationPanel.collapseWithDuration(ANIMATION_DURATION);
    142                 }
    143                 ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
    144                 mParams.startPosition = mSourceNotification.getLocationOnScreen();
    145                 mParams.startTranslationZ = mSourceNotification.getTranslationZ();
    146                 mParams.startClipTopAmount = mSourceNotification.getClipTopAmount();
    147                 if (mSourceNotification.isChildInGroup()) {
    148                     int parentClip = mSourceNotification
    149                             .getNotificationParent().getClipTopAmount();
    150                     mParams.parentStartClipTopAmount = parentClip;
    151                     // We need to calculate how much the child is clipped by the parent
    152                     // because children always have 0 clipTopAmount
    153                     if (parentClip != 0) {
    154                         float childClip = parentClip
    155                                 - mSourceNotification.getTranslationY();
    156                         if (childClip > 0.0f) {
    157                             mParams.startClipTopAmount = (int) Math.ceil(childClip);
    158                         }
    159                     }
    160                 }
    161                 int targetWidth = primary.sourceContainerBounds.width();
    162                 int notificationHeight = mSourceNotification.getActualHeight()
    163                         - mSourceNotification.getClipBottomAmount();
    164                 int notificationWidth = mSourceNotification.getWidth();
    165                 anim.setDuration(ANIMATION_DURATION);
    166                 anim.setInterpolator(Interpolators.LINEAR);
    167                 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    168                     @Override
    169                     public void onAnimationUpdate(ValueAnimator animation) {
    170                         mParams.linearProgress = animation.getAnimatedFraction();
    171                         float progress
    172                                 = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
    173                                         mParams.linearProgress);
    174                         int newWidth = (int) MathUtils.lerp(notificationWidth,
    175                                 targetWidth, progress);
    176                         mParams.left = (int) ((targetWidth - newWidth) / 2.0f);
    177                         mParams.right = mParams.left + newWidth;
    178                         mParams.top = (int) MathUtils.lerp(mParams.startPosition[1],
    179                                 primary.position.y, progress);
    180                         mParams.bottom = (int) MathUtils.lerp(mParams.startPosition[1]
    181                                         + notificationHeight,
    182                                 primary.position.y + primary.sourceContainerBounds.bottom,
    183                                 progress);
    184                         applyParamsToWindow(primary);
    185                         applyParamsToNotification(mParams);
    186                         applyParamsToNotificationList(mParams);
    187                     }
    188                 });
    189                 anim.addListener(new AnimatorListenerAdapter() {
    190                     @Override
    191                     public void onAnimationEnd(Animator animation) {
    192                         setExpandAnimationRunning(false);
    193                         if (mInstantCollapsePanel) {
    194                             mStatusBar.collapsePanel(false /* animate */);
    195                         }
    196                         invokeCallback(iRemoteAnimationFinishedCallback);
    197                     }
    198                 });
    199                 anim.start();
    200                 setAnimationPending(false);
    201             });
    202         }
    203 
    204         private void invokeCallback(IRemoteAnimationFinishedCallback callback) {
    205             try {
    206                 callback.onAnimationFinished();
    207             } catch (RemoteException e) {
    208                 e.printStackTrace();
    209             }
    210         }
    211 
    212         private RemoteAnimationTarget getPrimaryRemoteAnimationTarget(
    213                 RemoteAnimationTarget[] remoteAnimationTargets) {
    214             RemoteAnimationTarget primary = null;
    215             for (RemoteAnimationTarget app : remoteAnimationTargets) {
    216                 if (app.mode == RemoteAnimationTarget.MODE_OPENING) {
    217                     primary = app;
    218                     break;
    219                 }
    220             }
    221             return primary;
    222         }
    223 
    224         private void setExpandAnimationRunning(boolean running) {
    225             mNotificationPanel.setLaunchingNotification(running);
    226             mSourceNotification.setExpandAnimationRunning(running);
    227             mStatusBarWindow.setExpandAnimationRunning(running);
    228             mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null);
    229             if (!running) {
    230                 applyParamsToNotification(null);
    231                 applyParamsToNotificationList(null);
    232             }
    233 
    234         }
    235 
    236         private void applyParamsToNotificationList(ExpandAnimationParameters params) {
    237             mNotificationContainer.applyExpandAnimationParams(params);
    238             mNotificationPanel.applyExpandAnimationParams(params);
    239         }
    240 
    241         private void applyParamsToNotification(ExpandAnimationParameters params) {
    242             mSourceNotification.applyExpandAnimationParams(params);
    243         }
    244 
    245         private void applyParamsToWindow(RemoteAnimationTarget app) {
    246             Matrix m = new Matrix();
    247             m.postTranslate(0, (float) (mParams.top - app.position.y));
    248             mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight());
    249             SurfaceParams params = new SurfaceParams(new SurfaceControlCompat(app.leash),
    250                     1f /* alpha */, m, mWindowCrop, app.prefixOrderIndex);
    251             mSyncRtTransactionApplier.scheduleApply(params);
    252         }
    253 
    254         @Override
    255         public void onAnimationCancelled() throws RemoteException {
    256             mSourceNotification.post(() -> {
    257                 setAnimationPending(false);
    258                 mStatusBar.onLaunchAnimationCancelled();
    259             });
    260         }
    261     };
    262 
    263     public static class ExpandAnimationParameters {
    264         float linearProgress;
    265         int[] startPosition;
    266         float startTranslationZ;
    267         int left;
    268         int top;
    269         int right;
    270         int bottom;
    271         int startClipTopAmount;
    272         int parentStartClipTopAmount;
    273 
    274         public ExpandAnimationParameters() {
    275         }
    276 
    277         public int getTop() {
    278             return top;
    279         }
    280 
    281         public int getWidth() {
    282             return right - left;
    283         }
    284 
    285         public int getHeight() {
    286             return bottom - top;
    287         }
    288 
    289         public int getTopChange() {
    290             // We need this compensation to ensure that the QS moves in sync.
    291             int clipTopAmountCompensation = 0;
    292             if (startClipTopAmount != 0.0f) {
    293                 clipTopAmountCompensation = (int) MathUtils.lerp(0, startClipTopAmount,
    294                         Interpolators.FAST_OUT_SLOW_IN.getInterpolation(linearProgress));
    295             }
    296             return Math.min(top - startPosition[1] - clipTopAmountCompensation, 0);
    297         }
    298 
    299         public float getProgress() {
    300             return linearProgress;
    301         }
    302 
    303         public float getProgress(long delay, long duration) {
    304             return MathUtils.constrain((linearProgress * ANIMATION_DURATION - delay)
    305                     / duration, 0.0f, 1.0f);
    306         }
    307 
    308         public int getStartClipTopAmount() {
    309             return startClipTopAmount;
    310         }
    311 
    312         public int getParentStartClipTopAmount() {
    313             return parentStartClipTopAmount;
    314         }
    315 
    316         public float getStartTranslationZ() {
    317             return startTranslationZ;
    318         }
    319     }
    320 }
    321