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