Home | History | Annotate | Download | only in util
      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 package com.android.quickstep.util;
     17 
     18 import static com.android.launcher3.anim.Interpolators.LINEAR;
     19 import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR;
     20 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
     21 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
     22 
     23 import android.annotation.TargetApi;
     24 import android.graphics.Canvas;
     25 import android.graphics.Matrix;
     26 import android.graphics.Matrix.ScaleToFit;
     27 import android.graphics.PointF;
     28 import android.graphics.Rect;
     29 import android.graphics.RectF;
     30 import android.os.Build;
     31 import android.os.RemoteException;
     32 import android.support.annotation.Nullable;
     33 import android.view.animation.Interpolator;
     34 
     35 import com.android.launcher3.BaseDraggingActivity;
     36 import com.android.launcher3.DeviceProfile;
     37 import com.android.launcher3.R;
     38 import com.android.launcher3.Utilities;
     39 import com.android.launcher3.anim.Interpolators;
     40 import com.android.launcher3.views.BaseDragLayer;
     41 import com.android.quickstep.RecentsModel;
     42 import com.android.quickstep.views.RecentsView;
     43 import com.android.quickstep.views.TaskThumbnailView;
     44 import com.android.systemui.shared.recents.ISystemUiProxy;
     45 import com.android.systemui.shared.recents.utilities.RectFEvaluator;
     46 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
     47 import com.android.systemui.shared.system.TransactionCompat;
     48 import com.android.systemui.shared.system.WindowManagerWrapper;
     49 
     50 import java.util.function.BiConsumer;
     51 
     52 /**
     53  * Utility class to handle window clip animation
     54  */
     55 @TargetApi(Build.VERSION_CODES.P)
     56 public class ClipAnimationHelper {
     57 
     58     // The bounds of the source app in device coordinates
     59     private final Rect mSourceStackBounds = new Rect();
     60     // The insets of the source app
     61     private final Rect mSourceInsets = new Rect();
     62     // The source app bounds with the source insets applied, in the source app window coordinates
     63     private final RectF mSourceRect = new RectF();
     64     // The bounds of the task view in launcher window coordinates
     65     private final RectF mTargetRect = new RectF();
     66     // Set when the final window destination is changed, such as offsetting for quick scrub
     67     private final PointF mTargetOffset = new PointF();
     68     // The insets to be used for clipping the app window, which can be larger than mSourceInsets
     69     // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
     70     // app window coordinates.
     71     private final RectF mSourceWindowClipInsets = new RectF();
     72 
     73     // The bounds of launcher (not including insets) in device coordinates
     74     public final Rect mHomeStackBounds = new Rect();
     75 
     76     // The clip rect in source app window coordinates
     77     private final Rect mClipRect = new Rect();
     78     private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
     79     private final Matrix mTmpMatrix = new Matrix();
     80     private final RectF mTmpRectF = new RectF();
     81 
     82     private float mTargetScale = 1f;
     83     private float mOffsetScale = 1f;
     84     private Interpolator mInterpolator = LINEAR;
     85     // We translate y slightly faster than the rest of the animation for quick scrub.
     86     private Interpolator mOffsetYInterpolator = LINEAR;
     87 
     88     // Whether to boost the opening animation target layers, or the closing
     89     private int mBoostModeTargetLayers = -1;
     90     // Wether or not applyTransform has been called yet since prepareAnimation()
     91     private boolean mIsFirstFrame = true;
     92 
     93     private BiConsumer<TransactionCompat, RemoteAnimationTargetCompat> mTaskTransformCallback =
     94             (t, a) -> { };
     95 
     96     private void updateSourceStack(RemoteAnimationTargetCompat target) {
     97         mSourceInsets.set(target.contentInsets);
     98         mSourceStackBounds.set(target.sourceContainerBounds);
     99 
    100         // TODO: Should sourceContainerBounds already have this offset?
    101         mSourceStackBounds.offsetTo(target.position.x, target.position.y);
    102 
    103     }
    104 
    105     public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
    106         mHomeStackBounds.set(homeStackBounds);
    107         updateSourceStack(target);
    108     }
    109 
    110     public void updateTargetRect(TransformedRect targetRect) {
    111         mOffsetScale = targetRect.scale;
    112         mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
    113                 mSourceStackBounds.width() - mSourceInsets.right,
    114                 mSourceStackBounds.height() - mSourceInsets.bottom);
    115         mTargetRect.set(targetRect.rect);
    116         Utilities.scaleRectFAboutCenter(mTargetRect, targetRect.scale);
    117         mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
    118                 mHomeStackBounds.top - mSourceStackBounds.top);
    119 
    120         // Calculate the clip based on the target rect (since the content insets and the
    121         // launcher insets may differ, so the aspect ratio of the target rect can differ
    122         // from the source rect. The difference between the target rect (scaled to the
    123         // source rect) is the amount to clip on each edge.
    124         RectF scaledTargetRect = new RectF(mTargetRect);
    125         Utilities.scaleRectFAboutCenter(scaledTargetRect,
    126                 mSourceRect.width() / mTargetRect.width());
    127         scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
    128         mSourceWindowClipInsets.set(
    129                 Math.max(scaledTargetRect.left, 0),
    130                 Math.max(scaledTargetRect.top, 0),
    131                 Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
    132                 Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
    133         mSourceRect.set(scaledTargetRect);
    134     }
    135 
    136     public void prepareAnimation(boolean isOpening) {
    137         mIsFirstFrame = true;
    138         mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
    139     }
    140 
    141     public RectF applyTransform(RemoteAnimationTargetSet targetSet, float progress) {
    142         RectF currentRect;
    143         mTmpRectF.set(mTargetRect);
    144         Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale);
    145         float offsetYProgress = mOffsetYInterpolator.getInterpolation(progress);
    146         progress = mInterpolator.getInterpolation(progress);
    147         currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
    148 
    149         synchronized (mTargetOffset) {
    150             // Stay lined up with the center of the target, since it moves for quick scrub.
    151             currentRect.offset(mTargetOffset.x * mOffsetScale * progress,
    152                     mTargetOffset.y  * offsetYProgress);
    153         }
    154 
    155         mClipRect.left = (int) (mSourceWindowClipInsets.left * progress);
    156         mClipRect.top = (int) (mSourceWindowClipInsets.top * progress);
    157         mClipRect.right = (int)
    158                 (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress));
    159         mClipRect.bottom = (int)
    160                 (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress));
    161 
    162         TransactionCompat transaction = new TransactionCompat();
    163         if (mIsFirstFrame) {
    164             RemoteAnimationProvider.prepareTargetsForFirstFrame(targetSet.unfilteredApps,
    165                     transaction, mBoostModeTargetLayers);
    166             mIsFirstFrame = false;
    167         }
    168         for (RemoteAnimationTargetCompat app : targetSet.apps) {
    169             if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
    170                 mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL);
    171                 mTmpMatrix.postTranslate(app.position.x, app.position.y);
    172                 transaction.setMatrix(app.leash, mTmpMatrix)
    173                         .setWindowCrop(app.leash, mClipRect);
    174             }
    175 
    176             if (app.isNotInRecents
    177                     || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
    178                 transaction.setAlpha(app.leash, 1 - progress);
    179             }
    180 
    181             mTaskTransformCallback.accept(transaction, app);
    182         }
    183         transaction.setEarlyWakeup();
    184         transaction.apply();
    185         return currentRect;
    186     }
    187 
    188     public void setTaskTransformCallback
    189             (BiConsumer<TransactionCompat, RemoteAnimationTargetCompat> callback) {
    190         mTaskTransformCallback = callback;
    191     }
    192 
    193     public void offsetTarget(float scale, float offsetX, float offsetY, Interpolator interpolator) {
    194         synchronized (mTargetOffset) {
    195             mTargetOffset.set(offsetX, offsetY);
    196         }
    197         mTargetScale = scale;
    198         mInterpolator = interpolator;
    199         mOffsetYInterpolator = Interpolators.clampToProgress(mInterpolator, 0,
    200                 QUICK_SCRUB_TRANSLATION_Y_FACTOR);
    201     }
    202 
    203     public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
    204         fromTaskThumbnailView(ttv, rv, null);
    205     }
    206 
    207     public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv,
    208             @Nullable RemoteAnimationTargetCompat target) {
    209         BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext());
    210         BaseDragLayer dl = activity.getDragLayer();
    211 
    212         int[] pos = new int[2];
    213         dl.getLocationOnScreen(pos);
    214         mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight());
    215         mHomeStackBounds.offset(pos[0], pos[1]);
    216 
    217         if (target != null) {
    218             updateSourceStack(target);
    219         } else  if (rv.shouldUseMultiWindowTaskSizeStrategy()) {
    220             updateStackBoundsToMultiWindowTaskSize(activity);
    221         } else {
    222             mSourceStackBounds.set(mHomeStackBounds);
    223             mSourceInsets.set(activity.getDeviceProfile().getInsets());
    224         }
    225 
    226         TransformedRect targetRect = new TransformedRect();
    227         dl.getDescendantRectRelativeToSelf(ttv, targetRect.rect);
    228         updateTargetRect(targetRect);
    229 
    230         if (target == null) {
    231             // Transform the clip relative to the target rect. Only do this in the case where we
    232             // aren't applying the insets to the app windows (where the clip should be in target app
    233             // space)
    234             float scale = mTargetRect.width() / mSourceRect.width();
    235             mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale;
    236             mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale;
    237             mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale;
    238             mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale;
    239         }
    240     }
    241 
    242     private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
    243         ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy();
    244         if (sysUiProxy != null) {
    245             try {
    246                 mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds());
    247                 return;
    248             } catch (RemoteException e) {
    249                 // Use half screen size
    250             }
    251         }
    252 
    253         // Assume that the task size is half screen size (minus the insets and the divider size)
    254         DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile();
    255         // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
    256         // account for system insets
    257         int taskWidth = fullDp.availableWidthPx;
    258         int taskHeight = fullDp.availableHeightPx;
    259         int halfDividerSize = activity.getResources()
    260                 .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
    261 
    262         Rect insets = new Rect();
    263         WindowManagerWrapper.getInstance().getStableInsets(insets);
    264         if (fullDp.isLandscape) {
    265             taskWidth = taskWidth / 2 - halfDividerSize;
    266         } else {
    267             taskHeight = taskHeight / 2 - halfDividerSize;
    268         }
    269 
    270         // Align the task to bottom left/right edge (closer to nav bar).
    271         int left = activity.getDeviceProfile().isSeascape() ? insets.left
    272                 : (insets.left + fullDp.availableWidthPx - taskWidth);
    273         mSourceStackBounds.set(0, 0, taskWidth, taskHeight);
    274         mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight);
    275     }
    276 
    277     public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) {
    278         RectF currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
    279         canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left,
    280                 mSourceStackBounds.top - mHomeStackBounds.top);
    281         mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL);
    282 
    283         canvas.concat(mTmpMatrix);
    284         canvas.translate(mTargetRect.left, mTargetRect.top);
    285 
    286         float insetProgress = (1 - progress);
    287         ttv.drawOnCanvas(canvas,
    288                 -mSourceWindowClipInsets.left * insetProgress,
    289                 -mSourceWindowClipInsets.top * insetProgress,
    290                 ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress,
    291                 ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress,
    292                 ttv.getCornerRadius() * progress);
    293     }
    294 
    295     public RectF getTargetRect() {
    296         return mTargetRect;
    297     }
    298 
    299     public RectF getSourceRect() {
    300         return mSourceRect;
    301     }
    302 }
    303