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 */ 16package com.android.quickstep.util; 17 18import static com.android.launcher3.anim.Interpolators.LINEAR; 19import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; 20import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; 21import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; 22 23import android.annotation.TargetApi; 24import android.graphics.Canvas; 25import android.graphics.Matrix; 26import android.graphics.Matrix.ScaleToFit; 27import android.graphics.PointF; 28import android.graphics.Rect; 29import android.graphics.RectF; 30import android.os.Build; 31import android.os.RemoteException; 32import android.support.annotation.Nullable; 33import android.view.animation.Interpolator; 34 35import com.android.launcher3.BaseDraggingActivity; 36import com.android.launcher3.DeviceProfile; 37import com.android.launcher3.R; 38import com.android.launcher3.Utilities; 39import com.android.launcher3.anim.Interpolators; 40import com.android.launcher3.views.BaseDragLayer; 41import com.android.quickstep.RecentsModel; 42import com.android.quickstep.views.RecentsView; 43import com.android.quickstep.views.TaskThumbnailView; 44import com.android.systemui.shared.recents.ISystemUiProxy; 45import com.android.systemui.shared.recents.utilities.RectFEvaluator; 46import com.android.systemui.shared.system.RemoteAnimationTargetCompat; 47import com.android.systemui.shared.system.TransactionCompat; 48import com.android.systemui.shared.system.WindowManagerWrapper; 49 50import java.util.function.BiConsumer; 51 52/** 53 * Utility class to handle window clip animation 54 */ 55@TargetApi(Build.VERSION_CODES.P) 56public 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