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 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 */ 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 */ 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 */ 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 */ 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 { 145 void onFocusStateChanged(int prevFocusState, int curFocusState); 146 } 147 148 /** 149 * @return True if we should use the grid layout. 150 */ 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 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 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 */ 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 */ 340 public void reset() { 341 mTaskIndexOverrideMap.clear(); 342 setFocusState(getInitialFocusState()); 343 } 344 345 /** 346 * Sets the system insets. 347 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 613 public void clearUnfocusedTaskOverrides() { 614 mTaskIndexOverrideMap.clear(); 615 } 616 617 /** 618 * Updates this stack when a scroll happens. 619 * 620 */ 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 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 */ 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 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 */ 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 */ 692 public TaskViewTransform getFrontOfStackTransform() { 693 return mFrontOfStackTransform; 694 } 695 696 /** 697 * Returns whether this stack layout has been initialized. 698 */ 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 */ 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 */ 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 1235 public Rect getTaskRect() { 1236 if (Recents.getConfiguration().isLowRamDevice) { 1237 return mTaskStackLowRamLayoutAlgorithm.getTaskRect(); 1238 } 1239 return useGridLayout() ? mTaskGridLayoutAlgorithm.getTaskGridRect() : mTaskRect; 1240 } 1241 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 }