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