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.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 19 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius; 20 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; 21 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; 22 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; 23 24 import android.annotation.TargetApi; 25 import android.content.Context; 26 import android.graphics.Matrix; 27 import android.graphics.Matrix.ScaleToFit; 28 import android.graphics.Rect; 29 import android.graphics.RectF; 30 import android.os.Build; 31 import android.os.RemoteException; 32 33 import androidx.annotation.Nullable; 34 35 import com.android.launcher3.BaseDraggingActivity; 36 import com.android.launcher3.DeviceProfile; 37 import com.android.launcher3.LauncherState; 38 import com.android.launcher3.R; 39 import com.android.launcher3.Utilities; 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.quickstep.views.TaskView; 45 import com.android.systemui.shared.recents.ISystemUiProxy; 46 import com.android.systemui.shared.recents.utilities.RectFEvaluator; 47 import com.android.systemui.shared.system.RemoteAnimationTargetCompat; 48 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; 49 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; 50 import com.android.systemui.shared.system.TransactionCompat; 51 import com.android.systemui.shared.system.WindowManagerWrapper; 52 53 import java.util.function.BiFunction; 54 55 /** 56 * Utility class to handle window clip animation 57 */ 58 @TargetApi(Build.VERSION_CODES.P) 59 public class ClipAnimationHelper { 60 61 // The bounds of the source app in device coordinates 62 private final Rect mSourceStackBounds = new Rect(); 63 // The insets of the source app 64 private final Rect mSourceInsets = new Rect(); 65 // The source app bounds with the source insets applied, in the source app window coordinates 66 private final RectF mSourceRect = new RectF(); 67 // The bounds of the task view in launcher window coordinates 68 private final RectF mTargetRect = new RectF(); 69 // The insets to be used for clipping the app window, which can be larger than mSourceInsets 70 // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In 71 // app window coordinates. 72 private final RectF mSourceWindowClipInsets = new RectF(); 73 // The insets to be used for clipping the app window. For live tile, we don't transform the clip 74 // relative to the target rect. 75 private final RectF mSourceWindowClipInsetsForLiveTile = new RectF(); 76 77 // The bounds of launcher (not including insets) in device coordinates 78 public final Rect mHomeStackBounds = new Rect(); 79 80 // The clip rect in source app window coordinates 81 private final RectF mClipRectF = new RectF(); 82 private final RectFEvaluator mRectFEvaluator = new RectFEvaluator(); 83 private final Matrix mTmpMatrix = new Matrix(); 84 private final Rect mTmpRect = new Rect(); 85 private final RectF mTmpRectF = new RectF(); 86 private final RectF mCurrentRectWithInsets = new RectF(); 87 // Corner radius of windows, in pixels 88 private final float mWindowCornerRadius; 89 // Corner radius of windows when they're in overview mode. 90 private final float mTaskCornerRadius; 91 // If windows can have real time rounded corners. 92 private final boolean mSupportsRoundedCornersOnWindows; 93 // Whether or not to actually use the rounded cornders on windows 94 private boolean mUseRoundedCornersOnWindows; 95 96 // Corner radius currently applied to transformed window. 97 private float mCurrentCornerRadius; 98 99 // Whether to boost the opening animation target layers, or the closing 100 private int mBoostModeTargetLayers = -1; 101 102 private BiFunction<RemoteAnimationTargetCompat, Float, Float> mTaskAlphaCallback = 103 (t, a1) -> a1; 104 ClipAnimationHelper(Context context)105 public ClipAnimationHelper(Context context) { 106 mWindowCornerRadius = getWindowCornerRadius(context.getResources()); 107 mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources()); 108 mTaskCornerRadius = TaskCornerRadius.get(context); 109 mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows; 110 } 111 updateSourceStack(RemoteAnimationTargetCompat target)112 private void updateSourceStack(RemoteAnimationTargetCompat target) { 113 mSourceInsets.set(target.contentInsets); 114 mSourceStackBounds.set(target.sourceContainerBounds); 115 116 // TODO: Should sourceContainerBounds already have this offset? 117 mSourceStackBounds.offsetTo(target.position.x, target.position.y); 118 119 } 120 updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target)121 public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) { 122 mHomeStackBounds.set(homeStackBounds); 123 updateSourceStack(target); 124 } 125 updateTargetRect(Rect targetRect)126 public void updateTargetRect(Rect targetRect) { 127 mSourceRect.set(mSourceInsets.left, mSourceInsets.top, 128 mSourceStackBounds.width() - mSourceInsets.right, 129 mSourceStackBounds.height() - mSourceInsets.bottom); 130 mTargetRect.set(targetRect); 131 mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left, 132 mHomeStackBounds.top - mSourceStackBounds.top); 133 134 // Calculate the clip based on the target rect (since the content insets and the 135 // launcher insets may differ, so the aspect ratio of the target rect can differ 136 // from the source rect. The difference between the target rect (scaled to the 137 // source rect) is the amount to clip on each edge. 138 RectF scaledTargetRect = new RectF(mTargetRect); 139 Utilities.scaleRectFAboutCenter(scaledTargetRect, 140 mSourceRect.width() / mTargetRect.width()); 141 scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top); 142 mSourceWindowClipInsets.set( 143 Math.max(scaledTargetRect.left, 0), 144 Math.max(scaledTargetRect.top, 0), 145 Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0), 146 Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0)); 147 mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets); 148 mSourceRect.set(scaledTargetRect); 149 } 150 prepareAnimation(DeviceProfile dp, boolean isOpening)151 public void prepareAnimation(DeviceProfile dp, boolean isOpening) { 152 mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING; 153 mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode; 154 } 155 applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params)156 public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) { 157 return applyTransform(targetSet, params, true /* launcherOnTop */); 158 } 159 applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params, boolean launcherOnTop)160 public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params, 161 boolean launcherOnTop) { 162 float progress = params.progress; 163 if (params.currentRect == null) { 164 RectF currentRect; 165 mTmpRectF.set(mTargetRect); 166 Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale); 167 currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF); 168 currentRect.offset(params.offsetX, 0); 169 170 // Don't clip past progress > 1. 171 progress = Math.min(1, progress); 172 final RectF sourceWindowClipInsets = params.forLiveTile 173 ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets; 174 mClipRectF.left = sourceWindowClipInsets.left * progress; 175 mClipRectF.top = sourceWindowClipInsets.top * progress; 176 mClipRectF.right = 177 mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress); 178 mClipRectF.bottom = 179 mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress); 180 params.setCurrentRectAndTargetAlpha(currentRect, 1); 181 } 182 183 SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length]; 184 for (int i = 0; i < targetSet.unfilteredApps.length; i++) { 185 RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i]; 186 mTmpMatrix.setTranslate(app.position.x, app.position.y); 187 Rect crop = mTmpRect; 188 crop.set(app.sourceContainerBounds); 189 crop.offsetTo(0, 0); 190 float alpha = 1f; 191 int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers); 192 float cornerRadius = 0f; 193 float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width(); 194 if (app.mode == targetSet.targetMode) { 195 alpha = mTaskAlphaCallback.apply(app, params.targetAlpha); 196 if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { 197 mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL); 198 mTmpMatrix.postTranslate(app.position.x, app.position.y); 199 mClipRectF.roundOut(crop); 200 if (mSupportsRoundedCornersOnWindows) { 201 if (params.cornerRadius > -1) { 202 cornerRadius = params.cornerRadius; 203 scale = params.currentRect.width() / crop.width(); 204 } else { 205 float windowCornerRadius = mUseRoundedCornersOnWindows 206 ? mWindowCornerRadius : 0; 207 cornerRadius = Utilities.mapRange(progress, windowCornerRadius, 208 mTaskCornerRadius); 209 } 210 mCurrentCornerRadius = cornerRadius; 211 } 212 } else if (targetSet.hasRecents) { 213 // If home has a different target then recents, reverse anim the 214 // home target. 215 alpha = 1 - (progress * params.targetAlpha); 216 } 217 } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) { 218 crop = null; 219 layer = Integer.MAX_VALUE; 220 } 221 222 // Since radius is in Surface space, but we draw the rounded corners in screen space, we 223 // have to undo the scale. 224 surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer, 225 cornerRadius / scale); 226 } 227 applySurfaceParams(params.syncTransactionApplier, surfaceParams); 228 return params.currentRect; 229 } 230 getCurrentRectWithInsets()231 public RectF getCurrentRectWithInsets() { 232 mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF); 233 return mCurrentRectWithInsets; 234 } 235 applySurfaceParams(@ullable SyncRtSurfaceTransactionApplierCompat syncTransactionApplier, SurfaceParams[] params)236 private void applySurfaceParams(@Nullable SyncRtSurfaceTransactionApplierCompat 237 syncTransactionApplier, SurfaceParams[] params) { 238 if (syncTransactionApplier != null) { 239 syncTransactionApplier.scheduleApply(params); 240 } else { 241 TransactionCompat t = new TransactionCompat(); 242 for (SurfaceParams param : params) { 243 SyncRtSurfaceTransactionApplierCompat.applyParams(t, param); 244 } 245 t.setEarlyWakeup(); 246 t.apply(); 247 } 248 } 249 setTaskAlphaCallback( BiFunction<RemoteAnimationTargetCompat, Float, Float> callback)250 public void setTaskAlphaCallback( 251 BiFunction<RemoteAnimationTargetCompat, Float, Float> callback) { 252 mTaskAlphaCallback = callback; 253 } 254 fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv)255 public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) { 256 fromTaskThumbnailView(ttv, rv, null); 257 } 258 fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, @Nullable RemoteAnimationTargetCompat target)259 public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, 260 @Nullable RemoteAnimationTargetCompat target) { 261 BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext()); 262 BaseDragLayer dl = activity.getDragLayer(); 263 264 int[] pos = new int[2]; 265 dl.getLocationOnScreen(pos); 266 mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight()); 267 mHomeStackBounds.offset(pos[0], pos[1]); 268 269 if (target != null) { 270 updateSourceStack(target); 271 } else if (rv.shouldUseMultiWindowTaskSizeStrategy()) { 272 updateStackBoundsToMultiWindowTaskSize(activity); 273 } else { 274 mSourceStackBounds.set(mHomeStackBounds); 275 Rect fallback = dl.getInsets(); 276 mSourceInsets.set(ttv.getInsets(fallback)); 277 } 278 279 Rect targetRect = new Rect(); 280 dl.getDescendantRectRelativeToSelf(ttv, targetRect); 281 updateTargetRect(targetRect); 282 283 if (target == null) { 284 // Transform the clip relative to the target rect. Only do this in the case where we 285 // aren't applying the insets to the app windows (where the clip should be in target app 286 // space) 287 float scale = mTargetRect.width() / mSourceRect.width(); 288 mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale; 289 mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale; 290 mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale; 291 mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale; 292 } 293 } 294 295 /** 296 * Compute scale and translation y such that the specified task view fills the screen. 297 */ updateForFullscreenOverview(TaskView v)298 public ClipAnimationHelper updateForFullscreenOverview(TaskView v) { 299 TaskThumbnailView thumbnailView = v.getThumbnail(); 300 RecentsView recentsView = v.getRecentsView(); 301 fromTaskThumbnailView(thumbnailView, recentsView); 302 Rect taskSize = new Rect(); 303 recentsView.getTaskSize(taskSize); 304 updateTargetRect(taskSize); 305 return this; 306 } 307 308 /** 309 * @return The source rect's scale and translation relative to the target rect. 310 */ getScaleAndTranslation()311 public LauncherState.ScaleAndTranslation getScaleAndTranslation() { 312 float scale = mSourceRect.width() / mTargetRect.width(); 313 float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY(); 314 return new LauncherState.ScaleAndTranslation(scale, 0, translationY); 315 } 316 updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity)317 private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) { 318 ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy(); 319 if (sysUiProxy != null) { 320 try { 321 mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds()); 322 return; 323 } catch (RemoteException e) { 324 // Use half screen size 325 } 326 } 327 328 // Assume that the task size is half screen size (minus the insets and the divider size) 329 DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile(); 330 // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to 331 // account for system insets 332 int taskWidth = fullDp.availableWidthPx; 333 int taskHeight = fullDp.availableHeightPx; 334 int halfDividerSize = activity.getResources() 335 .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; 336 337 Rect insets = new Rect(); 338 WindowManagerWrapper.getInstance().getStableInsets(insets); 339 if (fullDp.isLandscape) { 340 taskWidth = taskWidth / 2 - halfDividerSize; 341 } else { 342 taskHeight = taskHeight / 2 - halfDividerSize; 343 } 344 345 // Align the task to bottom left/right edge (closer to nav bar). 346 int left = activity.getDeviceProfile().isSeascape() ? insets.left 347 : (insets.left + fullDp.availableWidthPx - taskWidth); 348 mSourceStackBounds.set(0, 0, taskWidth, taskHeight); 349 mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight); 350 } 351 getTargetRect()352 public RectF getTargetRect() { 353 return mTargetRect; 354 } 355 getCurrentCornerRadius()356 public float getCurrentCornerRadius() { 357 return mCurrentCornerRadius; 358 } 359 360 public static class TransformParams { 361 float progress; 362 public float offsetX; 363 public float offsetScale; 364 @Nullable RectF currentRect; 365 float targetAlpha; 366 boolean forLiveTile; 367 float cornerRadius; 368 369 SyncRtSurfaceTransactionApplierCompat syncTransactionApplier; 370 TransformParams()371 public TransformParams() { 372 progress = 0; 373 offsetX = 0; 374 offsetScale = 1; 375 currentRect = null; 376 targetAlpha = 0; 377 forLiveTile = false; 378 cornerRadius = -1; 379 } 380 setProgress(float progress)381 public TransformParams setProgress(float progress) { 382 this.progress = progress; 383 this.currentRect = null; 384 return this; 385 } 386 setCornerRadius(float cornerRadius)387 public TransformParams setCornerRadius(float cornerRadius) { 388 this.cornerRadius = cornerRadius; 389 return this; 390 } 391 setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha)392 public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) { 393 this.currentRect = currentRect; 394 this.targetAlpha = targetAlpha; 395 return this; 396 } 397 setOffsetX(float offsetX)398 public TransformParams setOffsetX(float offsetX) { 399 this.offsetX = offsetX; 400 return this; 401 } 402 setOffsetScale(float offsetScale)403 public TransformParams setOffsetScale(float offsetScale) { 404 this.offsetScale = offsetScale; 405 return this; 406 } 407 setForLiveTile(boolean forLiveTile)408 public TransformParams setForLiveTile(boolean forLiveTile) { 409 this.forLiveTile = forLiveTile; 410 return this; 411 } 412 setSyncTransactionApplier( SyncRtSurfaceTransactionApplierCompat applier)413 public TransformParams setSyncTransactionApplier( 414 SyncRtSurfaceTransactionApplierCompat applier) { 415 this.syncTransactionApplier = applier; 416 return this; 417 } 418 } 419 } 420 421