• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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