1 /*
2  * Copyright (C) 2015 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.internal.policy;
18 
19 import static android.view.WindowManager.DOCKED_INVALID;
20 import static android.view.WindowManager.DOCKED_LEFT;
21 import static android.view.WindowManager.DOCKED_RIGHT;
22 
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.graphics.Rect;
27 import android.hardware.display.DisplayManager;
28 import android.view.Display;
29 import android.view.DisplayInfo;
30 
31 import java.util.ArrayList;
32 
33 /**
34  * Calculates the snap targets and the snap position given a position and a velocity. All positions
35  * here are to be interpreted as the left/top edge of the divider rectangle.
36  *
37  * @hide
38  */
39 public class DividerSnapAlgorithm {
40 
41     private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
42     private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
43 
44     /**
45      * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio
46      */
47     private static final int SNAP_MODE_16_9 = 0;
48 
49     /**
50      * 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)
51      */
52     private static final int SNAP_FIXED_RATIO = 1;
53 
54     /**
55      * 1 snap target: 1:1
56      */
57     private static final int SNAP_ONLY_1_1 = 2;
58 
59     /**
60      * 1 snap target: minimized height, (1 - minimized height)
61      */
62     private static final int SNAP_MODE_MINIMIZED = 3;
63 
64     private final float mMinFlingVelocityPxPerSecond;
65     private final float mMinDismissVelocityPxPerSecond;
66     private final int mDisplayWidth;
67     private final int mDisplayHeight;
68     private final int mDividerSize;
69     private final ArrayList<SnapTarget> mTargets = new ArrayList<>();
70     private final Rect mInsets = new Rect();
71     private final int mSnapMode;
72     private final int mMinimalSizeResizableTask;
73     private final int mTaskHeightInMinimizedMode;
74     private final float mFixedRatio;
75     private boolean mIsHorizontalDivision;
76 
77     /** The first target which is still splitting the screen */
78     private final SnapTarget mFirstSplitTarget;
79 
80     /** The last target which is still splitting the screen */
81     private final SnapTarget mLastSplitTarget;
82 
83     private final SnapTarget mDismissStartTarget;
84     private final SnapTarget mDismissEndTarget;
85     private final SnapTarget mMiddleTarget;
86 
create(Context ctx, Rect insets)87     public static DividerSnapAlgorithm create(Context ctx, Rect insets) {
88         DisplayInfo displayInfo = new DisplayInfo();
89         ctx.getSystemService(DisplayManager.class).getDisplay(
90                 Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo);
91         int dividerWindowWidth = ctx.getResources().getDimensionPixelSize(
92                 com.android.internal.R.dimen.docked_stack_divider_thickness);
93         int dividerInsets = ctx.getResources().getDimensionPixelSize(
94                 com.android.internal.R.dimen.docked_stack_divider_insets);
95         return new DividerSnapAlgorithm(ctx.getResources(),
96                 displayInfo.logicalWidth, displayInfo.logicalHeight,
97                 dividerWindowWidth - 2 * dividerInsets,
98                 ctx.getApplicationContext().getResources().getConfiguration().orientation
99                         == Configuration.ORIENTATION_PORTRAIT,
100                 insets);
101     }
102 
DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets)103     public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
104             boolean isHorizontalDivision, Rect insets) {
105         this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
106                 DOCKED_INVALID, false);
107     }
108 
DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets, int dockSide)109     public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
110         boolean isHorizontalDivision, Rect insets, int dockSide) {
111         this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
112             dockSide, false);
113     }
114 
DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode)115     public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
116             boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode) {
117         mMinFlingVelocityPxPerSecond =
118                 MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
119         mMinDismissVelocityPxPerSecond =
120                 MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
121         mDividerSize = dividerSize;
122         mDisplayWidth = displayWidth;
123         mDisplayHeight = displayHeight;
124         mIsHorizontalDivision = isHorizontalDivision;
125         mInsets.set(insets);
126         mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED :
127                 res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode);
128         mFixedRatio = res.getFraction(
129                 com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
130         mMinimalSizeResizableTask = res.getDimensionPixelSize(
131                 com.android.internal.R.dimen.default_minimal_size_resizable_task);
132         mTaskHeightInMinimizedMode = res.getDimensionPixelSize(
133                 com.android.internal.R.dimen.task_height_of_minimized_mode);
134         calculateTargets(isHorizontalDivision, dockSide);
135         mFirstSplitTarget = mTargets.get(1);
136         mLastSplitTarget = mTargets.get(mTargets.size() - 2);
137         mDismissStartTarget = mTargets.get(0);
138         mDismissEndTarget = mTargets.get(mTargets.size() - 1);
139         mMiddleTarget = mTargets.get(mTargets.size() / 2);
140         mMiddleTarget.isMiddleTarget = true;
141     }
142 
143     /**
144      * @return whether it's feasible to enable split screen in the current configuration, i.e. when
145      *         snapping in the middle both tasks are larger than the minimal task size.
146      */
isSplitScreenFeasible()147     public boolean isSplitScreenFeasible() {
148         int statusBarSize = mInsets.top;
149         int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right;
150         int size = mIsHorizontalDivision
151                 ? mDisplayHeight
152                 : mDisplayWidth;
153         int availableSpace = size - navBarSize - statusBarSize - mDividerSize;
154         return availableSpace / 2 >= mMinimalSizeResizableTask;
155     }
156 
calculateSnapTarget(int position, float velocity)157     public SnapTarget calculateSnapTarget(int position, float velocity) {
158         return calculateSnapTarget(position, velocity, true /* hardDismiss */);
159     }
160 
161     /**
162      * @param position the top/left position of the divider
163      * @param velocity current dragging velocity
164      * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
165      */
calculateSnapTarget(int position, float velocity, boolean hardDismiss)166     public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
167         if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
168             return mDismissStartTarget;
169         }
170         if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
171             return mDismissEndTarget;
172         }
173         if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
174             return snap(position, hardDismiss);
175         }
176         if (velocity < 0) {
177             return mFirstSplitTarget;
178         } else {
179             return mLastSplitTarget;
180         }
181     }
182 
calculateNonDismissingSnapTarget(int position)183     public SnapTarget calculateNonDismissingSnapTarget(int position) {
184         SnapTarget target = snap(position, false /* hardDismiss */);
185         if (target == mDismissStartTarget) {
186             return mFirstSplitTarget;
187         } else if (target == mDismissEndTarget) {
188             return mLastSplitTarget;
189         } else {
190             return target;
191         }
192     }
193 
calculateDismissingFraction(int position)194     public float calculateDismissingFraction(int position) {
195         if (position < mFirstSplitTarget.position) {
196             return 1f - (float) (position - getStartInset())
197                     / (mFirstSplitTarget.position - getStartInset());
198         } else if (position > mLastSplitTarget.position) {
199             return (float) (position - mLastSplitTarget.position)
200                     / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize);
201         }
202         return 0f;
203     }
204 
getClosestDismissTarget(int position)205     public SnapTarget getClosestDismissTarget(int position) {
206         if (position < mFirstSplitTarget.position) {
207             return mDismissStartTarget;
208         } else if (position > mLastSplitTarget.position) {
209             return mDismissEndTarget;
210         } else if (position - mDismissStartTarget.position
211                 < mDismissEndTarget.position - position) {
212             return mDismissStartTarget;
213         } else {
214             return mDismissEndTarget;
215         }
216     }
217 
getFirstSplitTarget()218     public SnapTarget getFirstSplitTarget() {
219         return mFirstSplitTarget;
220     }
221 
getLastSplitTarget()222     public SnapTarget getLastSplitTarget() {
223         return mLastSplitTarget;
224     }
225 
getDismissStartTarget()226     public SnapTarget getDismissStartTarget() {
227         return mDismissStartTarget;
228     }
229 
getDismissEndTarget()230     public SnapTarget getDismissEndTarget() {
231         return mDismissEndTarget;
232     }
233 
getStartInset()234     private int getStartInset() {
235         if (mIsHorizontalDivision) {
236             return mInsets.top;
237         } else {
238             return mInsets.left;
239         }
240     }
241 
getEndInset()242     private int getEndInset() {
243         if (mIsHorizontalDivision) {
244             return mInsets.bottom;
245         } else {
246             return mInsets.right;
247         }
248     }
249 
snap(int position, boolean hardDismiss)250     private SnapTarget snap(int position, boolean hardDismiss) {
251         int minIndex = -1;
252         float minDistance = Float.MAX_VALUE;
253         int size = mTargets.size();
254         for (int i = 0; i < size; i++) {
255             SnapTarget target = mTargets.get(i);
256             float distance = Math.abs(position - target.position);
257             if (hardDismiss) {
258                 distance /= target.distanceMultiplier;
259             }
260             if (distance < minDistance) {
261                 minIndex = i;
262                 minDistance = distance;
263             }
264         }
265         return mTargets.get(minIndex);
266     }
267 
calculateTargets(boolean isHorizontalDivision, int dockedSide)268     private void calculateTargets(boolean isHorizontalDivision, int dockedSide) {
269         mTargets.clear();
270         int dividerMax = isHorizontalDivision
271                 ? mDisplayHeight
272                 : mDisplayWidth;
273         int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
274         int startPos = -mDividerSize;
275         if (dockedSide == DOCKED_RIGHT) {
276             startPos += mInsets.left;
277         }
278         mTargets.add(new SnapTarget(startPos, startPos, SnapTarget.FLAG_DISMISS_START,
279                 0.35f));
280         switch (mSnapMode) {
281             case SNAP_MODE_16_9:
282                 addRatio16_9Targets(isHorizontalDivision, dividerMax);
283                 break;
284             case SNAP_FIXED_RATIO:
285                 addFixedDivisionTargets(isHorizontalDivision, dividerMax);
286                 break;
287             case SNAP_ONLY_1_1:
288                 addMiddleTarget(isHorizontalDivision);
289                 break;
290             case SNAP_MODE_MINIMIZED:
291                 addMinimizedTarget(isHorizontalDivision, dockedSide);
292                 break;
293         }
294         mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
295                 SnapTarget.FLAG_DISMISS_END, 0.35f));
296     }
297 
addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, int bottomPosition, int dividerMax)298     private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
299             int bottomPosition, int dividerMax) {
300         maybeAddTarget(topPosition, topPosition - mInsets.top);
301         addMiddleTarget(isHorizontalDivision);
302         maybeAddTarget(bottomPosition, dividerMax - mInsets.bottom
303                 - (bottomPosition + mDividerSize));
304     }
305 
addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax)306     private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
307         int start = isHorizontalDivision ? mInsets.top : mInsets.left;
308         int end = isHorizontalDivision
309                 ? mDisplayHeight - mInsets.bottom
310                 : mDisplayWidth - mInsets.right;
311         int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
312         int topPosition = start + size;
313         int bottomPosition = end - size - mDividerSize;
314         addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
315     }
316 
addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax)317     private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) {
318         int start = isHorizontalDivision ? mInsets.top : mInsets.left;
319         int end = isHorizontalDivision
320                 ? mDisplayHeight - mInsets.bottom
321                 : mDisplayWidth - mInsets.right;
322         int startOther = isHorizontalDivision ? mInsets.left : mInsets.top;
323         int endOther = isHorizontalDivision
324                 ? mDisplayWidth - mInsets.right
325                 : mDisplayHeight - mInsets.bottom;
326         float size = 9.0f / 16.0f * (endOther - startOther);
327         int sizeInt = (int) Math.floor(size);
328         int topPosition = start + sizeInt;
329         int bottomPosition = end - sizeInt - mDividerSize;
330         addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
331     }
332 
333     /**
334      * Adds a target at {@param position} but only if the area with size of {@param smallerSize}
335      * meets the minimal size requirement.
336      */
maybeAddTarget(int position, int smallerSize)337     private void maybeAddTarget(int position, int smallerSize) {
338         if (smallerSize >= mMinimalSizeResizableTask) {
339             mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
340         }
341     }
342 
addMiddleTarget(boolean isHorizontalDivision)343     private void addMiddleTarget(boolean isHorizontalDivision) {
344         int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
345                 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
346         mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
347     }
348 
addMinimizedTarget(boolean isHorizontalDivision, int dockedSide)349     private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
350         // In portrait offset the position by the statusbar height, in landscape add the statusbar
351         // height as well to match portrait offset
352         int position = mTaskHeightInMinimizedMode + mInsets.top;
353         if (!isHorizontalDivision) {
354             if (dockedSide == DOCKED_LEFT) {
355                 position += mInsets.left;
356             } else if (dockedSide == DOCKED_RIGHT) {
357                 position = mDisplayWidth - position - mInsets.right - mDividerSize;
358             }
359         }
360         mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
361     }
362 
getMiddleTarget()363     public SnapTarget getMiddleTarget() {
364         return mMiddleTarget;
365     }
366 
getNextTarget(SnapTarget snapTarget)367     public SnapTarget getNextTarget(SnapTarget snapTarget) {
368         int index = mTargets.indexOf(snapTarget);
369         if (index != -1 && index < mTargets.size() - 1) {
370             return mTargets.get(index + 1);
371         }
372         return snapTarget;
373     }
374 
getPreviousTarget(SnapTarget snapTarget)375     public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
376         int index = mTargets.indexOf(snapTarget);
377         if (index != -1 && index > 0) {
378             return mTargets.get(index - 1);
379         }
380         return snapTarget;
381     }
382 
383     /**
384      * @return whether or not there are more than 1 split targets that do not include the two
385      * dismiss targets, used in deciding to display the middle target for accessibility
386      */
showMiddleSplitTargetForAccessibility()387     public boolean showMiddleSplitTargetForAccessibility() {
388         return (mTargets.size() - 2) > 1;
389     }
390 
isFirstSplitTargetAvailable()391     public boolean isFirstSplitTargetAvailable() {
392         return mFirstSplitTarget != mMiddleTarget;
393     }
394 
isLastSplitTargetAvailable()395     public boolean isLastSplitTargetAvailable() {
396         return mLastSplitTarget != mMiddleTarget;
397     }
398 
399     /**
400      * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
401      * if {@param increment} is negative and moves right otherwise.
402      */
cycleNonDismissTarget(SnapTarget snapTarget, int increment)403     public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
404         int index = mTargets.indexOf(snapTarget);
405         if (index != -1) {
406             SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
407                     % mTargets.size());
408             if (newTarget == mDismissStartTarget) {
409                 return mLastSplitTarget;
410             } else if (newTarget == mDismissEndTarget) {
411                 return mFirstSplitTarget;
412             } else {
413                 return newTarget;
414             }
415         }
416         return snapTarget;
417     }
418 
419     /**
420      * Represents a snap target for the divider.
421      */
422     public static class SnapTarget {
423         public static final int FLAG_NONE = 0;
424 
425         /** If the divider reaches this value, the left/top task should be dismissed. */
426         public static final int FLAG_DISMISS_START = 1;
427 
428         /** If the divider reaches this value, the right/bottom task should be dismissed */
429         public static final int FLAG_DISMISS_END = 2;
430 
431         /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
432         public final int position;
433 
434         /**
435          * Like {@link #position}, but used to calculate the task bounds which might be different
436          * from the stack bounds.
437          */
438         public final int taskPosition;
439 
440         public final int flag;
441 
442         public boolean isMiddleTarget;
443 
444         /**
445          * Multiplier used to calculate distance to snap position. The lower this value, the harder
446          * it's to snap on this target
447          */
448         private final float distanceMultiplier;
449 
SnapTarget(int position, int taskPosition, int flag)450         public SnapTarget(int position, int taskPosition, int flag) {
451             this(position, taskPosition, flag, 1f);
452         }
453 
SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier)454         public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
455             this.position = position;
456             this.taskPosition = taskPosition;
457             this.flag = flag;
458             this.distanceMultiplier = distanceMultiplier;
459         }
460     }
461 }
462