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