1 /* 2 * Copyright (C) 2025 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.wm.shell.common.split; 18 19 import static android.view.WindowManager.DOCKED_BOTTOM; 20 import static android.view.WindowManager.DOCKED_INVALID; 21 import static android.view.WindowManager.DOCKED_LEFT; 22 import static android.view.WindowManager.DOCKED_RIGHT; 23 import static android.view.WindowManager.DOCKED_TOP; 24 25 import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM; 26 import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; 27 import static com.android.wm.shell.shared.animation.Interpolators.FAST_DIM_INTERPOLATOR; 28 29 import android.graphics.Point; 30 import android.graphics.Rect; 31 32 /** 33 * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_FLEX} 34 * is the desired parallax effect. 35 */ 36 public class FlexParallaxSpec implements ParallaxSpec { 37 final Rect mTempRect = new Rect(); 38 39 @Override getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit)40 public int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, 41 boolean isLeftRightSplit) { 42 if (position < snapAlgorithm.getMiddleTarget().getPosition()) { 43 return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; 44 } else if (position > snapAlgorithm.getMiddleTarget().getPosition()) { 45 return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; 46 } 47 return DOCKED_INVALID; 48 } 49 50 /** 51 * Calculates the amount of dim to apply to a task surface moving offscreen in flexible split. 52 * In flexible split, there are two dimming "behaviors". 53 * 1) "slow dim": when moving the divider from the middle of the screen to a target at 10% or 54 * 90%, we dim the app slightly as it moves partially offscreen. 55 * 2) "fast dim": when moving the divider from a side snap target further toward the screen 56 * edge, we dim the app rapidly as it approaches the dismiss point. 57 * @return 0f = no dim applied. 1f = full black. 58 */ getDimValue(int position, DividerSnapAlgorithm snapAlgorithm)59 public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { 60 // On tablets, apps don't go offscreen, so only dim for dismissal. 61 if (!snapAlgorithm.areOffscreenRatiosSupported()) { 62 return ParallaxSpec.super.getDimValue(position, snapAlgorithm); 63 } 64 65 int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition(); 66 int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition(); 67 int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition(); 68 int lastTargetPos = snapAlgorithm.getLastSplitTarget().getPosition(); 69 int endDismissPos = snapAlgorithm.getDismissEndTarget().getPosition(); 70 float progress; 71 72 if (startDismissPos <= position && position < firstTargetPos) { 73 // Divider is on the left/top (between 0% and 10% of screen), "fast dim" as it moves 74 // toward the screen edge 75 progress = (float) (firstTargetPos - position) / (firstTargetPos - startDismissPos); 76 return fastDim(progress); 77 } else if (firstTargetPos <= position && position < middleTargetPos) { 78 // Divider is between 10% and 50%, "slow dim" as it moves toward the left/top target 79 progress = (float) (middleTargetPos - position) / (middleTargetPos - firstTargetPos); 80 return slowDim(progress); 81 } else if (middleTargetPos <= position && position < lastTargetPos) { 82 // Divider is between 50% and 90%, "slow dim" as it moves toward the right/bottom target 83 progress = (float) (position - middleTargetPos) / (lastTargetPos - middleTargetPos); 84 return slowDim(progress); 85 } else if (lastTargetPos <= position && position <= endDismissPos) { 86 // Divider is on the right/bottom (between 90% and 100% of screen), "fast dim" as it 87 // moves toward screen edge 88 progress = (float) (position - lastTargetPos) / (endDismissPos - lastTargetPos); 89 return fastDim(progress); 90 } 91 return 0f; 92 } 93 94 /** 95 * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at zero and ramps 96 * up to the default amount of dimming for an offscreen app, 97 * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM}. 98 */ slowDim(float progress)99 private float slowDim(float progress) { 100 return DIM_INTERPOLATOR.getInterpolation(progress) * DEFAULT_OFFSCREEN_DIM; 101 } 102 103 /** 104 * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at 105 * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM} and ramps up to 100% dim (full black). 106 */ fastDim(float progress)107 private float fastDim(float progress) { 108 return DEFAULT_OFFSCREEN_DIM + (FAST_DIM_INTERPOLATOR.getInterpolation(progress) 109 * (1 - DEFAULT_OFFSCREEN_DIM)); 110 } 111 112 @Override getParallax(Point retreatingOut, Point advancingOut, int position, DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, Rect advancingContent, int dimmingSide, boolean topLeftShrink)113 public void getParallax(Point retreatingOut, Point advancingOut, int position, 114 DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, 115 Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, 116 Rect advancingContent, int dimmingSide, boolean topLeftShrink) { 117 // Whether an app is getting pushed offscreen by the divider. 118 boolean isRetreatingOffscreen = !displayBounds.contains(retreatingSurface); 119 // Whether an app was getting pulled onscreen at the beginning of the drag. 120 boolean advancingSideStartedOffscreen = !displayBounds.contains(advancingContent); 121 122 // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10) 123 if (isRetreatingOffscreen && !advancingSideStartedOffscreen) { 124 // On the left side, we use parallax to simulate the contents sticking to the 125 // divider. This is because surfaces naturally expand to the bottom and right, 126 // so when a surface's area expands, the contents stick to the left. This is 127 // correct behavior on the right-side surface, but not the left. 128 if (topLeftShrink) { 129 if (isLeftRightSplit) { 130 retreatingOut.x = retreatingSurface.width() - retreatingContent.width(); 131 } else { 132 retreatingOut.y = retreatingSurface.height() - retreatingContent.height(); 133 } 134 } 135 // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss) 136 } else { 137 mTempRect.set(retreatingSurface); 138 Point rootOffset = new Point(); 139 // 10:90 -> 50:50, 10:90, or dismiss right 140 if (advancingSideStartedOffscreen) { 141 // We have to handle a complicated case here to keep the parallax smooth. 142 // When the divider crosses the 50% mark, the retreating-side app surface 143 // will start expanding offscreen. This is expected and unavoidable, but 144 // makes the parallax look disjointed. In order to preserve the illusion, 145 // we add another offset (rootOffset) to simulate the surface staying 146 // onscreen. 147 if (mTempRect.intersect(displayBounds)) { 148 if (retreatingSurface.left < displayBounds.left) { 149 rootOffset.x = displayBounds.left - retreatingSurface.left; 150 } 151 if (retreatingSurface.top < displayBounds.top) { 152 rootOffset.y = displayBounds.top - retreatingSurface.top; 153 } 154 } 155 156 // On the left side, we again have to simulate the contents sticking to the 157 // divider. 158 if (!topLeftShrink) { 159 if (isLeftRightSplit) { 160 advancingOut.x = advancingSurface.width() - advancingContent.width(); 161 } else { 162 advancingOut.y = advancingSurface.height() - advancingContent.height(); 163 } 164 } 165 } 166 167 // In all these cases, the shrinking app also receives a center parallax. 168 if (isLeftRightSplit) { 169 retreatingOut.x = rootOffset.x 170 + ((mTempRect.width() - retreatingContent.width()) / 2); 171 } else { 172 retreatingOut.y = rootOffset.y 173 + ((mTempRect.height() - retreatingContent.height()) / 2); 174 } 175 } 176 } 177 } 178