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