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 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     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