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.Recents; 33 import com.android.systemui.recents.RecentsActivityLaunchState; 34 import com.android.systemui.recents.RecentsConfiguration; 35 import com.android.systemui.recents.RecentsDebugFlags; 36 import com.android.systemui.recents.misc.FreePathInterpolator; 37 import com.android.systemui.recents.misc.SystemServicesProxy; 38 import com.android.systemui.shared.recents.utilities.Utilities; 39 import com.android.systemui.shared.recents.model.Task; 40 import com.android.systemui.shared.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 Recents.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 = Recents.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 = Recents.getConfiguration().isLowRamDevice 498 ? mTaskStackLowRamLayoutAlgorithm.getMinScrollP() 499 : 0; 500 mMaxScrollP = Recents.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 (Recents.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 = Recents.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 || Recents.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 = Recents.getConfiguration().getLaunchState(); 669 RecentsDebugFlags debugFlags = Recents.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 (Recents.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 (Recents.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 : Recents.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 = Recents.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) Utilities.mapRange(focusState, unfocusedY, focusedY); 936 z = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedNonOverrideRangeX), 937 mMinTranslationZ, mMaxTranslationZ); 938 dimAlpha = Utilities.mapRange(focusState, unfocusedDim, focusedDim); 939 viewOutlineAlpha = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedRangeX), 940 OUTLINE_ALPHA_MIN_VALUE, OUTLINE_ALPHA_MAX_VALUE); 941 } 942 943 // Fill out the transform 944 transformOut.scale = 1f; 945 transformOut.alpha = 1f; 946 transformOut.translationZ = z; 947 transformOut.dimAlpha = dimAlpha; 948 transformOut.viewOutlineAlpha = viewOutlineAlpha; 949 transformOut.rect.set(mTaskRect); 950 transformOut.rect.offset(x, y); 951 Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale); 952 transformOut.visible = (transformOut.rect.top < mStackRect.bottom) && 953 (frontTransform == null || transformOut.rect.top != frontTransform.rect.top); 954 } 955 956 /** 957 * Returns the untransformed task view bounds. 958 */ getUntransformedTaskViewBounds()959 public Rect getUntransformedTaskViewBounds() { 960 return new Rect(mTaskRect); 961 } 962 963 /** 964 * Returns the scroll progress to scroll to such that the top of the task is at the top of the 965 * stack. 966 */ getStackScrollForTask(Task t)967 float getStackScrollForTask(Task t) { 968 Float overrideP = mTaskIndexOverrideMap.get(t.key.id, null); 969 if (Recents.getConfiguration().isLowRamDevice || overrideP == null) { 970 return (float) mTaskIndexMap.get(t.key.id, 0); 971 } 972 return overrideP; 973 } 974 975 /** 976 * Returns the original scroll progress to scroll to such that the top of the task is at the top 977 * of the stack. 978 */ getStackScrollForTaskIgnoreOverrides(Task t)979 float getStackScrollForTaskIgnoreOverrides(Task t) { 980 return (float) mTaskIndexMap.get(t.key.id, 0); 981 } 982 983 /** 984 * Returns the scroll progress to scroll to such that the top of the task at the initial top 985 * offset (which is at the task's brightest point). 986 */ getStackScrollForTaskAtInitialOffset(Task t)987 float getStackScrollForTaskAtInitialOffset(Task t) { 988 if (Recents.getConfiguration().isLowRamDevice) { 989 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); 990 return mTaskStackLowRamLayoutAlgorithm.getInitialScrollP(mNumStackTasks, 991 launchState.launchedFromHome || launchState.launchedFromPipApp 992 || launchState.launchedWithNextPipApp); 993 } 994 float normX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 995 mUnfocusedRange.offset(0f); 996 return Utilities.clamp((float) mTaskIndexMap.get(t.key.id, 0) - Math.max(0, 997 mUnfocusedRange.getAbsoluteX(normX)), mMinScrollP, mMaxScrollP); 998 } 999 1000 /** 1001 * Maps a movement in screen y, relative to {@param downY}, to a movement in along the arc 1002 * length of the curve. We know the curve is mostly flat, so we just map the length of the 1003 * screen along the arc-length proportionally (1/arclength). 1004 */ getDeltaPForY(int downY, int y)1005 public float getDeltaPForY(int downY, int y) { 1006 if (Recents.getConfiguration().isLowRamDevice) { 1007 return mTaskStackLowRamLayoutAlgorithm.scrollToPercentage(downY - y); 1008 } 1009 float deltaP = (float) (y - downY) / mStackRect.height() * 1010 mUnfocusedCurveInterpolator.getArcLength(); 1011 return -deltaP; 1012 } 1013 1014 /** 1015 * This is the inverse of {@link #getDeltaPForY}. Given a movement along the arc length 1016 * of the curve, map back to the screen y. 1017 */ getYForDeltaP(float downScrollP, float p)1018 public int getYForDeltaP(float downScrollP, float p) { 1019 if (Recents.getConfiguration().isLowRamDevice) { 1020 return mTaskStackLowRamLayoutAlgorithm.percentageToScroll(downScrollP - p); 1021 } 1022 int y = (int) ((p - downScrollP) * mStackRect.height() * 1023 (1f / mUnfocusedCurveInterpolator.getArcLength())); 1024 return -y; 1025 } 1026 1027 /** 1028 * Returns the task stack bounds in the current orientation. This rect takes into account the 1029 * top and right system insets (but not the bottom inset) and left/right paddings, but _not_ 1030 * the top/bottom padding or insets. 1031 */ getTaskStackBounds(Rect displayRect, Rect windowRect, int topInset, int leftInset, int rightInset, Rect taskStackBounds)1032 public void getTaskStackBounds(Rect displayRect, Rect windowRect, int topInset, int leftInset, 1033 int rightInset, Rect taskStackBounds) { 1034 taskStackBounds.set(windowRect.left + leftInset, windowRect.top + topInset, 1035 windowRect.right - rightInset, windowRect.bottom); 1036 1037 // Ensure that the new width is at most the smaller display edge size 1038 int sideMargin = getScaleForExtent(windowRect, displayRect, mBaseSideMargin, mMinMargin, 1039 WIDTH); 1040 int targetStackWidth = taskStackBounds.width() - 2 * sideMargin; 1041 if (Utilities.getAppConfiguration(mContext).orientation 1042 == Configuration.ORIENTATION_LANDSCAPE) { 1043 // If we are in landscape, calculate the width of the stack in portrait and ensure that 1044 // we are not larger than that size 1045 Rect portraitDisplayRect = new Rect(0, 0, 1046 Math.min(displayRect.width(), displayRect.height()), 1047 Math.max(displayRect.width(), displayRect.height())); 1048 int portraitSideMargin = getScaleForExtent(portraitDisplayRect, portraitDisplayRect, 1049 mBaseSideMargin, mMinMargin, WIDTH); 1050 targetStackWidth = Math.min(targetStackWidth, 1051 portraitDisplayRect.width() - 2 * portraitSideMargin); 1052 } 1053 taskStackBounds.inset((taskStackBounds.width() - targetStackWidth) / 2, 0); 1054 } 1055 1056 /** 1057 * Retrieves resources that are constant regardless of the current configuration of the device. 1058 */ getDimensionForDevice(Context ctx, int phoneResId, int tabletResId, int xlargeTabletResId, int gridLayoutResId)1059 public static int getDimensionForDevice(Context ctx, int phoneResId, 1060 int tabletResId, int xlargeTabletResId, int gridLayoutResId) { 1061 return getDimensionForDevice(ctx, phoneResId, phoneResId, tabletResId, tabletResId, 1062 xlargeTabletResId, xlargeTabletResId, gridLayoutResId); 1063 } 1064 1065 /** 1066 * Retrieves resources that are constant regardless of the current configuration of the device. 1067 */ getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId, int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId, int xlargeTabletLandResId, int gridLayoutResId)1068 public static int getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId, 1069 int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId, 1070 int xlargeTabletLandResId, int gridLayoutResId) { 1071 RecentsConfiguration config = Recents.getConfiguration(); 1072 Resources res = ctx.getResources(); 1073 boolean isLandscape = Utilities.getAppConfiguration(ctx).orientation == 1074 Configuration.ORIENTATION_LANDSCAPE; 1075 if (config.isGridEnabled) { 1076 return res.getDimensionPixelSize(gridLayoutResId); 1077 } else if (config.isXLargeScreen) { 1078 return res.getDimensionPixelSize(isLandscape 1079 ? xlargeTabletLandResId 1080 : xlargeTabletPortResId); 1081 } else if (config.isLargeScreen) { 1082 return res.getDimensionPixelSize(isLandscape 1083 ? tabletLandResId 1084 : tabletPortResId); 1085 } else { 1086 return res.getDimensionPixelSize(isLandscape 1087 ? phoneLandResId 1088 : phonePortResId); 1089 } 1090 } 1091 1092 /** 1093 * Returns the normalized x on the unfocused curve given an absolute Y position (relative to the 1094 * stack height). 1095 */ getNormalizedXFromUnfocusedY(float y, @AnchorSide int fromSide)1096 private float getNormalizedXFromUnfocusedY(float y, @AnchorSide int fromSide) { 1097 float offset = (fromSide == FROM_TOP) 1098 ? mStackRect.height() - y 1099 : y; 1100 float offsetPct = offset / mStackRect.height(); 1101 return mUnfocusedCurveInterpolator.getX(offsetPct); 1102 } 1103 1104 /** 1105 * Returns the normalized x on the focused curve given an absolute Y position (relative to the 1106 * stack height). 1107 */ getNormalizedXFromFocusedY(float y, @AnchorSide int fromSide)1108 private float getNormalizedXFromFocusedY(float y, @AnchorSide int fromSide) { 1109 float offset = (fromSide == FROM_TOP) 1110 ? mStackRect.height() - y 1111 : y; 1112 float offsetPct = offset / mStackRect.height(); 1113 return mFocusedCurveInterpolator.getX(offsetPct); 1114 } 1115 1116 /** 1117 * Creates a new path for the focused curve. 1118 */ constructFocusedCurve()1119 private Path constructFocusedCurve() { 1120 // Initialize the focused curve. This curve is a piecewise curve composed of several 1121 // linear pieces that goes from (0,1) through (0.5, peek height offset), 1122 // (0.5, bottom task offsets), and (1,0). 1123 float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); 1124 float bottomPeekHeightPct = (float) (mStackBottomOffset + mFocusedBottomPeekHeight) / 1125 mStackRect.height(); 1126 float minBottomPeekHeightPct = (float) (mFocusedTopPeekHeight + mTaskRect.height() - 1127 mMinMargin) / mStackRect.height(); 1128 Path p = new Path(); 1129 p.moveTo(0f, 1f); 1130 p.lineTo(0.5f, 1f - topPeekHeightPct); 1131 p.lineTo(1f - (0.5f / mFocusedRange.relativeMax), Math.max(1f - minBottomPeekHeightPct, 1132 bottomPeekHeightPct)); 1133 p.lineTo(1f, 0f); 1134 return p; 1135 } 1136 1137 /** 1138 * Creates a new path for the unfocused curve. 1139 */ constructUnfocusedCurve()1140 private Path constructUnfocusedCurve() { 1141 // Initialize the unfocused curve. This curve is a piecewise curve composed of two quadradic 1142 // beziers that goes from (0,1) through (0.5, peek height offset) and ends at (1,0). This 1143 // ensures that we match the range, at which 0.5 represents the stack scroll at the current 1144 // task progress. Because the height offset can change depending on a resource, we compute 1145 // the control point of the second bezier such that between it and a first known point, 1146 // there is a tangent at (0.5, peek height offset). 1147 float cpoint1X = 0.4f; 1148 float cpoint1Y = 0.975f; 1149 float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); 1150 float slope = ((1f - topPeekHeightPct) - cpoint1Y) / (0.5f - cpoint1X); 1151 float b = 1f - slope * cpoint1X; 1152 float cpoint2X = 0.65f; 1153 float cpoint2Y = slope * cpoint2X + b; 1154 Path p = new Path(); 1155 p.moveTo(0f, 1f); 1156 p.cubicTo(0f, 1f, cpoint1X, cpoint1Y, 0.5f, 1f - topPeekHeightPct); 1157 p.cubicTo(0.5f, 1f - topPeekHeightPct, cpoint2X, cpoint2Y, 1f, 0f); 1158 return p; 1159 } 1160 1161 /** 1162 * Creates a new path for the focused dim curve. 1163 */ constructFocusedDimCurve()1164 private Path constructFocusedDimCurve() { 1165 Path p = new Path(); 1166 // The focused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused 1167 // task), then goes back to max dim at the next task 1168 p.moveTo(0f, MAX_DIM); 1169 p.lineTo(0.5f, 0f); 1170 p.lineTo(0.5f + (0.5f / mFocusedRange.relativeMax), MAX_DIM); 1171 p.lineTo(1f, MAX_DIM); 1172 return p; 1173 } 1174 1175 /** 1176 * Creates a new path for the unfocused dim curve. 1177 */ constructUnfocusedDimCurve()1178 private Path constructUnfocusedDimCurve() { 1179 float focusX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 1180 float cpoint2X = focusX + (1f - focusX) / 2; 1181 Path p = new Path(); 1182 // The unfocused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused 1183 // task), then goes back to max dim towards the front of the stack 1184 p.moveTo(0f, MAX_DIM); 1185 p.cubicTo(focusX * 0.5f, MAX_DIM, focusX * 0.75f, MAX_DIM * 0.75f, focusX, 0f); 1186 p.cubicTo(cpoint2X, 0f, cpoint2X, MED_DIM, 1f, MED_DIM); 1187 return p; 1188 } 1189 1190 /** 1191 * Scales the given {@param value} to the scale of the {@param instance} rect relative to the 1192 * {@param other} rect in the {@param extent} side. 1193 */ getScaleForExtent(Rect instance, Rect other, int value, int minValue, @Extent int extent)1194 private int getScaleForExtent(Rect instance, Rect other, int value, int minValue, 1195 @Extent int extent) { 1196 if (extent == WIDTH) { 1197 float scale = Utilities.clamp01((float) instance.width() / other.width()); 1198 return Math.max(minValue, (int) (scale * value)); 1199 } else if (extent == HEIGHT) { 1200 float scale = Utilities.clamp01((float) instance.height() / other.height()); 1201 return Math.max(minValue, (int) (scale * value)); 1202 } 1203 return value; 1204 } 1205 1206 /** 1207 * Updates the current transforms that would put a TaskView at the front and back of the stack. 1208 */ updateFrontBackTransforms()1209 private void updateFrontBackTransforms() { 1210 // Return early if we have not yet initialized 1211 if (mStackRect.isEmpty()) { 1212 return; 1213 } 1214 if (Recents.getConfiguration().isLowRamDevice) { 1215 mTaskStackLowRamLayoutAlgorithm.getBackOfStackTransform(mBackOfStackTransform, this); 1216 mTaskStackLowRamLayoutAlgorithm.getFrontOfStackTransform(mFrontOfStackTransform, this); 1217 return; 1218 } 1219 1220 float min = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMin, 1221 mFocusedRange.relativeMin); 1222 float max = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMax, 1223 mFocusedRange.relativeMax); 1224 getStackTransform(min, min, 0f, mFocusState, mBackOfStackTransform, null, 1225 true /* ignoreSingleTaskCase */, true /* forceUpdate */); 1226 getStackTransform(max, max, 0f, mFocusState, mFrontOfStackTransform, null, 1227 true /* ignoreSingleTaskCase */, true /* forceUpdate */); 1228 mBackOfStackTransform.visible = true; 1229 mFrontOfStackTransform.visible = true; 1230 } 1231 1232 /** 1233 * Returns the proper task rectangle according to the current grid state. 1234 */ getTaskRect()1235 public Rect getTaskRect() { 1236 if (Recents.getConfiguration().isLowRamDevice) { 1237 return mTaskStackLowRamLayoutAlgorithm.getTaskRect(); 1238 } 1239 return useGridLayout() ? mTaskGridLayoutAlgorithm.getTaskGridRect() : mTaskRect; 1240 } 1241 dump(String prefix, PrintWriter writer)1242 public void dump(String prefix, PrintWriter writer) { 1243 String innerPrefix = prefix + " "; 1244 1245 writer.print(prefix); writer.print(TAG); 1246 writer.write(" numStackTasks="); writer.print(mNumStackTasks); 1247 writer.println(); 1248 1249 writer.print(innerPrefix); 1250 writer.print("insets="); writer.print(Utilities.dumpRect(mSystemInsets)); 1251 writer.print(" stack="); writer.print(Utilities.dumpRect(mStackRect)); 1252 writer.print(" task="); writer.print(Utilities.dumpRect(mTaskRect)); 1253 writer.print(" actionButton="); writer.print(Utilities.dumpRect(mStackActionButtonRect)); 1254 writer.println(); 1255 1256 writer.print(innerPrefix); 1257 writer.print("minScroll="); writer.print(mMinScrollP); 1258 writer.print(" maxScroll="); writer.print(mMaxScrollP); 1259 writer.print(" initialScroll="); writer.print(mInitialScrollP); 1260 writer.println(); 1261 1262 writer.print(innerPrefix); 1263 writer.print("focusState="); writer.print(mFocusState); 1264 writer.println(); 1265 1266 if (mTaskIndexOverrideMap.size() > 0) { 1267 for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) { 1268 int taskId = mTaskIndexOverrideMap.keyAt(i); 1269 float x = mTaskIndexMap.get(taskId); 1270 float overrideX = mTaskIndexOverrideMap.get(taskId, 0f); 1271 1272 writer.print(innerPrefix); 1273 writer.print("taskId= "); writer.print(taskId); 1274 writer.print(" x= "); writer.print(x); 1275 writer.print(" overrideX= "); writer.print(overrideX); 1276 writer.println(); 1277 } 1278 } 1279 } 1280 }