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    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
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
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     */
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
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     */
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
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
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
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
194    public SnapTarget getFirstSplitTarget() {
195        return mFirstSplitTarget;
196    }
197
198    public SnapTarget getLastSplitTarget() {
199        return mLastSplitTarget;
200    }
201
202    public SnapTarget getDismissStartTarget() {
203        return mDismissStartTarget;
204    }
205
206    public SnapTarget getDismissEndTarget() {
207        return mDismissEndTarget;
208    }
209
210    private int getStartInset() {
211        if (mIsHorizontalDivision) {
212            return mInsets.top;
213        } else {
214            return mInsets.left;
215        }
216    }
217
218    private int getEndInset() {
219        if (mIsHorizontalDivision) {
220            return mInsets.bottom;
221        } else {
222            return mInsets.right;
223        }
224    }
225
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
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
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
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
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     */
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
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
318    public SnapTarget getMiddleTarget() {
319        return mMiddleTarget;
320    }
321
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
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
338    public boolean isFirstSplitTargetAvailable() {
339        return mFirstSplitTarget != mMiddleTarget;
340    }
341
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     */
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
395        public SnapTarget(int position, int taskPosition, int flag) {
396            this(position, taskPosition, flag, 1f);
397        }
398
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