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