| /* |
| * 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.quickstep.util; |
| |
| import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; |
| import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius; |
| import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; |
| import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; |
| import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.Matrix; |
| import android.graphics.Matrix.ScaleToFit; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.os.Build; |
| import android.os.RemoteException; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.launcher3.BaseDraggingActivity; |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.LauncherState; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.views.BaseDragLayer; |
| import com.android.quickstep.RecentsModel; |
| import com.android.quickstep.views.RecentsView; |
| import com.android.quickstep.views.TaskThumbnailView; |
| import com.android.quickstep.views.TaskView; |
| import com.android.systemui.shared.recents.ISystemUiProxy; |
| import com.android.systemui.shared.recents.utilities.RectFEvaluator; |
| import com.android.systemui.shared.system.RemoteAnimationTargetCompat; |
| import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; |
| import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; |
| import com.android.systemui.shared.system.TransactionCompat; |
| import com.android.systemui.shared.system.WindowManagerWrapper; |
| |
| import java.util.function.BiFunction; |
| |
| /** |
| * Utility class to handle window clip animation |
| */ |
| @TargetApi(Build.VERSION_CODES.P) |
| public class ClipAnimationHelper { |
| |
| // The bounds of the source app in device coordinates |
| private final Rect mSourceStackBounds = new Rect(); |
| // The insets of the source app |
| private final Rect mSourceInsets = new Rect(); |
| // The source app bounds with the source insets applied, in the source app window coordinates |
| private final RectF mSourceRect = new RectF(); |
| // The bounds of the task view in launcher window coordinates |
| private final RectF mTargetRect = new RectF(); |
| // The insets to be used for clipping the app window, which can be larger than mSourceInsets |
| // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In |
| // app window coordinates. |
| private final RectF mSourceWindowClipInsets = new RectF(); |
| // The insets to be used for clipping the app window. For live tile, we don't transform the clip |
| // relative to the target rect. |
| private final RectF mSourceWindowClipInsetsForLiveTile = new RectF(); |
| |
| // The bounds of launcher (not including insets) in device coordinates |
| public final Rect mHomeStackBounds = new Rect(); |
| |
| // The clip rect in source app window coordinates |
| private final RectF mClipRectF = new RectF(); |
| private final RectFEvaluator mRectFEvaluator = new RectFEvaluator(); |
| private final Matrix mTmpMatrix = new Matrix(); |
| private final Rect mTmpRect = new Rect(); |
| private final RectF mTmpRectF = new RectF(); |
| private final RectF mCurrentRectWithInsets = new RectF(); |
| // Corner radius of windows, in pixels |
| private final float mWindowCornerRadius; |
| // Corner radius of windows when they're in overview mode. |
| private final float mTaskCornerRadius; |
| // If windows can have real time rounded corners. |
| private final boolean mSupportsRoundedCornersOnWindows; |
| // Whether or not to actually use the rounded cornders on windows |
| private boolean mUseRoundedCornersOnWindows; |
| |
| // Corner radius currently applied to transformed window. |
| private float mCurrentCornerRadius; |
| |
| // Whether to boost the opening animation target layers, or the closing |
| private int mBoostModeTargetLayers = -1; |
| |
| private BiFunction<RemoteAnimationTargetCompat, Float, Float> mTaskAlphaCallback = |
| (t, a1) -> a1; |
| |
| public ClipAnimationHelper(Context context) { |
| mWindowCornerRadius = getWindowCornerRadius(context.getResources()); |
| mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources()); |
| mTaskCornerRadius = TaskCornerRadius.get(context); |
| mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows; |
| } |
| |
| private void updateSourceStack(RemoteAnimationTargetCompat target) { |
| mSourceInsets.set(target.contentInsets); |
| mSourceStackBounds.set(target.sourceContainerBounds); |
| |
| // TODO: Should sourceContainerBounds already have this offset? |
| mSourceStackBounds.offsetTo(target.position.x, target.position.y); |
| |
| } |
| |
| public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) { |
| mHomeStackBounds.set(homeStackBounds); |
| updateSourceStack(target); |
| } |
| |
| public void updateTargetRect(Rect targetRect) { |
| mSourceRect.set(mSourceInsets.left, mSourceInsets.top, |
| mSourceStackBounds.width() - mSourceInsets.right, |
| mSourceStackBounds.height() - mSourceInsets.bottom); |
| mTargetRect.set(targetRect); |
| mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left, |
| mHomeStackBounds.top - mSourceStackBounds.top); |
| |
| // Calculate the clip based on the target rect (since the content insets and the |
| // launcher insets may differ, so the aspect ratio of the target rect can differ |
| // from the source rect. The difference between the target rect (scaled to the |
| // source rect) is the amount to clip on each edge. |
| RectF scaledTargetRect = new RectF(mTargetRect); |
| Utilities.scaleRectFAboutCenter(scaledTargetRect, |
| mSourceRect.width() / mTargetRect.width()); |
| scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top); |
| mSourceWindowClipInsets.set( |
| Math.max(scaledTargetRect.left, 0), |
| Math.max(scaledTargetRect.top, 0), |
| Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0), |
| Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0)); |
| mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets); |
| mSourceRect.set(scaledTargetRect); |
| } |
| |
| public void prepareAnimation(DeviceProfile dp, boolean isOpening) { |
| mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING; |
| mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode; |
| } |
| |
| public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) { |
| return applyTransform(targetSet, params, true /* launcherOnTop */); |
| } |
| |
| public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params, |
| boolean launcherOnTop) { |
| float progress = params.progress; |
| if (params.currentRect == null) { |
| RectF currentRect; |
| mTmpRectF.set(mTargetRect); |
| Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale); |
| currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF); |
| currentRect.offset(params.offsetX, 0); |
| |
| // Don't clip past progress > 1. |
| progress = Math.min(1, progress); |
| final RectF sourceWindowClipInsets = params.forLiveTile |
| ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets; |
| mClipRectF.left = sourceWindowClipInsets.left * progress; |
| mClipRectF.top = sourceWindowClipInsets.top * progress; |
| mClipRectF.right = |
| mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress); |
| mClipRectF.bottom = |
| mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress); |
| params.setCurrentRectAndTargetAlpha(currentRect, 1); |
| } |
| |
| SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length]; |
| for (int i = 0; i < targetSet.unfilteredApps.length; i++) { |
| RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i]; |
| mTmpMatrix.setTranslate(app.position.x, app.position.y); |
| Rect crop = mTmpRect; |
| crop.set(app.sourceContainerBounds); |
| crop.offsetTo(0, 0); |
| float alpha = 1f; |
| int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers); |
| float cornerRadius = 0f; |
| float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width(); |
| if (app.mode == targetSet.targetMode) { |
| alpha = mTaskAlphaCallback.apply(app, params.targetAlpha); |
| if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { |
| mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL); |
| mTmpMatrix.postTranslate(app.position.x, app.position.y); |
| mClipRectF.roundOut(crop); |
| if (mSupportsRoundedCornersOnWindows) { |
| if (params.cornerRadius > -1) { |
| cornerRadius = params.cornerRadius; |
| scale = params.currentRect.width() / crop.width(); |
| } else { |
| float windowCornerRadius = mUseRoundedCornersOnWindows |
| ? mWindowCornerRadius : 0; |
| cornerRadius = Utilities.mapRange(progress, windowCornerRadius, |
| mTaskCornerRadius); |
| } |
| mCurrentCornerRadius = cornerRadius; |
| } |
| } else if (targetSet.hasRecents) { |
| // If home has a different target then recents, reverse anim the |
| // home target. |
| alpha = 1 - (progress * params.targetAlpha); |
| } |
| } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) { |
| crop = null; |
| layer = Integer.MAX_VALUE; |
| } |
| |
| // Since radius is in Surface space, but we draw the rounded corners in screen space, we |
| // have to undo the scale. |
| surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer, |
| cornerRadius / scale); |
| } |
| applySurfaceParams(params.syncTransactionApplier, surfaceParams); |
| return params.currentRect; |
| } |
| |
| public RectF getCurrentRectWithInsets() { |
| mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF); |
| return mCurrentRectWithInsets; |
| } |
| |
| private void applySurfaceParams(@Nullable SyncRtSurfaceTransactionApplierCompat |
| syncTransactionApplier, SurfaceParams[] params) { |
| if (syncTransactionApplier != null) { |
| syncTransactionApplier.scheduleApply(params); |
| } else { |
| TransactionCompat t = new TransactionCompat(); |
| for (SurfaceParams param : params) { |
| SyncRtSurfaceTransactionApplierCompat.applyParams(t, param); |
| } |
| t.setEarlyWakeup(); |
| t.apply(); |
| } |
| } |
| |
| public void setTaskAlphaCallback( |
| BiFunction<RemoteAnimationTargetCompat, Float, Float> callback) { |
| mTaskAlphaCallback = callback; |
| } |
| |
| public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) { |
| fromTaskThumbnailView(ttv, rv, null); |
| } |
| |
| public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, |
| @Nullable RemoteAnimationTargetCompat target) { |
| BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext()); |
| BaseDragLayer dl = activity.getDragLayer(); |
| |
| int[] pos = new int[2]; |
| dl.getLocationOnScreen(pos); |
| mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight()); |
| mHomeStackBounds.offset(pos[0], pos[1]); |
| |
| if (target != null) { |
| updateSourceStack(target); |
| } else if (rv.shouldUseMultiWindowTaskSizeStrategy()) { |
| updateStackBoundsToMultiWindowTaskSize(activity); |
| } else { |
| mSourceStackBounds.set(mHomeStackBounds); |
| Rect fallback = dl.getInsets(); |
| mSourceInsets.set(ttv.getInsets(fallback)); |
| } |
| |
| Rect targetRect = new Rect(); |
| dl.getDescendantRectRelativeToSelf(ttv, targetRect); |
| updateTargetRect(targetRect); |
| |
| if (target == null) { |
| // Transform the clip relative to the target rect. Only do this in the case where we |
| // aren't applying the insets to the app windows (where the clip should be in target app |
| // space) |
| float scale = mTargetRect.width() / mSourceRect.width(); |
| mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale; |
| mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale; |
| mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale; |
| mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale; |
| } |
| } |
| |
| /** |
| * Compute scale and translation y such that the specified task view fills the screen. |
| */ |
| public ClipAnimationHelper updateForFullscreenOverview(TaskView v) { |
| TaskThumbnailView thumbnailView = v.getThumbnail(); |
| RecentsView recentsView = v.getRecentsView(); |
| fromTaskThumbnailView(thumbnailView, recentsView); |
| Rect taskSize = new Rect(); |
| recentsView.getTaskSize(taskSize); |
| updateTargetRect(taskSize); |
| return this; |
| } |
| |
| /** |
| * @return The source rect's scale and translation relative to the target rect. |
| */ |
| public LauncherState.ScaleAndTranslation getScaleAndTranslation() { |
| float scale = mSourceRect.width() / mTargetRect.width(); |
| float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY(); |
| return new LauncherState.ScaleAndTranslation(scale, 0, translationY); |
| } |
| |
| private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) { |
| ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy(); |
| if (sysUiProxy != null) { |
| try { |
| mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds()); |
| return; |
| } catch (RemoteException e) { |
| // Use half screen size |
| } |
| } |
| |
| // Assume that the task size is half screen size (minus the insets and the divider size) |
| DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile(); |
| // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to |
| // account for system insets |
| int taskWidth = fullDp.availableWidthPx; |
| int taskHeight = fullDp.availableHeightPx; |
| int halfDividerSize = activity.getResources() |
| .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; |
| |
| Rect insets = new Rect(); |
| WindowManagerWrapper.getInstance().getStableInsets(insets); |
| if (fullDp.isLandscape) { |
| taskWidth = taskWidth / 2 - halfDividerSize; |
| } else { |
| taskHeight = taskHeight / 2 - halfDividerSize; |
| } |
| |
| // Align the task to bottom left/right edge (closer to nav bar). |
| int left = activity.getDeviceProfile().isSeascape() ? insets.left |
| : (insets.left + fullDp.availableWidthPx - taskWidth); |
| mSourceStackBounds.set(0, 0, taskWidth, taskHeight); |
| mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight); |
| } |
| |
| public RectF getTargetRect() { |
| return mTargetRect; |
| } |
| |
| public float getCurrentCornerRadius() { |
| return mCurrentCornerRadius; |
| } |
| |
| public static class TransformParams { |
| float progress; |
| public float offsetX; |
| public float offsetScale; |
| @Nullable RectF currentRect; |
| float targetAlpha; |
| boolean forLiveTile; |
| float cornerRadius; |
| |
| SyncRtSurfaceTransactionApplierCompat syncTransactionApplier; |
| |
| public TransformParams() { |
| progress = 0; |
| offsetX = 0; |
| offsetScale = 1; |
| currentRect = null; |
| targetAlpha = 0; |
| forLiveTile = false; |
| cornerRadius = -1; |
| } |
| |
| public TransformParams setProgress(float progress) { |
| this.progress = progress; |
| this.currentRect = null; |
| return this; |
| } |
| |
| public TransformParams setCornerRadius(float cornerRadius) { |
| this.cornerRadius = cornerRadius; |
| return this; |
| } |
| |
| public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) { |
| this.currentRect = currentRect; |
| this.targetAlpha = targetAlpha; |
| return this; |
| } |
| |
| public TransformParams setOffsetX(float offsetX) { |
| this.offsetX = offsetX; |
| return this; |
| } |
| |
| public TransformParams setOffsetScale(float offsetScale) { |
| this.offsetScale = offsetScale; |
| return this; |
| } |
| |
| public TransformParams setForLiveTile(boolean forLiveTile) { |
| this.forLiveTile = forLiveTile; |
| return this; |
| } |
| |
| public TransformParams setSyncTransactionApplier( |
| SyncRtSurfaceTransactionApplierCompat applier) { |
| this.syncTransactionApplier = applier; |
| return this; |
| } |
| } |
| } |