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