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