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