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 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 */ 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 */ 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 */ 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 */ 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 { 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 */ 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 */ 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 */ 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 */ 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 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 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 */ 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 */ 421 public void reset() { 422 mTaskIndexOverrideMap.clear(); 423 setFocusState(getInitialFocusState()); 424 } 425 426 /** 427 * Sets the system insets. 428 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 691 public void clearUnfocusedTaskOverrides() { 692 mTaskIndexOverrideMap.clear(); 693 } 694 695 /** 696 * Updates this stack when a scroll happens. 697 * 698 */ 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 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 */ 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 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 */ 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 */ 770 public TaskViewTransform getFrontOfStackTransform() { 771 return mFrontOfStackTransform; 772 } 773 774 /** 775 * Returns the current stack state. 776 */ 777 public StackState getStackState() { 778 return mState; 779 } 780 781 /** 782 * Returns whether this stack layout has been initialized. 783 */ 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 */ 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 */ 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 1311 public Rect getTaskRect() { 1312 return useGridLayout() ? mTaskGridLayoutAlgorithm.getTaskGridRect() : mTaskRect; 1313 } 1314 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 }