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