• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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_LEFT;
20 import static android.view.WindowManager.DOCKED_RIGHT;
21 
22 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
23 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66;
24 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
25 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33;
26 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
27 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
28 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE;
29 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
30 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS;
31 import static com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition;
32 
33 import android.content.res.Resources;
34 import android.graphics.Rect;
35 
36 import androidx.annotation.Nullable;
37 
38 import com.android.wm.shell.Flags;
39 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
40 
41 import java.util.ArrayList;
42 import java.util.stream.IntStream;
43 
44 /**
45  * Calculates the snap targets and the snap position given a position and a velocity. All positions
46  * here are to be interpreted as the left/top edge of the divider rectangle.
47  *
48  * @hide
49  */
50 public class DividerSnapAlgorithm {
51 
52     private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
53     private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
54 
55     /**
56      * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio
57      */
58     private static final int SNAP_MODE_16_9 = 0;
59 
60     /**
61      * 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)
62      */
63     private static final int SNAP_FIXED_RATIO = 1;
64 
65     /**
66      * 1 snap target: 1:1
67      */
68     private static final int SNAP_ONLY_1_1 = 2;
69 
70     /**
71      * 1 snap target: minimized height, (1 - minimized height)
72      */
73     private static final int SNAP_MODE_MINIMIZED = 3;
74 
75     /**
76      * A mode where apps can be "flexibly offscreen" on smaller displays.
77      */
78     private static final int SNAP_FLEXIBLE_SPLIT = 4;
79 
80     private final float mMinFlingVelocityPxPerSecond;
81     private final float mMinDismissVelocityPxPerSecond;
82     private final int mDisplayWidth;
83     private final int mDisplayHeight;
84     private final int mDividerSize;
85     private final ArrayList<SnapTarget> mTargets = new ArrayList<>();
86     private final Rect mInsets = new Rect();
87     private final Rect mPinnedTaskbarInsets = new Rect();
88     private final int mSnapMode;
89     private final boolean mFreeSnapMode;
90     private final int mMinimalSizeResizableTask;
91     private final int mTaskHeightInMinimizedMode;
92     private final float mFixedRatio;
93     /** Allows split ratios to calculated dynamically instead of using {@link #mFixedRatio}. */
94     private final boolean mCalculateRatiosBasedOnAvailableSpace;
95     /** Allows split ratios that go offscreen (a.k.a. "flexible split") */
96     private final boolean mAllowOffscreenRatios;
97     private final boolean mIsLeftRightSplit;
98     /** In SNAP_MODE_MINIMIZED, the side of the screen on which an app will "dock" when minimized */
99     private final int mDockSide;
100 
101     /** The first target which is still splitting the screen */
102     private final SnapTarget mFirstSplitTarget;
103 
104     /** The last target which is still splitting the screen */
105     private final SnapTarget mLastSplitTarget;
106 
107     private final SnapTarget mDismissStartTarget;
108     private final SnapTarget mDismissEndTarget;
109     private final SnapTarget mMiddleTarget;
110 
111 
DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide)112     public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
113             boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide) {
114         this(res, displayWidth, displayHeight, dividerSize, isLeftRightSplit, insets,
115                 pinnedTaskbarInsets, dockSide, false /* minimized */, true /* resizable */);
116     }
117 
DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide, boolean isMinimizedMode, boolean isHomeResizable)118     public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
119             boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide,
120             boolean isMinimizedMode, boolean isHomeResizable) {
121         mMinFlingVelocityPxPerSecond =
122                 MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
123         mMinDismissVelocityPxPerSecond =
124                 MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
125         mDividerSize = dividerSize;
126         mDisplayWidth = displayWidth;
127         mDisplayHeight = displayHeight;
128         mIsLeftRightSplit = isLeftRightSplit;
129         mDockSide = dockSide;
130         mInsets.set(insets);
131         mPinnedTaskbarInsets.set(pinnedTaskbarInsets);
132         if (Flags.enableFlexibleTwoAppSplit()) {
133             mSnapMode = SNAP_FLEXIBLE_SPLIT;
134         } else {
135             // Set SNAP_MODE_MINIMIZED, SNAP_MODE_16_9, or SNAP_FIXED_RATIO depending on config
136             mSnapMode = isMinimizedMode
137                     ? SNAP_MODE_MINIMIZED
138                     : res.getInteger(
139                             com.android.internal.R.integer.config_dockedStackDividerSnapMode);
140         }
141         mFreeSnapMode = res.getBoolean(
142                 com.android.internal.R.bool.config_dockedStackDividerFreeSnapMode);
143         mFixedRatio = res.getFraction(
144                 com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
145         mMinimalSizeResizableTask = res.getDimensionPixelSize(
146                 com.android.internal.R.dimen.default_minimal_size_resizable_task);
147         mCalculateRatiosBasedOnAvailableSpace = res.getBoolean(
148                 com.android.internal.R.bool.config_flexibleSplitRatios);
149         // If this is a small screen or a foldable, use offscreen ratios
150         mAllowOffscreenRatios = SplitScreenUtils.allowOffscreenRatios(res);
151         mTaskHeightInMinimizedMode = isHomeResizable ? res.getDimensionPixelSize(
152                 com.android.internal.R.dimen.task_height_of_minimized_mode) : 0;
153         calculateTargets();
154         mFirstSplitTarget = mTargets.get(1);
155         mLastSplitTarget = mTargets.get(mTargets.size() - 2);
156         mDismissStartTarget = mTargets.get(0);
157         mDismissEndTarget = mTargets.get(mTargets.size() - 1);
158         mMiddleTarget = mTargets.get(mTargets.size() / 2);
159         mMiddleTarget.isMiddleTarget = true;
160     }
161 
162     /**
163      * @param position the top/left position of the divider
164      * @param velocity current dragging velocity
165      * @param hardToDismiss if set, make it a bit harder to get reach the dismiss targets
166      */
calculateSnapTarget(int position, float velocity, boolean hardToDismiss)167     public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardToDismiss) {
168         if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
169             return mDismissStartTarget;
170         }
171         if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
172             return mDismissEndTarget;
173         }
174         if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
175             return snap(position, hardToDismiss);
176         }
177         if (velocity < 0) {
178             return mFirstSplitTarget;
179         } else {
180             return mLastSplitTarget;
181         }
182     }
183 
calculateNonDismissingSnapTarget(int position)184     public SnapTarget calculateNonDismissingSnapTarget(int position) {
185         SnapTarget target = snap(position, false /* hardDismiss */);
186         if (target == mDismissStartTarget) {
187             return mFirstSplitTarget;
188         } else if (target == mDismissEndTarget) {
189             return mLastSplitTarget;
190         } else {
191             return target;
192         }
193     }
194 
195     /**
196      * Gets the SnapTarget corresponding to the given {@link SnapPosition}, or null if no such
197      * SnapTarget exists.
198      */
199     @Nullable
findSnapTarget(@napPosition int snapPosition)200     public SnapTarget findSnapTarget(@SnapPosition int snapPosition) {
201         for (SnapTarget t : mTargets) {
202             if (t.snapPosition == snapPosition) {
203                 return t;
204             }
205         }
206 
207         return null;
208     }
209 
calculateDismissingFraction(int position)210     public float calculateDismissingFraction(int position) {
211         if (position < mFirstSplitTarget.position) {
212             return 1f - (float) (position - getStartInset())
213                     / (mFirstSplitTarget.position - getStartInset());
214         } else if (position > mLastSplitTarget.position) {
215             return (float) (position - mLastSplitTarget.position)
216                     / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize);
217         }
218         return 0f;
219     }
220 
getFirstSplitTarget()221     public SnapTarget getFirstSplitTarget() {
222         return mFirstSplitTarget;
223     }
224 
getLastSplitTarget()225     public SnapTarget getLastSplitTarget() {
226         return mLastSplitTarget;
227     }
228 
getDismissStartTarget()229     public SnapTarget getDismissStartTarget() {
230         return mDismissStartTarget;
231     }
232 
getDismissEndTarget()233     public SnapTarget getDismissEndTarget() {
234         return mDismissEndTarget;
235     }
236 
getStartInset()237     private int getStartInset() {
238         if (mIsLeftRightSplit) {
239             return mInsets.left;
240         } else {
241             return mInsets.top;
242         }
243     }
244 
getEndInset()245     private int getEndInset() {
246         if (mIsLeftRightSplit) {
247             return mInsets.right;
248         } else {
249             return mInsets.bottom;
250         }
251     }
252 
shouldApplyFreeSnapMode(int position)253     private boolean shouldApplyFreeSnapMode(int position) {
254         if (!mFreeSnapMode) {
255             return false;
256         }
257         if (!isFirstSplitTargetAvailable() || !isLastSplitTargetAvailable()) {
258             return false;
259         }
260         return mFirstSplitTarget.position < position && position < mLastSplitTarget.position;
261     }
262 
263     /** Returns if we are currently on a device/screen that supports split apps going offscreen. */
areOffscreenRatiosSupported()264     public boolean areOffscreenRatiosSupported() {
265         return mAllowOffscreenRatios;
266     }
267 
snap(int position, boolean hardDismiss)268     private SnapTarget snap(int position, boolean hardDismiss) {
269         if (shouldApplyFreeSnapMode(position)) {
270             return new SnapTarget(position, SNAP_TO_NONE);
271         }
272         int minIndex = -1;
273         float minDistance = Float.MAX_VALUE;
274         int size = mTargets.size();
275         for (int i = 0; i < size; i++) {
276             SnapTarget target = mTargets.get(i);
277             float distance = Math.abs(position - target.position);
278             if (hardDismiss) {
279                 distance /= target.distanceMultiplier;
280             }
281             if (distance < minDistance) {
282                 minIndex = i;
283                 minDistance = distance;
284             }
285         }
286         return mTargets.get(minIndex);
287     }
288 
calculateTargets()289     private void calculateTargets() {
290         mTargets.clear();
291         int dividerMax = mIsLeftRightSplit
292                 ? mDisplayWidth
293                 : mDisplayHeight;
294         int startPos = -mDividerSize;
295         if (mDockSide == DOCKED_RIGHT) {
296             startPos += mInsets.left;
297         }
298         mTargets.add(new SnapTarget(startPos, SNAP_TO_START_AND_DISMISS, 0.35f));
299         switch (mSnapMode) {
300             case SNAP_MODE_16_9:
301                 addRatio16_9Targets(mIsLeftRightSplit, dividerMax);
302                 break;
303             case SNAP_FIXED_RATIO:
304                 addFixedDivisionTargets(mIsLeftRightSplit, dividerMax);
305                 break;
306             case SNAP_ONLY_1_1:
307                 addMiddleTarget(mIsLeftRightSplit);
308                 break;
309             case SNAP_MODE_MINIMIZED:
310                 addMinimizedTarget(mIsLeftRightSplit, mDockSide);
311                 break;
312             case SNAP_FLEXIBLE_SPLIT:
313                 addFlexSplitTargets(mIsLeftRightSplit, dividerMax);
314                 break;
315         }
316         mTargets.add(new SnapTarget(dividerMax, SNAP_TO_END_AND_DISMISS, 0.35f));
317     }
318 
addNonDismissingTargets(boolean isLeftRightSplit, int topPosition, int bottomPosition, int dividerMax)319     private void addNonDismissingTargets(boolean isLeftRightSplit, int topPosition,
320             int bottomPosition, int dividerMax) {
321         @PersistentSnapPosition int firstTarget =
322                 areOffscreenRatiosSupported() ? SNAP_TO_2_10_90 : SNAP_TO_2_33_66;
323         @PersistentSnapPosition int lastTarget =
324                 areOffscreenRatiosSupported() ? SNAP_TO_2_90_10 : SNAP_TO_2_66_33;
325         maybeAddTarget(topPosition, topPosition - getStartInset(), firstTarget);
326         addMiddleTarget(isLeftRightSplit);
327         maybeAddTarget(bottomPosition,
328                 dividerMax - getEndInset() - (bottomPosition + mDividerSize), lastTarget);
329     }
330 
addFixedDivisionTargets(boolean isLeftRightSplit, int dividerMax)331     private void addFixedDivisionTargets(boolean isLeftRightSplit, int dividerMax) {
332         int start = isLeftRightSplit ? mInsets.left : mInsets.top;
333         int end = isLeftRightSplit
334                 ? mDisplayWidth - mInsets.right
335                 : mDisplayHeight - mInsets.bottom;
336 
337         int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
338         if (mCalculateRatiosBasedOnAvailableSpace) {
339             size = Math.max(size, mMinimalSizeResizableTask);
340         }
341 
342         int topPosition = start + size;
343         int bottomPosition = end - size - mDividerSize;
344         addNonDismissingTargets(isLeftRightSplit, topPosition, bottomPosition, dividerMax);
345     }
346 
addFlexSplitTargets(boolean isLeftRightSplit, int dividerMax)347     private void addFlexSplitTargets(boolean isLeftRightSplit, int dividerMax) {
348         int start = 0;
349         int end = isLeftRightSplit ? mDisplayWidth : mDisplayHeight;
350         int pinnedTaskbarShiftStart = isLeftRightSplit
351                 ? mPinnedTaskbarInsets.left : mPinnedTaskbarInsets.top;
352         int pinnedTaskbarShiftEnd = isLeftRightSplit
353                 ? mPinnedTaskbarInsets.right : mPinnedTaskbarInsets.bottom;
354 
355         float ratio = areOffscreenRatiosSupported()
356                 ? SplitSpec.OFFSCREEN_ASYMMETRIC_RATIO
357                 : SplitSpec.ONSCREEN_ONLY_ASYMMETRIC_RATIO;
358 
359         // The intended size of the smaller app, in pixels
360         int size = (int) (ratio * (end - start)) - mDividerSize / 2;
361 
362         // If there are insets that interfere with the smaller app (visually or blocking touch
363         // targets), make the smaller app bigger by that amount to compensate. This applies to
364         // pinned taskbar, 3-button nav (both create an opaque bar at bottom) and status bar (blocks
365         // touch targets at top).
366         int extraSpace = IntStream.of(
367                 getStartInset(), getEndInset(), pinnedTaskbarShiftStart, pinnedTaskbarShiftEnd
368         ).max().getAsInt();
369 
370         int leftTopPosition = start + extraSpace + size;
371         int rightBottomPosition = end - extraSpace - size - mDividerSize;
372         addNonDismissingTargets(isLeftRightSplit, leftTopPosition, rightBottomPosition, dividerMax);
373     }
374 
addRatio16_9Targets(boolean isLeftRightSplit, int dividerMax)375     private void addRatio16_9Targets(boolean isLeftRightSplit, int dividerMax) {
376         int start = isLeftRightSplit ? mInsets.left : mInsets.top;
377         int end = isLeftRightSplit
378                 ? mDisplayWidth - mInsets.right
379                 : mDisplayHeight - mInsets.bottom;
380         int startOther = isLeftRightSplit ? mInsets.top : mInsets.left;
381         int endOther = isLeftRightSplit
382                 ? mDisplayHeight - mInsets.bottom
383                 : mDisplayWidth - mInsets.right;
384         float size = 9.0f / 16.0f * (endOther - startOther);
385         int sizeInt = (int) Math.floor(size);
386         int topPosition = start + sizeInt;
387         int bottomPosition = end - sizeInt - mDividerSize;
388         addNonDismissingTargets(isLeftRightSplit, topPosition, bottomPosition, dividerMax);
389     }
390 
391     /**
392      * Adds a target at {@param position} but only if the area with size of {@param smallerSize}
393      * meets the minimal size requirement.
394      */
maybeAddTarget(int position, int smallerSize, @SnapPosition int snapPosition)395     private void maybeAddTarget(int position, int smallerSize, @SnapPosition int snapPosition) {
396         if (smallerSize >= mMinimalSizeResizableTask || areOffscreenRatiosSupported()) {
397             mTargets.add(new SnapTarget(position, snapPosition));
398         }
399     }
400 
addMiddleTarget(boolean isLeftRightSplit)401     private void addMiddleTarget(boolean isLeftRightSplit) {
402         int position = DockedDividerUtils.calculateMiddlePosition(isLeftRightSplit,
403                 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
404         mTargets.add(new SnapTarget(position, SNAP_TO_2_50_50));
405     }
406 
addMinimizedTarget(boolean isLeftRightSplit, int dockedSide)407     private void addMinimizedTarget(boolean isLeftRightSplit, int dockedSide) {
408         // In portrait offset the position by the statusbar height, in landscape add the statusbar
409         // height as well to match portrait offset
410         int position = mTaskHeightInMinimizedMode + mInsets.top;
411         if (isLeftRightSplit) {
412             if (dockedSide == DOCKED_LEFT) {
413                 position += mInsets.left;
414             } else if (dockedSide == DOCKED_RIGHT) {
415                 position = mDisplayWidth - position - mInsets.right - mDividerSize;
416             }
417         }
418         mTargets.add(new SnapTarget(position, SNAP_TO_MINIMIZE));
419     }
420 
getMiddleTarget()421     public SnapTarget getMiddleTarget() {
422         return mMiddleTarget;
423     }
424 
425     /**
426      * @return whether or not there are more than 1 split targets that do not include the two
427      * dismiss targets, used in deciding to display the middle target for accessibility
428      */
showMiddleSplitTargetForAccessibility()429     public boolean showMiddleSplitTargetForAccessibility() {
430         return (mTargets.size() - 2) > 1;
431     }
432 
isFirstSplitTargetAvailable()433     public boolean isFirstSplitTargetAvailable() {
434         return mFirstSplitTarget != mMiddleTarget;
435     }
436 
isLastSplitTargetAvailable()437     public boolean isLastSplitTargetAvailable() {
438         return mLastSplitTarget != mMiddleTarget;
439     }
440 
441     /**
442      * Finds the {@link SnapPosition} nearest to the given position.
443      */
calculateNearestSnapPosition(int currentPosition)444     public int calculateNearestSnapPosition(int currentPosition) {
445         return snap(currentPosition, /* hardDismiss */ true).snapPosition;
446     }
447 
448     /**
449      * An object, calculated at boot time, representing a legal position for the split screen
450      * divider (i.e. the divider can be dragged to this spot).
451      */
452     public static class SnapTarget {
453         /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
454         public final int position;
455 
456         /**
457          * An int (enum) describing the placement of the divider in this snap target.
458          */
459         public final @SnapPosition int snapPosition;
460 
461         public boolean isMiddleTarget;
462 
463         /**
464          * Multiplier used to calculate distance to snap position. The lower this value, the harder
465          * it's to snap on this target
466          */
467         private final float distanceMultiplier;
468 
SnapTarget(int position, @SnapPosition int snapPosition)469         public SnapTarget(int position, @SnapPosition int snapPosition) {
470             this(position, snapPosition, 1f);
471         }
472 
SnapTarget(int position, @SnapPosition int snapPosition, float distanceMultiplier)473         public SnapTarget(int position, @SnapPosition int snapPosition,
474                 float distanceMultiplier) {
475             this.position = position;
476             this.snapPosition = snapPosition;
477             this.distanceMultiplier = distanceMultiplier;
478         }
479 
getPosition()480         public int getPosition() {
481             return position;
482         }
483     }
484 }
485