1 /* 2 * Copyright (C) 2020 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.systemui.stackdivider; 18 19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 21 import static android.view.WindowManager.DOCKED_BOTTOM; 22 import static android.view.WindowManager.DOCKED_INVALID; 23 import static android.view.WindowManager.DOCKED_LEFT; 24 import static android.view.WindowManager.DOCKED_RIGHT; 25 import static android.view.WindowManager.DOCKED_TOP; 26 27 import android.annotation.NonNull; 28 import android.content.Context; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.graphics.Rect; 32 import android.util.TypedValue; 33 import android.window.WindowContainerTransaction; 34 35 import com.android.internal.policy.DividerSnapAlgorithm; 36 import com.android.internal.policy.DockedDividerUtils; 37 import com.android.systemui.wm.DisplayLayout; 38 39 /** 40 * Handles split-screen related internal display layout. In general, this represents the 41 * WM-facing understanding of the splits. 42 */ 43 public class SplitDisplayLayout { 44 /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to 45 * restrict IME adjustment so that a min portion of top stack remains visible.*/ 46 private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f; 47 48 private static final int DIVIDER_WIDTH_INACTIVE_DP = 4; 49 50 SplitScreenTaskOrganizer mTiles; 51 DisplayLayout mDisplayLayout; 52 Context mContext; 53 54 // Lazy stuff 55 boolean mResourcesValid = false; 56 int mDividerSize; 57 int mDividerSizeInactive; 58 private DividerSnapAlgorithm mSnapAlgorithm = null; 59 private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null; 60 Rect mPrimary = null; 61 Rect mSecondary = null; 62 Rect mAdjustedPrimary = null; 63 Rect mAdjustedSecondary = null; 64 SplitDisplayLayout(Context ctx, DisplayLayout dl, SplitScreenTaskOrganizer taskTiles)65 public SplitDisplayLayout(Context ctx, DisplayLayout dl, SplitScreenTaskOrganizer taskTiles) { 66 mTiles = taskTiles; 67 mDisplayLayout = dl; 68 mContext = ctx; 69 } 70 rotateTo(int newRotation)71 void rotateTo(int newRotation) { 72 mDisplayLayout.rotateTo(mContext.getResources(), newRotation); 73 final Configuration config = new Configuration(); 74 config.unset(); 75 config.orientation = mDisplayLayout.getOrientation(); 76 Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 77 tmpRect.inset(mDisplayLayout.nonDecorInsets()); 78 config.windowConfiguration.setAppBounds(tmpRect); 79 tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 80 tmpRect.inset(mDisplayLayout.stableInsets()); 81 config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density()); 82 config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density()); 83 mContext = mContext.createConfigurationContext(config); 84 mSnapAlgorithm = null; 85 mMinimizedSnapAlgorithm = null; 86 mResourcesValid = false; 87 } 88 updateResources()89 private void updateResources() { 90 if (mResourcesValid) { 91 return; 92 } 93 mResourcesValid = true; 94 Resources res = mContext.getResources(); 95 mDividerSize = DockedDividerUtils.getDividerSize(res, 96 DockedDividerUtils.getDividerInsets(res)); 97 mDividerSizeInactive = (int) TypedValue.applyDimension( 98 TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics()); 99 } 100 getPrimarySplitSide()101 int getPrimarySplitSide() { 102 switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) { 103 case DisplayLayout.NAV_BAR_BOTTOM: 104 return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP; 105 case DisplayLayout.NAV_BAR_LEFT: 106 return DOCKED_RIGHT; 107 case DisplayLayout.NAV_BAR_RIGHT: 108 return DOCKED_LEFT; 109 default: 110 return DOCKED_INVALID; 111 } 112 } 113 getSnapAlgorithm()114 DividerSnapAlgorithm getSnapAlgorithm() { 115 if (mSnapAlgorithm == null) { 116 updateResources(); 117 boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); 118 mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), 119 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, 120 isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide()); 121 } 122 return mSnapAlgorithm; 123 } 124 getMinimizedSnapAlgorithm(boolean homeStackResizable)125 DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) { 126 if (mMinimizedSnapAlgorithm == null) { 127 updateResources(); 128 boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); 129 mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), 130 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, 131 isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(), 132 true /* isMinimized */, homeStackResizable); 133 } 134 return mMinimizedSnapAlgorithm; 135 } 136 resizeSplits(int position)137 void resizeSplits(int position) { 138 mPrimary = mPrimary == null ? new Rect() : mPrimary; 139 mSecondary = mSecondary == null ? new Rect() : mSecondary; 140 calcSplitBounds(position, mPrimary, mSecondary); 141 } 142 resizeSplits(int position, WindowContainerTransaction t)143 void resizeSplits(int position, WindowContainerTransaction t) { 144 resizeSplits(position); 145 t.setBounds(mTiles.mPrimary.token, mPrimary); 146 t.setBounds(mTiles.mSecondary.token, mSecondary); 147 148 t.setSmallestScreenWidthDp(mTiles.mPrimary.token, 149 getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); 150 t.setSmallestScreenWidthDp(mTiles.mSecondary.token, 151 getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); 152 } 153 calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary)154 void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) { 155 int dockSide = getPrimarySplitSide(); 156 DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary, 157 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); 158 159 DockedDividerUtils.calculateBoundsForPosition(position, 160 DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(), 161 mDisplayLayout.height(), mDividerSize); 162 } 163 calcResizableMinimizedHomeStackBounds()164 Rect calcResizableMinimizedHomeStackBounds() { 165 DividerSnapAlgorithm.SnapTarget miniMid = 166 getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget(); 167 Rect homeBounds = new Rect(); 168 DockedDividerUtils.calculateBoundsForPosition(miniMid.position, 169 DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds, 170 mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); 171 return homeBounds; 172 } 173 174 /** 175 * Updates the adjustment depending on it's current state. 176 */ updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop)177 void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) { 178 adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize, 179 mDividerSizeInactive, mPrimary, mSecondary); 180 } 181 182 /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */ adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds)183 private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, 184 int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) { 185 if (mAdjustedPrimary == null) { 186 mAdjustedPrimary = new Rect(); 187 mAdjustedSecondary = new Rect(); 188 } 189 190 final Rect displayStableRect = new Rect(); 191 dl.getStableBounds(displayStableRect); 192 193 final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop); 194 final int currDividerWidth = 195 (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction)); 196 197 // Calculate the highest we can move the bottom of the top stack to keep 30% visible. 198 final int minTopStackBottom = displayStableRect.top 199 + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN); 200 // Based on that, calculate the maximum amount we'll allow the ime to shift things. 201 final int maxOffset = mPrimary.bottom - minTopStackBottom; 202 // Calculate how much we would shift things without limits (basically the height of ime). 203 final int desiredOffset = hiddenTop - shownTop; 204 // Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints. 205 // We want an effect where the adjustment only occurs during the "highest" portion of the 206 // ime animation. This is done by shifting the adjustment values by the difference in 207 // offsets (effectively playing the whole adjustment animation some fixed amount of pixels 208 // below the ime top). 209 final int topCorrection = Math.max(0, desiredOffset - maxOffset); 210 final int adjustedTop = currImeTop + topCorrection; 211 // The actual yOffset is the distance between adjustedTop and the bottom of the display. 212 // Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only 213 // see adjustment upward. 214 final int yOffset = Math.max(0, dl.height() - adjustedTop); 215 216 // TOP 217 // Reduce the offset by an additional small amount to squish the divider bar. 218 mAdjustedPrimary.set(primaryBounds); 219 mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth)); 220 221 // BOTTOM 222 mAdjustedSecondary.set(secondaryBounds); 223 mAdjustedSecondary.offset(0, -yOffset); 224 } 225 getSmallestWidthDpForBounds(@onNull Context context, DisplayLayout dl, Rect bounds)226 static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl, 227 Rect bounds) { 228 int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(), 229 DockedDividerUtils.getDividerInsets(context.getResources())); 230 231 int minWidth = Integer.MAX_VALUE; 232 233 // Go through all screen orientations and find the orientation in which the task has the 234 // smallest width. 235 Rect tmpRect = new Rect(); 236 Rect rotatedDisplayRect = new Rect(); 237 Rect displayRect = new Rect(0, 0, dl.width(), dl.height()); 238 239 DisplayLayout tmpDL = new DisplayLayout(); 240 for (int rotation = 0; rotation < 4; rotation++) { 241 tmpDL.set(dl); 242 tmpDL.rotateTo(context.getResources(), rotation); 243 DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize); 244 245 tmpRect.set(bounds); 246 DisplayLayout.rotateBounds(tmpRect, displayRect, rotation - dl.rotation()); 247 rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height()); 248 final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect, 249 tmpDL.getOrientation()); 250 final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide, 251 dividerSize); 252 253 final int snappedPosition = 254 snap.calculateNonDismissingSnapTarget(position).position; 255 DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect, 256 tmpDL.width(), tmpDL.height(), dividerSize); 257 Rect insettedDisplay = new Rect(rotatedDisplayRect); 258 insettedDisplay.inset(tmpDL.stableInsets()); 259 tmpRect.intersect(insettedDisplay); 260 minWidth = Math.min(tmpRect.width(), minWidth); 261 } 262 return (int) (minWidth / dl.density()); 263 } 264 initSnapAlgorithmForRotation(Context context, DisplayLayout dl, int dividerSize)265 static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl, 266 int dividerSize) { 267 final Configuration config = new Configuration(); 268 config.unset(); 269 config.orientation = dl.getOrientation(); 270 Rect tmpRect = new Rect(0, 0, dl.width(), dl.height()); 271 tmpRect.inset(dl.nonDecorInsets()); 272 config.windowConfiguration.setAppBounds(tmpRect); 273 tmpRect.set(0, 0, dl.width(), dl.height()); 274 tmpRect.inset(dl.stableInsets()); 275 config.screenWidthDp = (int) (tmpRect.width() / dl.density()); 276 config.screenHeightDp = (int) (tmpRect.height() / dl.density()); 277 final Context rotationContext = context.createConfigurationContext(config); 278 return new DividerSnapAlgorithm( 279 rotationContext.getResources(), dl.width(), dl.height(), dividerSize, 280 config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets()); 281 } 282 283 /** 284 * Get the current primary-split side. Determined by its location of {@param bounds} within 285 * {@param displayRect} but if both are the same, it will try to dock to each side and determine 286 * if allowed in its respected {@param orientation}. 287 * 288 * @param bounds bounds of the primary split task to get which side is docked 289 * @param displayRect bounds of the display that contains the primary split task 290 * @param orientation the origination of device 291 * @return current primary-split side 292 */ getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation)293 static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) { 294 if (orientation == ORIENTATION_PORTRAIT) { 295 // Portrait mode, docked either at the top or the bottom. 296 final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top); 297 if (diff < 0) { 298 return DOCKED_BOTTOM; 299 } else { 300 // Top is default 301 return DOCKED_TOP; 302 } 303 } else if (orientation == ORIENTATION_LANDSCAPE) { 304 // Landscape mode, docked either on the left or on the right. 305 final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left); 306 if (diff < 0) { 307 return DOCKED_RIGHT; 308 } 309 return DOCKED_LEFT; 310 } 311 return DOCKED_INVALID; 312 } 313 } 314