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     }
141 
142     /**
143      * @return whether it's feasible to enable split screen in the current configuration, i.e. when
144      *         snapping in the middle both tasks are larger than the minimal task size.
145      */
isSplitScreenFeasible()146     public boolean isSplitScreenFeasible() {
147         int statusBarSize = mInsets.top;
148         int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right;
149         int size = mIsHorizontalDivision
150                 ? mDisplayHeight
151                 : mDisplayWidth;
152         int availableSpace = size - navBarSize - statusBarSize - mDividerSize;
153         return availableSpace / 2 >= mMinimalSizeResizableTask;
154     }
155 
calculateSnapTarget(int position, float velocity)156     public SnapTarget calculateSnapTarget(int position, float velocity) {
157         return calculateSnapTarget(position, velocity, true /* hardDismiss */);
158     }
159 
160     /**
161      * @param position the top/left position of the divider
162      * @param velocity current dragging velocity
163      * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
164      */
calculateSnapTarget(int position, float velocity, boolean hardDismiss)165     public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
166         if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
167             return mDismissStartTarget;
168         }
169         if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
170             return mDismissEndTarget;
171         }
172         if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
173             return snap(position, hardDismiss);
174         }
175         if (velocity < 0) {
176             return mFirstSplitTarget;
177         } else {
178             return mLastSplitTarget;
179         }
180     }
181 
calculateNonDismissingSnapTarget(int position)182     public SnapTarget calculateNonDismissingSnapTarget(int position) {
183         SnapTarget target = snap(position, false /* hardDismiss */);
184         if (target == mDismissStartTarget) {
185             return mFirstSplitTarget;
186         } else if (target == mDismissEndTarget) {
187             return mLastSplitTarget;
188         } else {
189             return target;
190         }
191     }
192 
calculateDismissingFraction(int position)193     public float calculateDismissingFraction(int position) {
194         if (position < mFirstSplitTarget.position) {
195             return 1f - (float) (position - getStartInset())
196                     / (mFirstSplitTarget.position - getStartInset());
197         } else if (position > mLastSplitTarget.position) {
198             return (float) (position - mLastSplitTarget.position)
199                     / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize);
200         }
201         return 0f;
202     }
203 
getClosestDismissTarget(int position)204     public SnapTarget getClosestDismissTarget(int position) {
205         if (position < mFirstSplitTarget.position) {
206             return mDismissStartTarget;
207         } else if (position > mLastSplitTarget.position) {
208             return mDismissEndTarget;
209         } else if (position - mDismissStartTarget.position
210                 < mDismissEndTarget.position - position) {
211             return mDismissStartTarget;
212         } else {
213             return mDismissEndTarget;
214         }
215     }
216 
getFirstSplitTarget()217     public SnapTarget getFirstSplitTarget() {
218         return mFirstSplitTarget;
219     }
220 
getLastSplitTarget()221     public SnapTarget getLastSplitTarget() {
222         return mLastSplitTarget;
223     }
224 
getDismissStartTarget()225     public SnapTarget getDismissStartTarget() {
226         return mDismissStartTarget;
227     }
228 
getDismissEndTarget()229     public SnapTarget getDismissEndTarget() {
230         return mDismissEndTarget;
231     }
232 
getStartInset()233     private int getStartInset() {
234         if (mIsHorizontalDivision) {
235             return mInsets.top;
236         } else {
237             return mInsets.left;
238         }
239     }
240 
getEndInset()241     private int getEndInset() {
242         if (mIsHorizontalDivision) {
243             return mInsets.bottom;
244         } else {
245             return mInsets.right;
246         }
247     }
248 
snap(int position, boolean hardDismiss)249     private SnapTarget snap(int position, boolean hardDismiss) {
250         int minIndex = -1;
251         float minDistance = Float.MAX_VALUE;
252         int size = mTargets.size();
253         for (int i = 0; i < size; i++) {
254             SnapTarget target = mTargets.get(i);
255             float distance = Math.abs(position - target.position);
256             if (hardDismiss) {
257                 distance /= target.distanceMultiplier;
258             }
259             if (distance < minDistance) {
260                 minIndex = i;
261                 minDistance = distance;
262             }
263         }
264         return mTargets.get(minIndex);
265     }
266 
calculateTargets(boolean isHorizontalDivision, int dockedSide)267     private void calculateTargets(boolean isHorizontalDivision, int dockedSide) {
268         mTargets.clear();
269         int dividerMax = isHorizontalDivision
270                 ? mDisplayHeight
271                 : mDisplayWidth;
272         int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
273         int startPos = -mDividerSize;
274         if (dockedSide == DOCKED_RIGHT) {
275             startPos += mInsets.left;
276         }
277         mTargets.add(new SnapTarget(startPos, startPos, SnapTarget.FLAG_DISMISS_START,
278                 0.35f));
279         switch (mSnapMode) {
280             case SNAP_MODE_16_9:
281                 addRatio16_9Targets(isHorizontalDivision, dividerMax);
282                 break;
283             case SNAP_FIXED_RATIO:
284                 addFixedDivisionTargets(isHorizontalDivision, dividerMax);
285                 break;
286             case SNAP_ONLY_1_1:
287                 addMiddleTarget(isHorizontalDivision);
288                 break;
289             case SNAP_MODE_MINIMIZED:
290                 addMinimizedTarget(isHorizontalDivision, dockedSide);
291                 break;
292         }
293         mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
294                 SnapTarget.FLAG_DISMISS_END, 0.35f));
295     }
296 
addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, int bottomPosition, int dividerMax)297     private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
298             int bottomPosition, int dividerMax) {
299         maybeAddTarget(topPosition, topPosition - mInsets.top);
300         addMiddleTarget(isHorizontalDivision);
301         maybeAddTarget(bottomPosition, dividerMax - mInsets.bottom
302                 - (bottomPosition + mDividerSize));
303     }
304 
addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax)305     private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
306         int start = isHorizontalDivision ? mInsets.top : mInsets.left;
307         int end = isHorizontalDivision
308                 ? mDisplayHeight - mInsets.bottom
309                 : mDisplayWidth - mInsets.right;
310         int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
311         int topPosition = start + size;
312         int bottomPosition = end - size - mDividerSize;
313         addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
314     }
315 
addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax)316     private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) {
317         int start = isHorizontalDivision ? mInsets.top : mInsets.left;
318         int end = isHorizontalDivision
319                 ? mDisplayHeight - mInsets.bottom
320                 : mDisplayWidth - mInsets.right;
321         int startOther = isHorizontalDivision ? mInsets.left : mInsets.top;
322         int endOther = isHorizontalDivision
323                 ? mDisplayWidth - mInsets.right
324                 : mDisplayHeight - mInsets.bottom;
325         float size = 9.0f / 16.0f * (endOther - startOther);
326         int sizeInt = (int) Math.floor(size);
327         int topPosition = start + sizeInt;
328         int bottomPosition = end - sizeInt - mDividerSize;
329         addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
330     }
331 
332     /**
333      * Adds a target at {@param position} but only if the area with size of {@param smallerSize}
334      * meets the minimal size requirement.
335      */
maybeAddTarget(int position, int smallerSize)336     private void maybeAddTarget(int position, int smallerSize) {
337         if (smallerSize >= mMinimalSizeResizableTask) {
338             mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
339         }
340     }
341 
addMiddleTarget(boolean isHorizontalDivision)342     private void addMiddleTarget(boolean isHorizontalDivision) {
343         int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
344                 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
345         mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
346     }
347 
addMinimizedTarget(boolean isHorizontalDivision, int dockedSide)348     private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
349         // In portrait offset the position by the statusbar height, in landscape add the statusbar
350         // height as well to match portrait offset
351         int position = mTaskHeightInMinimizedMode + mInsets.top;
352         if (!isHorizontalDivision) {
353             if (dockedSide == DOCKED_LEFT) {
354                 position += mInsets.left;
355             } else if (dockedSide == DOCKED_RIGHT) {
356                 position = mDisplayWidth - position - mInsets.right - mDividerSize;
357             }
358         }
359         mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
360     }
361 
getMiddleTarget()362     public SnapTarget getMiddleTarget() {
363         return mMiddleTarget;
364     }
365 
getNextTarget(SnapTarget snapTarget)366     public SnapTarget getNextTarget(SnapTarget snapTarget) {
367         int index = mTargets.indexOf(snapTarget);
368         if (index != -1 && index < mTargets.size() - 1) {
369             return mTargets.get(index + 1);
370         }
371         return snapTarget;
372     }
373 
getPreviousTarget(SnapTarget snapTarget)374     public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
375         int index = mTargets.indexOf(snapTarget);
376         if (index != -1 && index > 0) {
377             return mTargets.get(index - 1);
378         }
379         return snapTarget;
380     }
381 
382     /**
383      * @return whether or not there are more than 1 split targets that do not include the two
384      * dismiss targets, used in deciding to display the middle target for accessibility
385      */
showMiddleSplitTargetForAccessibility()386     public boolean showMiddleSplitTargetForAccessibility() {
387         return (mTargets.size() - 2) > 1;
388     }
389 
isFirstSplitTargetAvailable()390     public boolean isFirstSplitTargetAvailable() {
391         return mFirstSplitTarget != mMiddleTarget;
392     }
393 
isLastSplitTargetAvailable()394     public boolean isLastSplitTargetAvailable() {
395         return mLastSplitTarget != mMiddleTarget;
396     }
397 
398     /**
399      * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
400      * if {@param increment} is negative and moves right otherwise.
401      */
cycleNonDismissTarget(SnapTarget snapTarget, int increment)402     public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
403         int index = mTargets.indexOf(snapTarget);
404         if (index != -1) {
405             SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
406                     % mTargets.size());
407             if (newTarget == mDismissStartTarget) {
408                 return mLastSplitTarget;
409             } else if (newTarget == mDismissEndTarget) {
410                 return mFirstSplitTarget;
411             } else {
412                 return newTarget;
413             }
414         }
415         return snapTarget;
416     }
417 
418     /**
419      * Represents a snap target for the divider.
420      */
421     public static class SnapTarget {
422         public static final int FLAG_NONE = 0;
423 
424         /** If the divider reaches this value, the left/top task should be dismissed. */
425         public static final int FLAG_DISMISS_START = 1;
426 
427         /** If the divider reaches this value, the right/bottom task should be dismissed */
428         public static final int FLAG_DISMISS_END = 2;
429 
430         /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
431         public final int position;
432 
433         /**
434          * Like {@link #position}, but used to calculate the task bounds which might be different
435          * from the stack bounds.
436          */
437         public final int taskPosition;
438 
439         public final int flag;
440 
441         /**
442          * Multiplier used to calculate distance to snap position. The lower this value, the harder
443          * it's to snap on this target
444          */
445         private final float distanceMultiplier;
446 
SnapTarget(int position, int taskPosition, int flag)447         public SnapTarget(int position, int taskPosition, int flag) {
448             this(position, taskPosition, flag, 1f);
449         }
450 
SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier)451         public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
452             this.position = position;
453             this.taskPosition = taskPosition;
454             this.flag = flag;
455             this.distanceMultiplier = distanceMultiplier;
456         }
457     }
458 }
459