DividerSnapAlgorithm.java revision e15352e516fb6ecde12866f0eb27c32470ddbded
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
17package com.android.internal.policy;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.content.res.Resources;
22import android.graphics.Rect;
23import android.hardware.display.DisplayManager;
24import android.util.Log;
25import android.view.Display;
26import android.view.DisplayInfo;
27
28import 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 */
36public 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
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
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
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     */
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
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     */
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
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
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
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
207    public SnapTarget getFirstSplitTarget() {
208        return mFirstSplitTarget;
209    }
210
211    public SnapTarget getLastSplitTarget() {
212        return mLastSplitTarget;
213    }
214
215    public SnapTarget getDismissStartTarget() {
216        return mDismissStartTarget;
217    }
218
219    public SnapTarget getDismissEndTarget() {
220        return mDismissEndTarget;
221    }
222
223    private int getStartInset() {
224        if (mIsHorizontalDivision) {
225            return mInsets.top;
226        } else {
227            return mInsets.left;
228        }
229    }
230
231    private int getEndInset() {
232        if (mIsHorizontalDivision) {
233            return mInsets.bottom;
234        } else {
235            return mInsets.right;
236        }
237    }
238
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
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
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
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
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     */
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
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
334    private void addMinimizedTarget(boolean isHorizontalDivision) {
335        int position = mTaskHeightInMinimizedMode;
336        position += isHorizontalDivision ? mInsets.top : mInsets.left;
337        mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
338    }
339
340    public SnapTarget getMiddleTarget() {
341        return mMiddleTarget;
342    }
343
344    public SnapTarget getNextTarget(SnapTarget snapTarget) {
345        int index = mTargets.indexOf(snapTarget);
346        if (index != -1 && index < mTargets.size() - 1) {
347            return mTargets.get(index + 1);
348        }
349        return snapTarget;
350    }
351
352    public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
353        int index = mTargets.indexOf(snapTarget);
354        if (index != -1 && index > 0) {
355            return mTargets.get(index - 1);
356        }
357        return snapTarget;
358    }
359
360    public boolean isFirstSplitTargetAvailable() {
361        return mFirstSplitTarget != mMiddleTarget;
362    }
363
364    public boolean isLastSplitTargetAvailable() {
365        return mLastSplitTarget != mMiddleTarget;
366    }
367
368    /**
369     * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
370     * if {@param increment} is negative and moves right otherwise.
371     */
372    public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
373        int index = mTargets.indexOf(snapTarget);
374        if (index != -1) {
375            SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
376                    % mTargets.size());
377            if (newTarget == mDismissStartTarget) {
378                return mLastSplitTarget;
379            } else if (newTarget == mDismissEndTarget) {
380                return mFirstSplitTarget;
381            } else {
382                return newTarget;
383            }
384        }
385        return snapTarget;
386    }
387
388    /**
389     * Represents a snap target for the divider.
390     */
391    public static class SnapTarget {
392        public static final int FLAG_NONE = 0;
393
394        /** If the divider reaches this value, the left/top task should be dismissed. */
395        public static final int FLAG_DISMISS_START = 1;
396
397        /** If the divider reaches this value, the right/bottom task should be dismissed */
398        public static final int FLAG_DISMISS_END = 2;
399
400        /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
401        public final int position;
402
403        /**
404         * Like {@link #position}, but used to calculate the task bounds which might be different
405         * from the stack bounds.
406         */
407        public final int taskPosition;
408
409        public final int flag;
410
411        /**
412         * Multiplier used to calculate distance to snap position. The lower this value, the harder
413         * it's to snap on this target
414         */
415        private final float distanceMultiplier;
416
417        public SnapTarget(int position, int taskPosition, int flag) {
418            this(position, taskPosition, flag, 1f);
419        }
420
421        public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
422            this.position = position;
423            this.taskPosition = taskPosition;
424            this.flag = flag;
425            this.distanceMultiplier = distanceMultiplier;
426        }
427    }
428}
429