1 /* 2 * Copyright (C) 2014 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.recents.views; 18 19 import android.annotation.IntDef; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.graphics.Path; 24 import android.graphics.Rect; 25 import android.util.ArraySet; 26 import android.util.MutableFloat; 27 import android.util.SparseArray; 28 import android.util.SparseIntArray; 29 import android.view.ViewDebug; 30 31 import com.android.systemui.R; 32 import com.android.systemui.recents.Recents; 33 import com.android.systemui.recents.RecentsActivityLaunchState; 34 import com.android.systemui.recents.RecentsConfiguration; 35 import com.android.systemui.recents.RecentsDebugFlags; 36 import com.android.systemui.recents.misc.FreePathInterpolator; 37 import com.android.systemui.recents.misc.SystemServicesProxy; 38 import com.android.systemui.recents.misc.Utilities; 39 import com.android.systemui.recents.model.Task; 40 import com.android.systemui.recents.model.TaskStack; 41 42 import java.io.PrintWriter; 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.ArrayList; 46 import java.util.List; 47 48 /** 49 * Used to describe a visible range that can be normalized to [0, 1]. 50 */ 51 class Range { 52 final float relativeMin; 53 final float relativeMax; 54 float origin; 55 float min; 56 float max; 57 Range(float relMin, float relMax)58 public Range(float relMin, float relMax) { 59 min = relativeMin = relMin; 60 max = relativeMax = relMax; 61 } 62 63 /** 64 * Offsets this range to a given absolute position. 65 */ offset(float x)66 public void offset(float x) { 67 this.origin = x; 68 min = x + relativeMin; 69 max = x + relativeMax; 70 } 71 72 /** 73 * Returns x normalized to the range 0 to 1 such that 0 = min, 0.5 = origin and 1 = max 74 * 75 * @param x is an absolute value in the same domain as origin 76 */ getNormalizedX(float x)77 public float getNormalizedX(float x) { 78 if (x < origin) { 79 return 0.5f + 0.5f * (x - origin) / -relativeMin; 80 } else { 81 return 0.5f + 0.5f * (x - origin) / relativeMax; 82 } 83 } 84 85 /** 86 * Given a normalized {@param x} value in this range, projected onto the full range to get an 87 * absolute value about the given {@param origin}. 88 */ getAbsoluteX(float normX)89 public float getAbsoluteX(float normX) { 90 if (normX < 0.5f) { 91 return (normX - 0.5f) / 0.5f * -relativeMin; 92 } else { 93 return (normX - 0.5f) / 0.5f * relativeMax; 94 } 95 } 96 97 /** 98 * Returns whether a value at an absolute x would be within range. 99 */ isInRange(float absX)100 public boolean isInRange(float absX) { 101 return (absX >= Math.floor(min)) && (absX <= Math.ceil(max)); 102 } 103 } 104 105 /** 106 * The layout logic for a TaskStackView. This layout needs to be able to calculate the stack layout 107 * without an activity-specific context only with the information passed in. This layout can have 108 * two states focused and unfocused, and in the focused state, there is a task that is displayed 109 * more prominently in the stack. 110 */ 111 public class TaskStackLayoutAlgorithm { 112 113 private static final String TAG = "TaskStackLayoutAlgorithm"; 114 115 // The distribution of view bounds alpha 116 // XXX: This is a hack because you can currently set the max alpha to be > 1f 117 public static final float OUTLINE_ALPHA_MIN_VALUE = 0f; 118 public static final float OUTLINE_ALPHA_MAX_VALUE = 2f; 119 120 // The medium/maximum dim on the tasks 121 private static final float MED_DIM = 0.15f; 122 private static final float MAX_DIM = 0.25f; 123 124 // The various focus states 125 public static final int STATE_FOCUSED = 1; 126 public static final int STATE_UNFOCUSED = 0; 127 128 // The side that an offset is anchored 129 @Retention(RetentionPolicy.SOURCE) 130 @IntDef({FROM_TOP, FROM_BOTTOM}) 131 public @interface AnchorSide {} 132 private static final int FROM_TOP = 0; 133 private static final int FROM_BOTTOM = 1; 134 135 // The extent that we care about when calculating fractions 136 @Retention(RetentionPolicy.SOURCE) 137 @IntDef({WIDTH, HEIGHT}) 138 public @interface Extent {} 139 private static final int WIDTH = 0; 140 private static final int HEIGHT = 1; 141 142 public interface TaskStackLayoutAlgorithmCallbacks { onFocusStateChanged(int prevFocusState, int curFocusState)143 void onFocusStateChanged(int prevFocusState, int curFocusState); 144 } 145 146 /** 147 * The various stack/freeform states. 148 */ 149 public static class StackState { 150 151 public static final StackState FREEFORM_ONLY = new StackState(1f, 255); 152 public static final StackState STACK_ONLY = new StackState(0f, 0); 153 public static final StackState SPLIT = new StackState(0.5f, 255); 154 155 public final float freeformHeightPct; 156 public final int freeformBackgroundAlpha; 157 158 /** 159 * @param freeformHeightPct the percentage of the stack height (not including paddings) to 160 * allocate to the freeform workspace 161 * @param freeformBackgroundAlpha the background alpha for the freeform workspace 162 */ StackState(float freeformHeightPct, int freeformBackgroundAlpha)163 private StackState(float freeformHeightPct, int freeformBackgroundAlpha) { 164 this.freeformHeightPct = freeformHeightPct; 165 this.freeformBackgroundAlpha = freeformBackgroundAlpha; 166 } 167 168 /** 169 * Resolves the stack state for the layout given a task stack. 170 */ getStackStateForStack(TaskStack stack)171 public static StackState getStackStateForStack(TaskStack stack) { 172 SystemServicesProxy ssp = Recents.getSystemServices(); 173 boolean hasFreeformWorkspaces = ssp.hasFreeformWorkspaceSupport(); 174 int freeformCount = stack.getFreeformTaskCount(); 175 int stackCount = stack.getStackTaskCount(); 176 if (hasFreeformWorkspaces && stackCount > 0 && freeformCount > 0) { 177 return SPLIT; 178 } else if (hasFreeformWorkspaces && freeformCount > 0) { 179 return FREEFORM_ONLY; 180 } else { 181 return STACK_ONLY; 182 } 183 } 184 185 /** 186 * Computes the freeform and stack rect for this state. 187 * 188 * @param freeformRectOut the freeform rect to be written out 189 * @param stackRectOut the stack rect, we only write out the top of the stack 190 * @param taskStackBounds the full rect that the freeform rect can take up 191 */ computeRects(Rect freeformRectOut, Rect stackRectOut, Rect taskStackBounds, int topMargin, int freeformGap, int stackBottomOffset)192 public void computeRects(Rect freeformRectOut, Rect stackRectOut, 193 Rect taskStackBounds, int topMargin, int freeformGap, int stackBottomOffset) { 194 // The freeform height is the visible height (not including system insets) - padding 195 // above freeform and below stack - gap between the freeform and stack 196 int availableHeight = taskStackBounds.height() - topMargin - stackBottomOffset; 197 int ffPaddedHeight = (int) (availableHeight * freeformHeightPct); 198 int ffHeight = Math.max(0, ffPaddedHeight - freeformGap); 199 freeformRectOut.set(taskStackBounds.left, 200 taskStackBounds.top + topMargin, 201 taskStackBounds.right, 202 taskStackBounds.top + topMargin + ffHeight); 203 stackRectOut.set(taskStackBounds.left, 204 taskStackBounds.top, 205 taskStackBounds.right, 206 taskStackBounds.bottom); 207 if (ffPaddedHeight > 0) { 208 stackRectOut.top += ffPaddedHeight; 209 } else { 210 stackRectOut.top += topMargin; 211 } 212 } 213 } 214 215 // A report of the visibility state of the stack 216 public class VisibilityReport { 217 public int numVisibleTasks; 218 public int numVisibleThumbnails; 219 220 /** Package level ctor */ VisibilityReport(int tasks, int thumbnails)221 VisibilityReport(int tasks, int thumbnails) { 222 numVisibleTasks = tasks; 223 numVisibleThumbnails = thumbnails; 224 } 225 } 226 227 Context mContext; 228 private StackState mState = StackState.SPLIT; 229 private TaskStackLayoutAlgorithmCallbacks mCb; 230 231 // The task bounds (untransformed) for layout. This rect is anchored at mTaskRoot. 232 @ViewDebug.ExportedProperty(category="recents") 233 public Rect mTaskRect = new Rect(); 234 // The freeform workspace bounds, inset by the top system insets and is a fixed height 235 @ViewDebug.ExportedProperty(category="recents") 236 public Rect mFreeformRect = new Rect(); 237 // The stack bounds, inset from the top system insets, and runs to the bottom of the screen 238 @ViewDebug.ExportedProperty(category="recents") 239 public Rect mStackRect = new Rect(); 240 // This is the current system insets 241 @ViewDebug.ExportedProperty(category="recents") 242 public Rect mSystemInsets = new Rect(); 243 // This is the bounds of the stack action above the stack rect 244 @ViewDebug.ExportedProperty(category="recents") 245 public Rect mStackActionButtonRect = new Rect(); 246 247 // The visible ranges when the stack is focused and unfocused 248 private Range mUnfocusedRange; 249 private Range mFocusedRange; 250 251 // The base top margin for the stack from the system insets 252 @ViewDebug.ExportedProperty(category="recents") 253 private int mBaseTopMargin; 254 // The base side margin for the stack from the system insets 255 @ViewDebug.ExportedProperty(category="recents") 256 private int mBaseSideMargin; 257 // The base bottom margin for the stack from the system insets 258 @ViewDebug.ExportedProperty(category="recents") 259 private int mBaseBottomMargin; 260 private int mMinMargin; 261 262 // The gap between the freeform and stack layouts 263 @ViewDebug.ExportedProperty(category="recents") 264 private int mFreeformStackGap; 265 266 // The initial offset that the focused task is from the top 267 @ViewDebug.ExportedProperty(category="recents") 268 private int mInitialTopOffset; 269 private int mBaseInitialTopOffset; 270 // The initial offset that the launch-from task is from the bottom 271 @ViewDebug.ExportedProperty(category="recents") 272 private int mInitialBottomOffset; 273 private int mBaseInitialBottomOffset; 274 275 // The height between the top margin and the top of the focused task 276 @ViewDebug.ExportedProperty(category="recents") 277 private int mFocusedTopPeekHeight; 278 // The height between the bottom margin and the top of task in front of the focused task 279 @ViewDebug.ExportedProperty(category="recents") 280 private int mFocusedBottomPeekHeight; 281 282 // The offset from the bottom of the stack to the bottom of the bounds when the stack is 283 // scrolled to the front 284 @ViewDebug.ExportedProperty(category="recents") 285 private int mStackBottomOffset; 286 287 // The paths defining the motion of the tasks when the stack is focused and unfocused 288 private Path mUnfocusedCurve; 289 private Path mFocusedCurve; 290 private FreePathInterpolator mUnfocusedCurveInterpolator; 291 private FreePathInterpolator mFocusedCurveInterpolator; 292 293 // The paths defining the distribution of the dim to apply to tasks in the stack when focused 294 // and unfocused 295 private Path mUnfocusedDimCurve; 296 private Path mFocusedDimCurve; 297 private FreePathInterpolator mUnfocusedDimCurveInterpolator; 298 private FreePathInterpolator mFocusedDimCurveInterpolator; 299 300 // The state of the stack focus (0..1), which controls the transition of the stack from the 301 // focused to non-focused state 302 @ViewDebug.ExportedProperty(category="recents") 303 private int mFocusState; 304 305 // The smallest scroll progress, at this value, the back most task will be visible 306 @ViewDebug.ExportedProperty(category="recents") 307 float mMinScrollP; 308 // The largest scroll progress, at this value, the front most task will be visible above the 309 // navigation bar 310 @ViewDebug.ExportedProperty(category="recents") 311 float mMaxScrollP; 312 // The initial progress that the scroller is set when you first enter recents 313 @ViewDebug.ExportedProperty(category="recents") 314 float mInitialScrollP; 315 // The task progress for the front-most task in the stack 316 @ViewDebug.ExportedProperty(category="recents") 317 float mFrontMostTaskP; 318 319 // The last computed task counts 320 @ViewDebug.ExportedProperty(category="recents") 321 int mNumStackTasks; 322 @ViewDebug.ExportedProperty(category="recents") 323 int mNumFreeformTasks; 324 325 // The min/max z translations 326 @ViewDebug.ExportedProperty(category="recents") 327 int mMinTranslationZ; 328 @ViewDebug.ExportedProperty(category="recents") 329 int mMaxTranslationZ; 330 331 // Optimization, allows for quick lookup of task -> index 332 private SparseIntArray mTaskIndexMap = new SparseIntArray(); 333 private SparseArray<Float> mTaskIndexOverrideMap = new SparseArray<>(); 334 335 // The freeform workspace layout 336 FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm; 337 338 // The transform to place TaskViews at the front and back of the stack respectively 339 TaskViewTransform mBackOfStackTransform = new TaskViewTransform(); 340 TaskViewTransform mFrontOfStackTransform = new TaskViewTransform(); 341 TaskStackLayoutAlgorithm(Context context, TaskStackLayoutAlgorithmCallbacks cb)342 public TaskStackLayoutAlgorithm(Context context, TaskStackLayoutAlgorithmCallbacks cb) { 343 Resources res = context.getResources(); 344 mContext = context; 345 mCb = cb; 346 mFreeformLayoutAlgorithm = new FreeformWorkspaceLayoutAlgorithm(context); 347 mMinMargin = res.getDimensionPixelSize(R.dimen.recents_layout_min_margin); 348 mBaseTopMargin = getDimensionForDevice(context, 349 R.dimen.recents_layout_top_margin_phone, 350 R.dimen.recents_layout_top_margin_tablet, 351 R.dimen.recents_layout_top_margin_tablet_xlarge); 352 mBaseSideMargin = getDimensionForDevice(context, 353 R.dimen.recents_layout_side_margin_phone, 354 R.dimen.recents_layout_side_margin_tablet, 355 R.dimen.recents_layout_side_margin_tablet_xlarge); 356 mBaseBottomMargin = res.getDimensionPixelSize(R.dimen.recents_layout_bottom_margin); 357 mFreeformStackGap = 358 res.getDimensionPixelSize(R.dimen.recents_freeform_layout_bottom_margin); 359 360 reloadOnConfigurationChange(context); 361 } 362 363 /** 364 * Reloads the layout for the current configuration. 365 */ reloadOnConfigurationChange(Context context)366 public void reloadOnConfigurationChange(Context context) { 367 Resources res = context.getResources(); 368 mFocusedRange = new Range(res.getFloat(R.integer.recents_layout_focused_range_min), 369 res.getFloat(R.integer.recents_layout_focused_range_max)); 370 mUnfocusedRange = new Range(res.getFloat(R.integer.recents_layout_unfocused_range_min), 371 res.getFloat(R.integer.recents_layout_unfocused_range_max)); 372 mFocusState = getInitialFocusState(); 373 mFocusedTopPeekHeight = res.getDimensionPixelSize(R.dimen.recents_layout_top_peek_size); 374 mFocusedBottomPeekHeight = 375 res.getDimensionPixelSize(R.dimen.recents_layout_bottom_peek_size); 376 mMinTranslationZ = res.getDimensionPixelSize(R.dimen.recents_layout_z_min); 377 mMaxTranslationZ = res.getDimensionPixelSize(R.dimen.recents_layout_z_max); 378 mBaseInitialTopOffset = getDimensionForDevice(context, 379 R.dimen.recents_layout_initial_top_offset_phone_port, 380 R.dimen.recents_layout_initial_top_offset_phone_land, 381 R.dimen.recents_layout_initial_top_offset_tablet, 382 R.dimen.recents_layout_initial_top_offset_tablet, 383 R.dimen.recents_layout_initial_top_offset_tablet, 384 R.dimen.recents_layout_initial_top_offset_tablet); 385 mBaseInitialBottomOffset = getDimensionForDevice(context, 386 R.dimen.recents_layout_initial_bottom_offset_phone_port, 387 R.dimen.recents_layout_initial_bottom_offset_phone_land, 388 R.dimen.recents_layout_initial_bottom_offset_tablet, 389 R.dimen.recents_layout_initial_bottom_offset_tablet, 390 R.dimen.recents_layout_initial_bottom_offset_tablet, 391 R.dimen.recents_layout_initial_bottom_offset_tablet); 392 mFreeformLayoutAlgorithm.reloadOnConfigurationChange(context); 393 } 394 395 /** 396 * Resets this layout when the stack view is reset. 397 */ reset()398 public void reset() { 399 mTaskIndexOverrideMap.clear(); 400 setFocusState(getInitialFocusState()); 401 } 402 403 /** 404 * Sets the system insets. 405 */ setSystemInsets(Rect systemInsets)406 public boolean setSystemInsets(Rect systemInsets) { 407 boolean changed = !mSystemInsets.equals(systemInsets); 408 mSystemInsets.set(systemInsets); 409 return changed; 410 } 411 412 /** 413 * Sets the focused state. 414 */ setFocusState(int focusState)415 public void setFocusState(int focusState) { 416 int prevFocusState = mFocusState; 417 mFocusState = focusState; 418 updateFrontBackTransforms(); 419 if (mCb != null) { 420 mCb.onFocusStateChanged(prevFocusState, focusState); 421 } 422 } 423 424 /** 425 * Gets the focused state. 426 */ getFocusState()427 public int getFocusState() { 428 return mFocusState; 429 } 430 431 /** 432 * Computes the stack and task rects. The given task stack bounds already has the top/right 433 * insets and left/right padding already applied. 434 */ initialize(Rect displayRect, Rect windowRect, Rect taskStackBounds, StackState state)435 public void initialize(Rect displayRect, Rect windowRect, Rect taskStackBounds, 436 StackState state) { 437 Rect lastStackRect = new Rect(mStackRect); 438 439 int topMargin = getScaleForExtent(windowRect, displayRect, mBaseTopMargin, mMinMargin, HEIGHT); 440 int bottomMargin = getScaleForExtent(windowRect, displayRect, mBaseBottomMargin, mMinMargin, 441 HEIGHT); 442 mInitialTopOffset = getScaleForExtent(windowRect, displayRect, mBaseInitialTopOffset, 443 mMinMargin, HEIGHT); 444 mInitialBottomOffset = mBaseInitialBottomOffset; 445 446 // Compute the stack bounds 447 mState = state; 448 mStackBottomOffset = mSystemInsets.bottom + bottomMargin; 449 state.computeRects(mFreeformRect, mStackRect, taskStackBounds, topMargin, 450 mFreeformStackGap, mStackBottomOffset); 451 452 // The stack action button will take the full un-padded header space above the stack 453 mStackActionButtonRect.set(mStackRect.left, mStackRect.top - topMargin, 454 mStackRect.right, mStackRect.top + mFocusedTopPeekHeight); 455 456 // Anchor the task rect top aligned to the stack rect 457 int height = mStackRect.height() - mInitialTopOffset - mStackBottomOffset; 458 mTaskRect.set(mStackRect.left, mStackRect.top, mStackRect.right, mStackRect.top + height); 459 460 // Short circuit here if the stack rects haven't changed so we don't do all the work below 461 if (!lastStackRect.equals(mStackRect)) { 462 // Reinitialize the focused and unfocused curves 463 mUnfocusedCurve = constructUnfocusedCurve(); 464 mUnfocusedCurveInterpolator = new FreePathInterpolator(mUnfocusedCurve); 465 mFocusedCurve = constructFocusedCurve(); 466 mFocusedCurveInterpolator = new FreePathInterpolator(mFocusedCurve); 467 mUnfocusedDimCurve = constructUnfocusedDimCurve(); 468 mUnfocusedDimCurveInterpolator = new FreePathInterpolator(mUnfocusedDimCurve); 469 mFocusedDimCurve = constructFocusedDimCurve(); 470 mFocusedDimCurveInterpolator = new FreePathInterpolator(mFocusedDimCurve); 471 472 updateFrontBackTransforms(); 473 } 474 } 475 476 /** 477 * Computes the minimum and maximum scroll progress values and the progress values for each task 478 * in the stack. 479 */ update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet)480 void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet) { 481 SystemServicesProxy ssp = Recents.getSystemServices(); 482 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); 483 484 // Clear the progress map 485 mTaskIndexMap.clear(); 486 487 // Return early if we have no tasks 488 ArrayList<Task> tasks = stack.getStackTasks(); 489 if (tasks.isEmpty()) { 490 mFrontMostTaskP = 0; 491 mMinScrollP = mMaxScrollP = mInitialScrollP = 0; 492 mNumStackTasks = mNumFreeformTasks = 0; 493 return; 494 } 495 496 // Filter the set of freeform and stack tasks 497 ArrayList<Task> freeformTasks = new ArrayList<>(); 498 ArrayList<Task> stackTasks = new ArrayList<>(); 499 for (int i = 0; i < tasks.size(); i++) { 500 Task task = tasks.get(i); 501 if (ignoreTasksSet.contains(task.key)) { 502 continue; 503 } 504 if (task.isFreeformTask()) { 505 freeformTasks.add(task); 506 } else { 507 stackTasks.add(task); 508 } 509 } 510 mNumStackTasks = stackTasks.size(); 511 mNumFreeformTasks = freeformTasks.size(); 512 513 // Put each of the tasks in the progress map at a fixed index (does not need to actually 514 // map to a scroll position, just by index) 515 int taskCount = stackTasks.size(); 516 for (int i = 0; i < taskCount; i++) { 517 Task task = stackTasks.get(i); 518 mTaskIndexMap.put(task.key.id, i); 519 } 520 521 // Update the freeform tasks 522 if (!freeformTasks.isEmpty()) { 523 mFreeformLayoutAlgorithm.update(freeformTasks, this); 524 } 525 526 // Calculate the min/max/initial scroll 527 Task launchTask = stack.getLaunchTarget(); 528 int launchTaskIndex = launchTask != null 529 ? stack.indexOfStackTask(launchTask) 530 : mNumStackTasks - 1; 531 if (getInitialFocusState() == STATE_FOCUSED) { 532 int maxBottomOffset = mStackBottomOffset + mTaskRect.height(); 533 float maxBottomNormX = getNormalizedXFromFocusedY(maxBottomOffset, FROM_BOTTOM); 534 mFocusedRange.offset(0f); 535 mMinScrollP = 0; 536 mMaxScrollP = Math.max(mMinScrollP, (mNumStackTasks - 1) - 537 Math.max(0, mFocusedRange.getAbsoluteX(maxBottomNormX))); 538 if (launchState.launchedFromHome) { 539 mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); 540 } else { 541 mInitialScrollP = Utilities.clamp(launchTaskIndex - 1, mMinScrollP, mMaxScrollP); 542 } 543 } else if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) { 544 // If there is one stack task, ignore the min/max/initial scroll positions 545 mMinScrollP = 0; 546 mMaxScrollP = 0; 547 mInitialScrollP = 0; 548 } else { 549 // Set the max scroll to be the point where the front most task is visible with the 550 // stack bottom offset 551 int maxBottomOffset = mStackBottomOffset + mTaskRect.height(); 552 float maxBottomNormX = getNormalizedXFromUnfocusedY(maxBottomOffset, FROM_BOTTOM); 553 mUnfocusedRange.offset(0f); 554 mMinScrollP = 0; 555 mMaxScrollP = Math.max(mMinScrollP, (mNumStackTasks - 1) - 556 Math.max(0, mUnfocusedRange.getAbsoluteX(maxBottomNormX))); 557 boolean scrollToFront = launchState.launchedFromHome || 558 launchState.launchedViaDockGesture; 559 if (launchState.launchedWithAltTab) { 560 mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); 561 } else if (scrollToFront) { 562 mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); 563 } else { 564 // We are overriding the initial two task positions, so set the initial scroll 565 // position to match the second task (aka focused task) position 566 float initialTopNormX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 567 mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, (mNumStackTasks - 2)) 568 - Math.max(0, mUnfocusedRange.getAbsoluteX(initialTopNormX))); 569 } 570 } 571 } 572 573 /** 574 * Creates task overrides to ensure the initial stack layout if necessary. 575 */ setTaskOverridesForInitialState(TaskStack stack, boolean ignoreScrollToFront)576 public void setTaskOverridesForInitialState(TaskStack stack, boolean ignoreScrollToFront) { 577 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); 578 579 mTaskIndexOverrideMap.clear(); 580 581 boolean scrollToFront = launchState.launchedFromHome || 582 launchState.launchedViaDockGesture; 583 if (getInitialFocusState() == STATE_UNFOCUSED && mNumStackTasks > 1) { 584 if (ignoreScrollToFront || (!launchState.launchedWithAltTab && !scrollToFront)) { 585 // Set the initial scroll to the predefined state (which differs from the stack) 586 float [] initialNormX = null; 587 float minBottomTaskNormX = getNormalizedXFromUnfocusedY(mSystemInsets.bottom + 588 mInitialBottomOffset, FROM_BOTTOM); 589 float maxBottomTaskNormX = getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight + 590 mTaskRect.height() - mMinMargin, FROM_TOP); 591 if (mNumStackTasks <= 2) { 592 // For small stacks, position the tasks so that they are top aligned to under 593 // the action button, but ensure that it is at least a certain offset from the 594 // bottom of the stack 595 initialNormX = new float[] { 596 Math.min(maxBottomTaskNormX, minBottomTaskNormX), 597 getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight, FROM_TOP) 598 }; 599 } else { 600 initialNormX = new float[] { 601 minBottomTaskNormX, 602 getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP) 603 }; 604 } 605 606 mUnfocusedRange.offset(0f); 607 List<Task> tasks = stack.getStackTasks(); 608 int taskCount = tasks.size(); 609 for (int i = taskCount - 1; i >= 0; i--) { 610 int indexFromFront = taskCount - i - 1; 611 if (indexFromFront >= initialNormX.length) { 612 break; 613 } 614 float newTaskProgress = mInitialScrollP + 615 mUnfocusedRange.getAbsoluteX(initialNormX[indexFromFront]); 616 mTaskIndexOverrideMap.put(tasks.get(i).key.id, newTaskProgress); 617 } 618 } 619 } 620 } 621 622 /** 623 * Adds and override task progress for the given task when transitioning from focused to 624 * unfocused state. 625 */ addUnfocusedTaskOverride(Task task, float stackScroll)626 public void addUnfocusedTaskOverride(Task task, float stackScroll) { 627 if (mFocusState != STATE_UNFOCUSED) { 628 mFocusedRange.offset(stackScroll); 629 mUnfocusedRange.offset(stackScroll); 630 float focusedRangeX = mFocusedRange.getNormalizedX(mTaskIndexMap.get(task.key.id)); 631 float focusedY = mFocusedCurveInterpolator.getInterpolation(focusedRangeX); 632 float unfocusedRangeX = mUnfocusedCurveInterpolator.getX(focusedY); 633 float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX); 634 if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) { 635 mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress); 636 } 637 } 638 } 639 640 /** 641 * Adds and override task progress for the given task when transitioning from focused to 642 * unfocused state. 643 */ addUnfocusedTaskOverride(TaskView taskView, float stackScroll)644 public void addUnfocusedTaskOverride(TaskView taskView, float stackScroll) { 645 mFocusedRange.offset(stackScroll); 646 mUnfocusedRange.offset(stackScroll); 647 648 Task task = taskView.getTask(); 649 int top = taskView.getTop() - mTaskRect.top; 650 float focusedRangeX = getNormalizedXFromFocusedY(top, FROM_TOP); 651 float unfocusedRangeX = getNormalizedXFromUnfocusedY(top, FROM_TOP); 652 float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX); 653 if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) { 654 mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress); 655 } 656 } 657 clearUnfocusedTaskOverrides()658 public void clearUnfocusedTaskOverrides() { 659 mTaskIndexOverrideMap.clear(); 660 } 661 662 /** 663 * Updates this stack when a scroll happens. 664 * 665 */ updateFocusStateOnScroll(float lastTargetStackScroll, float targetStackScroll, float lastStackScroll)666 public float updateFocusStateOnScroll(float lastTargetStackScroll, float targetStackScroll, 667 float lastStackScroll) { 668 if (targetStackScroll == lastStackScroll) { 669 return targetStackScroll; 670 } 671 672 float deltaScroll = targetStackScroll - lastStackScroll; 673 float deltaTargetScroll = targetStackScroll - lastTargetStackScroll; 674 float newScroll = targetStackScroll; 675 mUnfocusedRange.offset(targetStackScroll); 676 for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) { 677 int taskId = mTaskIndexOverrideMap.keyAt(i); 678 float x = mTaskIndexMap.get(taskId); 679 float overrideX = mTaskIndexOverrideMap.get(taskId, 0f); 680 float newOverrideX = overrideX + deltaScroll; 681 if (isInvalidOverrideX(x, overrideX, newOverrideX)) { 682 // Remove the override once we reach the original task index 683 mTaskIndexOverrideMap.removeAt(i); 684 } else if ((overrideX >= x && deltaScroll <= 0f) || 685 (overrideX <= x && deltaScroll >= 0f)) { 686 // Scrolling from override x towards x, then lock the task in place 687 mTaskIndexOverrideMap.put(taskId, newOverrideX); 688 } else { 689 // Scrolling override x away from x, we should still move the scroll towards x 690 newScroll = lastStackScroll; 691 newOverrideX = overrideX - deltaTargetScroll; 692 if (isInvalidOverrideX(x, overrideX, newOverrideX)) { 693 mTaskIndexOverrideMap.removeAt(i); 694 } else{ 695 mTaskIndexOverrideMap.put(taskId, newOverrideX); 696 } 697 } 698 } 699 return newScroll; 700 } 701 isInvalidOverrideX(float x, float overrideX, float newOverrideX)702 private boolean isInvalidOverrideX(float x, float overrideX, float newOverrideX) { 703 boolean outOfBounds = mUnfocusedRange.getNormalizedX(newOverrideX) < 0f || 704 mUnfocusedRange.getNormalizedX(newOverrideX) > 1f; 705 return outOfBounds || (overrideX >= x && x >= newOverrideX) || 706 (overrideX <= x && x <= newOverrideX); 707 } 708 709 /** 710 * Returns the default focus state. 711 */ getInitialFocusState()712 public int getInitialFocusState() { 713 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); 714 RecentsDebugFlags debugFlags = Recents.getDebugFlags(); 715 if (debugFlags.isPagingEnabled() || launchState.launchedWithAltTab) { 716 return STATE_FOCUSED; 717 } else { 718 return STATE_UNFOCUSED; 719 } 720 } 721 722 /** 723 * Returns the TaskViewTransform that would put the task just off the back of the stack. 724 */ getBackOfStackTransform()725 public TaskViewTransform getBackOfStackTransform() { 726 return mBackOfStackTransform; 727 } 728 729 /** 730 * Returns the TaskViewTransform that would put the task just off the front of the stack. 731 */ getFrontOfStackTransform()732 public TaskViewTransform getFrontOfStackTransform() { 733 return mFrontOfStackTransform; 734 } 735 736 /** 737 * Returns the current stack state. 738 */ getStackState()739 public StackState getStackState() { 740 return mState; 741 } 742 743 /** 744 * Returns whether this stack layout has been initialized. 745 */ isInitialized()746 public boolean isInitialized() { 747 return !mStackRect.isEmpty(); 748 } 749 750 /** 751 * Computes the maximum number of visible tasks and thumbnails when the scroll is at the initial 752 * stack scroll. Requires that update() is called first. 753 */ computeStackVisibilityReport(ArrayList<Task> tasks)754 public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) { 755 // Ensure minimum visibility count 756 if (tasks.size() <= 1) { 757 return new VisibilityReport(1, 1); 758 } 759 760 // Quick return when there are no stack tasks 761 if (mNumStackTasks == 0) { 762 return new VisibilityReport(Math.max(mNumFreeformTasks, 1), 763 Math.max(mNumFreeformTasks, 1)); 764 } 765 766 // Otherwise, walk backwards in the stack and count the number of tasks and visible 767 // thumbnails and add that to the total freeform task count 768 TaskViewTransform tmpTransform = new TaskViewTransform(); 769 Range currentRange = getInitialFocusState() > 0f ? mFocusedRange : mUnfocusedRange; 770 currentRange.offset(mInitialScrollP); 771 int taskBarHeight = mContext.getResources().getDimensionPixelSize( 772 R.dimen.recents_task_view_header_height); 773 int numVisibleTasks = Math.max(mNumFreeformTasks, 1); 774 int numVisibleThumbnails = Math.max(mNumFreeformTasks, 1); 775 float prevScreenY = Integer.MAX_VALUE; 776 for (int i = tasks.size() - 1; i >= 0; i--) { 777 Task task = tasks.get(i); 778 779 // Skip freeform 780 if (task.isFreeformTask()) { 781 continue; 782 } 783 784 // Skip invisible 785 float taskProgress = getStackScrollForTask(task); 786 if (!currentRange.isInRange(taskProgress)) { 787 continue; 788 } 789 790 boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task); 791 if (isFrontMostTaskInGroup) { 792 getStackTransform(taskProgress, taskProgress, mInitialScrollP, mFocusState, 793 tmpTransform, null, false /* ignoreSingleTaskCase */, 794 false /* forceUpdate */); 795 float screenY = tmpTransform.rect.top; 796 boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight; 797 if (hasVisibleThumbnail) { 798 numVisibleThumbnails++; 799 numVisibleTasks++; 800 prevScreenY = screenY; 801 } else { 802 // Once we hit the next front most task that does not have a visible thumbnail, 803 // walk through remaining visible set 804 for (int j = i; j >= 0; j--) { 805 numVisibleTasks++; 806 taskProgress = getStackScrollForTask(tasks.get(j)); 807 if (!currentRange.isInRange(taskProgress)) { 808 continue; 809 } 810 } 811 break; 812 } 813 } else if (!isFrontMostTaskInGroup) { 814 // Affiliated task, no thumbnail 815 numVisibleTasks++; 816 } 817 } 818 return new VisibilityReport(numVisibleTasks, numVisibleThumbnails); 819 } 820 821 /** 822 * Returns the transform for the given task. This transform is relative to the mTaskRect, which 823 * is what the view is measured and laid out with. 824 */ getStackTransform(Task task, float stackScroll, TaskViewTransform transformOut, TaskViewTransform frontTransform)825 public TaskViewTransform getStackTransform(Task task, float stackScroll, 826 TaskViewTransform transformOut, TaskViewTransform frontTransform) { 827 return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform, 828 false /* forceUpdate */, false /* ignoreTaskOverrides */); 829 } 830 getStackTransform(Task task, float stackScroll, TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean ignoreTaskOverrides)831 public TaskViewTransform getStackTransform(Task task, float stackScroll, 832 TaskViewTransform transformOut, TaskViewTransform frontTransform, 833 boolean ignoreTaskOverrides) { 834 return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform, 835 false /* forceUpdate */, ignoreTaskOverrides); 836 } 837 getStackTransform(Task task, float stackScroll, int focusState, TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate, boolean ignoreTaskOverrides)838 public TaskViewTransform getStackTransform(Task task, float stackScroll, int focusState, 839 TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate, 840 boolean ignoreTaskOverrides) { 841 if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) { 842 mFreeformLayoutAlgorithm.getTransform(task, transformOut, this); 843 return transformOut; 844 } else { 845 // Return early if we have an invalid index 846 int nonOverrideTaskProgress = mTaskIndexMap.get(task.key.id, -1); 847 if (task == null || nonOverrideTaskProgress == -1) { 848 transformOut.reset(); 849 return transformOut; 850 } 851 float taskProgress = ignoreTaskOverrides 852 ? nonOverrideTaskProgress 853 : getStackScrollForTask(task); 854 getStackTransform(taskProgress, nonOverrideTaskProgress, stackScroll, focusState, 855 transformOut, frontTransform, false /* ignoreSingleTaskCase */, forceUpdate); 856 return transformOut; 857 } 858 } 859 860 /** 861 * Like {@link #getStackTransform}, but in screen coordinates 862 */ getStackTransformScreenCoordinates(Task task, float stackScroll, TaskViewTransform transformOut, TaskViewTransform frontTransform, Rect windowOverrideRect)863 public TaskViewTransform getStackTransformScreenCoordinates(Task task, float stackScroll, 864 TaskViewTransform transformOut, TaskViewTransform frontTransform, 865 Rect windowOverrideRect) { 866 TaskViewTransform transform = getStackTransform(task, stackScroll, mFocusState, 867 transformOut, frontTransform, true /* forceUpdate */, 868 false /* ignoreTaskOverrides */); 869 return transformToScreenCoordinates(transform, windowOverrideRect); 870 } 871 872 /** 873 * Transforms the given {@param transformOut} to the screen coordinates, overriding the current 874 * window rectangle with {@param windowOverrideRect} if non-null. 875 */ transformToScreenCoordinates(TaskViewTransform transformOut, Rect windowOverrideRect)876 public TaskViewTransform transformToScreenCoordinates(TaskViewTransform transformOut, 877 Rect windowOverrideRect) { 878 Rect windowRect = windowOverrideRect != null 879 ? windowOverrideRect 880 : Recents.getSystemServices().getWindowRect(); 881 transformOut.rect.offset(windowRect.left, windowRect.top); 882 return transformOut; 883 } 884 885 /** 886 * Update/get the transform. 887 * 888 * @param ignoreSingleTaskCase When set, will ensure that the transform computed does not take 889 * into account the special single-task case. This is only used 890 * internally to ensure that we can calculate the transform for any 891 * position in the stack. 892 */ getStackTransform(float taskProgress, float nonOverrideTaskProgress, float stackScroll, int focusState, TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean ignoreSingleTaskCase, boolean forceUpdate)893 public void getStackTransform(float taskProgress, float nonOverrideTaskProgress, 894 float stackScroll, int focusState, TaskViewTransform transformOut, 895 TaskViewTransform frontTransform, boolean ignoreSingleTaskCase, boolean forceUpdate) { 896 SystemServicesProxy ssp = Recents.getSystemServices(); 897 898 // Ensure that the task is in range 899 mUnfocusedRange.offset(stackScroll); 900 mFocusedRange.offset(stackScroll); 901 boolean unfocusedVisible = mUnfocusedRange.isInRange(taskProgress); 902 boolean focusedVisible = mFocusedRange.isInRange(taskProgress); 903 904 // Skip if the task is not visible 905 if (!forceUpdate && !unfocusedVisible && !focusedVisible) { 906 transformOut.reset(); 907 return; 908 } 909 910 // Map the absolute task progress to the normalized x at the stack scroll. We use this to 911 // calculate positions along the curve. 912 mUnfocusedRange.offset(stackScroll); 913 mFocusedRange.offset(stackScroll); 914 float unfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); 915 float focusedRangeX = mFocusedRange.getNormalizedX(taskProgress); 916 917 // Map the absolute task progress to the normalized x at the bounded stack scroll. We use 918 // this to calculate bounded properties, like translationZ and outline alpha. 919 float boundedStackScroll = Utilities.clamp(stackScroll, mMinScrollP, mMaxScrollP); 920 mUnfocusedRange.offset(boundedStackScroll); 921 mFocusedRange.offset(boundedStackScroll); 922 float boundedScrollUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); 923 float boundedScrollUnfocusedNonOverrideRangeX = 924 mUnfocusedRange.getNormalizedX(nonOverrideTaskProgress); 925 926 // Map the absolute task progress to the normalized x at the upper bounded stack scroll. 927 // We use this to calculate the dim, which is bounded only on one end. 928 float lowerBoundedStackScroll = Utilities.clamp(stackScroll, -Float.MAX_VALUE, mMaxScrollP); 929 mUnfocusedRange.offset(lowerBoundedStackScroll); 930 mFocusedRange.offset(lowerBoundedStackScroll); 931 float lowerBoundedUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); 932 float lowerBoundedFocusedRangeX = mFocusedRange.getNormalizedX(taskProgress); 933 934 int x = (mStackRect.width() - mTaskRect.width()) / 2; 935 int y; 936 float z; 937 float dimAlpha; 938 float viewOutlineAlpha; 939 if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) { 940 // When there is exactly one task, then decouple the task from the stack and just move 941 // in screen space 942 float tmpP = (mMinScrollP - stackScroll) / mNumStackTasks; 943 int centerYOffset = (mStackRect.top - mTaskRect.top) + 944 (mStackRect.height() - mSystemInsets.bottom - mTaskRect.height()) / 2; 945 y = centerYOffset + getYForDeltaP(tmpP, 0); 946 z = mMaxTranslationZ; 947 dimAlpha = 0f; 948 viewOutlineAlpha = OUTLINE_ALPHA_MIN_VALUE + 949 (OUTLINE_ALPHA_MAX_VALUE - OUTLINE_ALPHA_MIN_VALUE) / 2f; 950 951 } else { 952 // Otherwise, update the task to the stack layout 953 int unfocusedY = (int) ((1f - mUnfocusedCurveInterpolator.getInterpolation( 954 unfocusedRangeX)) * mStackRect.height()); 955 int focusedY = (int) ((1f - mFocusedCurveInterpolator.getInterpolation( 956 focusedRangeX)) * mStackRect.height()); 957 float unfocusedDim = mUnfocusedDimCurveInterpolator.getInterpolation( 958 lowerBoundedUnfocusedRangeX); 959 float focusedDim = mFocusedDimCurveInterpolator.getInterpolation( 960 lowerBoundedFocusedRangeX); 961 962 // Special case, because we override the initial task positions differently for small 963 // stacks, we clamp the dim to 0 in the initial position, and then only modulate the 964 // dim when the task is scrolled back towards the top of the screen 965 if (mNumStackTasks <= 2 && nonOverrideTaskProgress == 0f) { 966 if (boundedScrollUnfocusedRangeX >= 0.5f) { 967 unfocusedDim = 0f; 968 } else { 969 float offset = mUnfocusedDimCurveInterpolator.getInterpolation(0.5f); 970 unfocusedDim -= offset; 971 unfocusedDim *= MAX_DIM / (MAX_DIM - offset); 972 } 973 } 974 975 y = (mStackRect.top - mTaskRect.top) + 976 (int) Utilities.mapRange(focusState, unfocusedY, focusedY); 977 z = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedNonOverrideRangeX), 978 mMinTranslationZ, mMaxTranslationZ); 979 dimAlpha = Utilities.mapRange(focusState, unfocusedDim, focusedDim); 980 viewOutlineAlpha = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedRangeX), 981 OUTLINE_ALPHA_MIN_VALUE, OUTLINE_ALPHA_MAX_VALUE); 982 } 983 984 // Fill out the transform 985 transformOut.scale = 1f; 986 transformOut.alpha = 1f; 987 transformOut.translationZ = z; 988 transformOut.dimAlpha = dimAlpha; 989 transformOut.viewOutlineAlpha = viewOutlineAlpha; 990 transformOut.rect.set(mTaskRect); 991 transformOut.rect.offset(x, y); 992 Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale); 993 transformOut.visible = (transformOut.rect.top < mStackRect.bottom) && 994 (frontTransform == null || transformOut.rect.top != frontTransform.rect.top); 995 } 996 997 /** 998 * Returns the untransformed task view bounds. 999 */ getUntransformedTaskViewBounds()1000 public Rect getUntransformedTaskViewBounds() { 1001 return new Rect(mTaskRect); 1002 } 1003 1004 /** 1005 * Returns the scroll progress to scroll to such that the top of the task is at the top of the 1006 * stack. 1007 */ getStackScrollForTask(Task t)1008 float getStackScrollForTask(Task t) { 1009 Float overrideP = mTaskIndexOverrideMap.get(t.key.id, null); 1010 if (overrideP == null) { 1011 return (float) mTaskIndexMap.get(t.key.id, 0); 1012 } 1013 return overrideP; 1014 } 1015 1016 /** 1017 * Returns the original scroll progress to scroll to such that the top of the task is at the top 1018 * of the stack. 1019 */ getStackScrollForTaskIgnoreOverrides(Task t)1020 float getStackScrollForTaskIgnoreOverrides(Task t) { 1021 return (float) mTaskIndexMap.get(t.key.id, 0); 1022 } 1023 1024 /** 1025 * Returns the scroll progress to scroll to such that the top of the task at the initial top 1026 * offset (which is at the task's brightest point). 1027 */ getStackScrollForTaskAtInitialOffset(Task t)1028 float getStackScrollForTaskAtInitialOffset(Task t) { 1029 float normX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 1030 mUnfocusedRange.offset(0f); 1031 return Utilities.clamp((float) mTaskIndexMap.get(t.key.id, 0) - Math.max(0, 1032 mUnfocusedRange.getAbsoluteX(normX)), mMinScrollP, mMaxScrollP); 1033 } 1034 1035 /** 1036 * Maps a movement in screen y, relative to {@param downY}, to a movement in along the arc 1037 * length of the curve. We know the curve is mostly flat, so we just map the length of the 1038 * screen along the arc-length proportionally (1/arclength). 1039 */ getDeltaPForY(int downY, int y)1040 public float getDeltaPForY(int downY, int y) { 1041 float deltaP = (float) (y - downY) / mStackRect.height() * 1042 mUnfocusedCurveInterpolator.getArcLength(); 1043 return -deltaP; 1044 } 1045 1046 /** 1047 * This is the inverse of {@link #getDeltaPForY}. Given a movement along the arc length 1048 * of the curve, map back to the screen y. 1049 */ getYForDeltaP(float downScrollP, float p)1050 public int getYForDeltaP(float downScrollP, float p) { 1051 int y = (int) ((p - downScrollP) * mStackRect.height() * 1052 (1f / mUnfocusedCurveInterpolator.getArcLength())); 1053 return -y; 1054 } 1055 1056 /** 1057 * Returns the task stack bounds in the current orientation. This rect takes into account the 1058 * top and right system insets (but not the bottom inset) and left/right paddings, but _not_ 1059 * the top/bottom padding or insets. 1060 */ getTaskStackBounds(Rect displayRect, Rect windowRect, int topInset, int rightInset, Rect taskStackBounds)1061 public void getTaskStackBounds(Rect displayRect, Rect windowRect, int topInset, int rightInset, 1062 Rect taskStackBounds) { 1063 taskStackBounds.set(windowRect.left, windowRect.top + topInset, 1064 windowRect.right - rightInset, windowRect.bottom); 1065 1066 // Ensure that the new width is at most the smaller display edge size 1067 int sideMargin = getScaleForExtent(windowRect, displayRect, mBaseSideMargin, mMinMargin, 1068 WIDTH); 1069 int targetStackWidth = taskStackBounds.width() - 2 * sideMargin; 1070 if (Utilities.getAppConfiguration(mContext).orientation 1071 == Configuration.ORIENTATION_LANDSCAPE) { 1072 // If we are in landscape, calculate the width of the stack in portrait and ensure that 1073 // we are not larger than that size 1074 Rect portraitDisplayRect = new Rect(0, 0, 1075 Math.min(displayRect.width(), displayRect.height()), 1076 Math.max(displayRect.width(), displayRect.height())); 1077 int portraitSideMargin = getScaleForExtent(portraitDisplayRect, portraitDisplayRect, 1078 mBaseSideMargin, mMinMargin, WIDTH); 1079 targetStackWidth = Math.min(targetStackWidth, 1080 portraitDisplayRect.width() - 2 * portraitSideMargin); 1081 } 1082 taskStackBounds.inset((taskStackBounds.width() - targetStackWidth) / 2, 0); 1083 } 1084 1085 /** 1086 * Retrieves resources that are constant regardless of the current configuration of the device. 1087 */ getDimensionForDevice(Context ctx, int phoneResId, int tabletResId, int xlargeTabletResId)1088 public static int getDimensionForDevice(Context ctx, int phoneResId, 1089 int tabletResId, int xlargeTabletResId) { 1090 return getDimensionForDevice(ctx, phoneResId, phoneResId, tabletResId, tabletResId, 1091 xlargeTabletResId, xlargeTabletResId); 1092 } 1093 1094 /** 1095 * Retrieves resources that are constant regardless of the current configuration of the device. 1096 */ getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId, int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId, int xlargeTabletLandResId)1097 public static int getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId, 1098 int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId, 1099 int xlargeTabletLandResId) { 1100 RecentsConfiguration config = Recents.getConfiguration(); 1101 Resources res = ctx.getResources(); 1102 boolean isLandscape = Utilities.getAppConfiguration(ctx).orientation == 1103 Configuration.ORIENTATION_LANDSCAPE; 1104 if (config.isXLargeScreen) { 1105 return res.getDimensionPixelSize(isLandscape 1106 ? xlargeTabletLandResId 1107 : xlargeTabletPortResId); 1108 } else if (config.isLargeScreen) { 1109 return res.getDimensionPixelSize(isLandscape 1110 ? tabletLandResId 1111 : tabletPortResId); 1112 } else { 1113 return res.getDimensionPixelSize(isLandscape 1114 ? phoneLandResId 1115 : phonePortResId); 1116 } 1117 } 1118 1119 /** 1120 * Returns the normalized x on the unfocused curve given an absolute Y position (relative to the 1121 * stack height). 1122 */ getNormalizedXFromUnfocusedY(float y, @AnchorSide int fromSide)1123 private float getNormalizedXFromUnfocusedY(float y, @AnchorSide int fromSide) { 1124 float offset = (fromSide == FROM_TOP) 1125 ? mStackRect.height() - y 1126 : y; 1127 float offsetPct = offset / mStackRect.height(); 1128 return mUnfocusedCurveInterpolator.getX(offsetPct); 1129 } 1130 1131 /** 1132 * Returns the normalized x on the focused curve given an absolute Y position (relative to the 1133 * stack height). 1134 */ getNormalizedXFromFocusedY(float y, @AnchorSide int fromSide)1135 private float getNormalizedXFromFocusedY(float y, @AnchorSide int fromSide) { 1136 float offset = (fromSide == FROM_TOP) 1137 ? mStackRect.height() - y 1138 : y; 1139 float offsetPct = offset / mStackRect.height(); 1140 return mFocusedCurveInterpolator.getX(offsetPct); 1141 } 1142 1143 /** 1144 * Creates a new path for the focused curve. 1145 */ constructFocusedCurve()1146 private Path constructFocusedCurve() { 1147 // Initialize the focused curve. This curve is a piecewise curve composed of several 1148 // linear pieces that goes from (0,1) through (0.5, peek height offset), 1149 // (0.5, bottom task offsets), and (1,0). 1150 float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); 1151 float bottomPeekHeightPct = (float) (mStackBottomOffset + mFocusedBottomPeekHeight) / 1152 mStackRect.height(); 1153 float minBottomPeekHeightPct = (float) (mFocusedTopPeekHeight + mTaskRect.height() - 1154 mMinMargin) / mStackRect.height(); 1155 Path p = new Path(); 1156 p.moveTo(0f, 1f); 1157 p.lineTo(0.5f, 1f - topPeekHeightPct); 1158 p.lineTo(1f - (0.5f / mFocusedRange.relativeMax), Math.max(1f - minBottomPeekHeightPct, 1159 bottomPeekHeightPct)); 1160 p.lineTo(1f, 0f); 1161 return p; 1162 } 1163 1164 /** 1165 * Creates a new path for the unfocused curve. 1166 */ constructUnfocusedCurve()1167 private Path constructUnfocusedCurve() { 1168 // Initialize the unfocused curve. This curve is a piecewise curve composed of two quadradic 1169 // beziers that goes from (0,1) through (0.5, peek height offset) and ends at (1,0). This 1170 // ensures that we match the range, at which 0.5 represents the stack scroll at the current 1171 // task progress. Because the height offset can change depending on a resource, we compute 1172 // the control point of the second bezier such that between it and a first known point, 1173 // there is a tangent at (0.5, peek height offset). 1174 float cpoint1X = 0.4f; 1175 float cpoint1Y = 0.975f; 1176 float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); 1177 float slope = ((1f - topPeekHeightPct) - cpoint1Y) / (0.5f - cpoint1X); 1178 float b = 1f - slope * cpoint1X; 1179 float cpoint2X = 0.65f; 1180 float cpoint2Y = slope * cpoint2X + b; 1181 Path p = new Path(); 1182 p.moveTo(0f, 1f); 1183 p.cubicTo(0f, 1f, cpoint1X, cpoint1Y, 0.5f, 1f - topPeekHeightPct); 1184 p.cubicTo(0.5f, 1f - topPeekHeightPct, cpoint2X, cpoint2Y, 1f, 0f); 1185 return p; 1186 } 1187 1188 /** 1189 * Creates a new path for the focused dim curve. 1190 */ constructFocusedDimCurve()1191 private Path constructFocusedDimCurve() { 1192 Path p = new Path(); 1193 // The focused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused 1194 // task), then goes back to max dim at the next task 1195 p.moveTo(0f, MAX_DIM); 1196 p.lineTo(0.5f, 0f); 1197 p.lineTo(0.5f + (0.5f / mFocusedRange.relativeMax), MAX_DIM); 1198 p.lineTo(1f, MAX_DIM); 1199 return p; 1200 } 1201 1202 /** 1203 * Creates a new path for the unfocused dim curve. 1204 */ constructUnfocusedDimCurve()1205 private Path constructUnfocusedDimCurve() { 1206 float focusX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 1207 float cpoint2X = focusX + (1f - focusX) / 2; 1208 Path p = new Path(); 1209 // The unfocused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused 1210 // task), then goes back to max dim towards the front of the stack 1211 p.moveTo(0f, MAX_DIM); 1212 p.cubicTo(focusX * 0.5f, MAX_DIM, focusX * 0.75f, MAX_DIM * 0.75f, focusX, 0f); 1213 p.cubicTo(cpoint2X, 0f, cpoint2X, MED_DIM, 1f, MED_DIM); 1214 return p; 1215 } 1216 1217 /** 1218 * Scales the given {@param value} to the scale of the {@param instance} rect relative to the 1219 * {@param other} rect in the {@param extent} side. 1220 */ getScaleForExtent(Rect instance, Rect other, int value, int minValue, @Extent int extent)1221 private int getScaleForExtent(Rect instance, Rect other, int value, int minValue, 1222 @Extent int extent) { 1223 if (extent == WIDTH) { 1224 float scale = Utilities.clamp01((float) instance.width() / other.width()); 1225 return Math.max(minValue, (int) (scale * value)); 1226 } else if (extent == HEIGHT) { 1227 float scale = Utilities.clamp01((float) instance.height() / other.height()); 1228 return Math.max(minValue, (int) (scale * value)); 1229 } 1230 return value; 1231 } 1232 1233 /** 1234 * Updates the current transforms that would put a TaskView at the front and back of the stack. 1235 */ updateFrontBackTransforms()1236 private void updateFrontBackTransforms() { 1237 // Return early if we have not yet initialized 1238 if (mStackRect.isEmpty()) { 1239 return; 1240 } 1241 1242 float min = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMin, 1243 mFocusedRange.relativeMin); 1244 float max = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMax, 1245 mFocusedRange.relativeMax); 1246 getStackTransform(min, min, 0f, mFocusState, mBackOfStackTransform, null, 1247 true /* ignoreSingleTaskCase */, true /* forceUpdate */); 1248 getStackTransform(max, max, 0f, mFocusState, mFrontOfStackTransform, null, 1249 true /* ignoreSingleTaskCase */, true /* forceUpdate */); 1250 mBackOfStackTransform.visible = true; 1251 mFrontOfStackTransform.visible = true; 1252 } 1253 dump(String prefix, PrintWriter writer)1254 public void dump(String prefix, PrintWriter writer) { 1255 String innerPrefix = prefix + " "; 1256 1257 writer.print(prefix); writer.print(TAG); 1258 writer.write(" numStackTasks="); writer.write(mNumStackTasks); 1259 writer.println(); 1260 1261 writer.print(innerPrefix); 1262 writer.print("insets="); writer.print(Utilities.dumpRect(mSystemInsets)); 1263 writer.print(" stack="); writer.print(Utilities.dumpRect(mStackRect)); 1264 writer.print(" task="); writer.print(Utilities.dumpRect(mTaskRect)); 1265 writer.print(" freeform="); writer.print(Utilities.dumpRect(mFreeformRect)); 1266 writer.print(" actionButton="); writer.print(Utilities.dumpRect(mStackActionButtonRect)); 1267 writer.println(); 1268 1269 writer.print(innerPrefix); 1270 writer.print("minScroll="); writer.print(mMinScrollP); 1271 writer.print(" maxScroll="); writer.print(mMaxScrollP); 1272 writer.print(" initialScroll="); writer.print(mInitialScrollP); 1273 writer.println(); 1274 1275 writer.print(innerPrefix); 1276 writer.print("focusState="); writer.print(mFocusState); 1277 writer.println(); 1278 1279 if (mTaskIndexOverrideMap.size() > 0) { 1280 for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) { 1281 int taskId = mTaskIndexOverrideMap.keyAt(i); 1282 float x = mTaskIndexMap.get(taskId); 1283 float overrideX = mTaskIndexOverrideMap.get(taskId, 0f); 1284 1285 writer.print(innerPrefix); 1286 writer.print("taskId= "); writer.print(taskId); 1287 writer.print(" x= "); writer.print(x); 1288 writer.print(" overrideX= "); writer.print(overrideX); 1289 writer.println(); 1290 } 1291 } 1292 } 1293 }