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.launchedFromBlacklistedApp) { 560 mInitialScrollP = mMaxScrollP; 561 } else if (launchState.launchedWithAltTab) { 562 mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); 563 } else if (scrollToFront) { 564 mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); 565 } else { 566 // We are overriding the initial two task positions, so set the initial scroll 567 // position to match the second task (aka focused task) position 568 float initialTopNormX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 569 mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, (mNumStackTasks - 2)) 570 - Math.max(0, mUnfocusedRange.getAbsoluteX(initialTopNormX))); 571 } 572 } 573 } 574 575 /** 576 * Creates task overrides to ensure the initial stack layout if necessary. 577 */ 578 public void setTaskOverridesForInitialState(TaskStack stack, boolean ignoreScrollToFront) { 579 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); 580 581 mTaskIndexOverrideMap.clear(); 582 583 boolean scrollToFront = launchState.launchedFromHome || 584 launchState.launchedFromBlacklistedApp || 585 launchState.launchedViaDockGesture; 586 if (getInitialFocusState() == STATE_UNFOCUSED && mNumStackTasks > 1) { 587 if (ignoreScrollToFront || (!launchState.launchedWithAltTab && !scrollToFront)) { 588 // Set the initial scroll to the predefined state (which differs from the stack) 589 float [] initialNormX = null; 590 float minBottomTaskNormX = getNormalizedXFromUnfocusedY(mSystemInsets.bottom + 591 mInitialBottomOffset, FROM_BOTTOM); 592 float maxBottomTaskNormX = getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight + 593 mTaskRect.height() - mMinMargin, FROM_TOP); 594 if (mNumStackTasks <= 2) { 595 // For small stacks, position the tasks so that they are top aligned to under 596 // the action button, but ensure that it is at least a certain offset from the 597 // bottom of the stack 598 initialNormX = new float[] { 599 Math.min(maxBottomTaskNormX, minBottomTaskNormX), 600 getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight, FROM_TOP) 601 }; 602 } else { 603 initialNormX = new float[] { 604 minBottomTaskNormX, 605 getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP) 606 }; 607 } 608 609 mUnfocusedRange.offset(0f); 610 List<Task> tasks = stack.getStackTasks(); 611 int taskCount = tasks.size(); 612 for (int i = taskCount - 1; i >= 0; i--) { 613 int indexFromFront = taskCount - i - 1; 614 if (indexFromFront >= initialNormX.length) { 615 break; 616 } 617 float newTaskProgress = mInitialScrollP + 618 mUnfocusedRange.getAbsoluteX(initialNormX[indexFromFront]); 619 mTaskIndexOverrideMap.put(tasks.get(i).key.id, newTaskProgress); 620 } 621 } 622 } 623 } 624 625 /** 626 * Adds and override task progress for the given task when transitioning from focused to 627 * unfocused state. 628 */ 629 public void addUnfocusedTaskOverride(Task task, float stackScroll) { 630 if (mFocusState != STATE_UNFOCUSED) { 631 mFocusedRange.offset(stackScroll); 632 mUnfocusedRange.offset(stackScroll); 633 float focusedRangeX = mFocusedRange.getNormalizedX(mTaskIndexMap.get(task.key.id)); 634 float focusedY = mFocusedCurveInterpolator.getInterpolation(focusedRangeX); 635 float unfocusedRangeX = mUnfocusedCurveInterpolator.getX(focusedY); 636 float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX); 637 if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) { 638 mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress); 639 } 640 } 641 } 642 643 /** 644 * Adds and override task progress for the given task when transitioning from focused to 645 * unfocused state. 646 */ 647 public void addUnfocusedTaskOverride(TaskView taskView, float stackScroll) { 648 mFocusedRange.offset(stackScroll); 649 mUnfocusedRange.offset(stackScroll); 650 651 Task task = taskView.getTask(); 652 int top = taskView.getTop() - mTaskRect.top; 653 float focusedRangeX = getNormalizedXFromFocusedY(top, FROM_TOP); 654 float unfocusedRangeX = getNormalizedXFromUnfocusedY(top, FROM_TOP); 655 float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX); 656 if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) { 657 mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress); 658 } 659 } 660 661 public void clearUnfocusedTaskOverrides() { 662 mTaskIndexOverrideMap.clear(); 663 } 664 665 /** 666 * Updates this stack when a scroll happens. 667 * 668 */ 669 public float updateFocusStateOnScroll(float lastTargetStackScroll, float targetStackScroll, 670 float lastStackScroll) { 671 if (targetStackScroll == lastStackScroll) { 672 return targetStackScroll; 673 } 674 675 float deltaScroll = targetStackScroll - lastStackScroll; 676 float deltaTargetScroll = targetStackScroll - lastTargetStackScroll; 677 float newScroll = targetStackScroll; 678 mUnfocusedRange.offset(targetStackScroll); 679 for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) { 680 int taskId = mTaskIndexOverrideMap.keyAt(i); 681 float x = mTaskIndexMap.get(taskId); 682 float overrideX = mTaskIndexOverrideMap.get(taskId, 0f); 683 float newOverrideX = overrideX + deltaScroll; 684 if (isInvalidOverrideX(x, overrideX, newOverrideX)) { 685 // Remove the override once we reach the original task index 686 mTaskIndexOverrideMap.removeAt(i); 687 } else if ((overrideX >= x && deltaScroll <= 0f) || 688 (overrideX <= x && deltaScroll >= 0f)) { 689 // Scrolling from override x towards x, then lock the task in place 690 mTaskIndexOverrideMap.put(taskId, newOverrideX); 691 } else { 692 // Scrolling override x away from x, we should still move the scroll towards x 693 newScroll = lastStackScroll; 694 newOverrideX = overrideX - deltaTargetScroll; 695 if (isInvalidOverrideX(x, overrideX, newOverrideX)) { 696 mTaskIndexOverrideMap.removeAt(i); 697 } else{ 698 mTaskIndexOverrideMap.put(taskId, newOverrideX); 699 } 700 } 701 } 702 return newScroll; 703 } 704 705 private boolean isInvalidOverrideX(float x, float overrideX, float newOverrideX) { 706 boolean outOfBounds = mUnfocusedRange.getNormalizedX(newOverrideX) < 0f || 707 mUnfocusedRange.getNormalizedX(newOverrideX) > 1f; 708 return outOfBounds || (overrideX >= x && x >= newOverrideX) || 709 (overrideX <= x && x <= newOverrideX); 710 } 711 712 /** 713 * Returns the default focus state. 714 */ 715 public int getInitialFocusState() { 716 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); 717 RecentsDebugFlags debugFlags = Recents.getDebugFlags(); 718 if (debugFlags.isPagingEnabled() || launchState.launchedWithAltTab) { 719 return STATE_FOCUSED; 720 } else { 721 return STATE_UNFOCUSED; 722 } 723 } 724 725 /** 726 * Returns the TaskViewTransform that would put the task just off the back of the stack. 727 */ 728 public TaskViewTransform getBackOfStackTransform() { 729 return mBackOfStackTransform; 730 } 731 732 /** 733 * Returns the TaskViewTransform that would put the task just off the front of the stack. 734 */ 735 public TaskViewTransform getFrontOfStackTransform() { 736 return mFrontOfStackTransform; 737 } 738 739 /** 740 * Returns the current stack state. 741 */ 742 public StackState getStackState() { 743 return mState; 744 } 745 746 /** 747 * Returns whether this stack layout has been initialized. 748 */ 749 public boolean isInitialized() { 750 return !mStackRect.isEmpty(); 751 } 752 753 /** 754 * Computes the maximum number of visible tasks and thumbnails when the scroll is at the initial 755 * stack scroll. Requires that update() is called first. 756 */ 757 public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) { 758 // Ensure minimum visibility count 759 if (tasks.size() <= 1) { 760 return new VisibilityReport(1, 1); 761 } 762 763 // Quick return when there are no stack tasks 764 if (mNumStackTasks == 0) { 765 return new VisibilityReport(Math.max(mNumFreeformTasks, 1), 766 Math.max(mNumFreeformTasks, 1)); 767 } 768 769 // Otherwise, walk backwards in the stack and count the number of tasks and visible 770 // thumbnails and add that to the total freeform task count 771 TaskViewTransform tmpTransform = new TaskViewTransform(); 772 Range currentRange = getInitialFocusState() > 0f ? mFocusedRange : mUnfocusedRange; 773 currentRange.offset(mInitialScrollP); 774 int taskBarHeight = mContext.getResources().getDimensionPixelSize( 775 R.dimen.recents_task_view_header_height); 776 int numVisibleTasks = Math.max(mNumFreeformTasks, 1); 777 int numVisibleThumbnails = Math.max(mNumFreeformTasks, 1); 778 float prevScreenY = Integer.MAX_VALUE; 779 for (int i = tasks.size() - 1; i >= 0; i--) { 780 Task task = tasks.get(i); 781 782 // Skip freeform 783 if (task.isFreeformTask()) { 784 continue; 785 } 786 787 // Skip invisible 788 float taskProgress = getStackScrollForTask(task); 789 if (!currentRange.isInRange(taskProgress)) { 790 continue; 791 } 792 793 boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task); 794 if (isFrontMostTaskInGroup) { 795 getStackTransform(taskProgress, taskProgress, mInitialScrollP, mFocusState, 796 tmpTransform, null, false /* ignoreSingleTaskCase */, 797 false /* forceUpdate */); 798 float screenY = tmpTransform.rect.top; 799 boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight; 800 if (hasVisibleThumbnail) { 801 numVisibleThumbnails++; 802 numVisibleTasks++; 803 prevScreenY = screenY; 804 } else { 805 // Once we hit the next front most task that does not have a visible thumbnail, 806 // walk through remaining visible set 807 for (int j = i; j >= 0; j--) { 808 numVisibleTasks++; 809 taskProgress = getStackScrollForTask(tasks.get(j)); 810 if (!currentRange.isInRange(taskProgress)) { 811 continue; 812 } 813 } 814 break; 815 } 816 } else if (!isFrontMostTaskInGroup) { 817 // Affiliated task, no thumbnail 818 numVisibleTasks++; 819 } 820 } 821 return new VisibilityReport(numVisibleTasks, numVisibleThumbnails); 822 } 823 824 /** 825 * Returns the transform for the given task. This transform is relative to the mTaskRect, which 826 * is what the view is measured and laid out with. 827 */ 828 public TaskViewTransform getStackTransform(Task task, float stackScroll, 829 TaskViewTransform transformOut, TaskViewTransform frontTransform) { 830 return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform, 831 false /* forceUpdate */, false /* ignoreTaskOverrides */); 832 } 833 834 public TaskViewTransform getStackTransform(Task task, float stackScroll, 835 TaskViewTransform transformOut, TaskViewTransform frontTransform, 836 boolean ignoreTaskOverrides) { 837 return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform, 838 false /* forceUpdate */, ignoreTaskOverrides); 839 } 840 841 public TaskViewTransform getStackTransform(Task task, float stackScroll, int focusState, 842 TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate, 843 boolean ignoreTaskOverrides) { 844 if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) { 845 mFreeformLayoutAlgorithm.getTransform(task, transformOut, this); 846 return transformOut; 847 } else { 848 // Return early if we have an invalid index 849 int nonOverrideTaskProgress = mTaskIndexMap.get(task.key.id, -1); 850 if (task == null || nonOverrideTaskProgress == -1) { 851 transformOut.reset(); 852 return transformOut; 853 } 854 float taskProgress = ignoreTaskOverrides 855 ? nonOverrideTaskProgress 856 : getStackScrollForTask(task); 857 getStackTransform(taskProgress, nonOverrideTaskProgress, stackScroll, focusState, 858 transformOut, frontTransform, false /* ignoreSingleTaskCase */, forceUpdate); 859 return transformOut; 860 } 861 } 862 863 /** 864 * Like {@link #getStackTransform}, but in screen coordinates 865 */ 866 public TaskViewTransform getStackTransformScreenCoordinates(Task task, float stackScroll, 867 TaskViewTransform transformOut, TaskViewTransform frontTransform, 868 Rect windowOverrideRect) { 869 TaskViewTransform transform = getStackTransform(task, stackScroll, mFocusState, 870 transformOut, frontTransform, true /* forceUpdate */, 871 false /* ignoreTaskOverrides */); 872 return transformToScreenCoordinates(transform, windowOverrideRect); 873 } 874 875 /** 876 * Transforms the given {@param transformOut} to the screen coordinates, overriding the current 877 * window rectangle with {@param windowOverrideRect} if non-null. 878 */ 879 public TaskViewTransform transformToScreenCoordinates(TaskViewTransform transformOut, 880 Rect windowOverrideRect) { 881 Rect windowRect = windowOverrideRect != null 882 ? windowOverrideRect 883 : Recents.getSystemServices().getWindowRect(); 884 transformOut.rect.offset(windowRect.left, windowRect.top); 885 return transformOut; 886 } 887 888 /** 889 * Update/get the transform. 890 * 891 * @param ignoreSingleTaskCase When set, will ensure that the transform computed does not take 892 * into account the special single-task case. This is only used 893 * internally to ensure that we can calculate the transform for any 894 * position in the stack. 895 */ 896 public void getStackTransform(float taskProgress, float nonOverrideTaskProgress, 897 float stackScroll, int focusState, TaskViewTransform transformOut, 898 TaskViewTransform frontTransform, boolean ignoreSingleTaskCase, boolean forceUpdate) { 899 SystemServicesProxy ssp = Recents.getSystemServices(); 900 901 // Ensure that the task is in range 902 mUnfocusedRange.offset(stackScroll); 903 mFocusedRange.offset(stackScroll); 904 boolean unfocusedVisible = mUnfocusedRange.isInRange(taskProgress); 905 boolean focusedVisible = mFocusedRange.isInRange(taskProgress); 906 907 // Skip if the task is not visible 908 if (!forceUpdate && !unfocusedVisible && !focusedVisible) { 909 transformOut.reset(); 910 return; 911 } 912 913 // Map the absolute task progress to the normalized x at the stack scroll. We use this to 914 // calculate positions along the curve. 915 mUnfocusedRange.offset(stackScroll); 916 mFocusedRange.offset(stackScroll); 917 float unfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); 918 float focusedRangeX = mFocusedRange.getNormalizedX(taskProgress); 919 920 // Map the absolute task progress to the normalized x at the bounded stack scroll. We use 921 // this to calculate bounded properties, like translationZ and outline alpha. 922 float boundedStackScroll = Utilities.clamp(stackScroll, mMinScrollP, mMaxScrollP); 923 mUnfocusedRange.offset(boundedStackScroll); 924 mFocusedRange.offset(boundedStackScroll); 925 float boundedScrollUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); 926 float boundedScrollUnfocusedNonOverrideRangeX = 927 mUnfocusedRange.getNormalizedX(nonOverrideTaskProgress); 928 929 // Map the absolute task progress to the normalized x at the upper bounded stack scroll. 930 // We use this to calculate the dim, which is bounded only on one end. 931 float lowerBoundedStackScroll = Utilities.clamp(stackScroll, -Float.MAX_VALUE, mMaxScrollP); 932 mUnfocusedRange.offset(lowerBoundedStackScroll); 933 mFocusedRange.offset(lowerBoundedStackScroll); 934 float lowerBoundedUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); 935 float lowerBoundedFocusedRangeX = mFocusedRange.getNormalizedX(taskProgress); 936 937 int x = (mStackRect.width() - mTaskRect.width()) / 2; 938 int y; 939 float z; 940 float dimAlpha; 941 float viewOutlineAlpha; 942 if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) { 943 // When there is exactly one task, then decouple the task from the stack and just move 944 // in screen space 945 float tmpP = (mMinScrollP - stackScroll) / mNumStackTasks; 946 int centerYOffset = (mStackRect.top - mTaskRect.top) + 947 (mStackRect.height() - mSystemInsets.bottom - mTaskRect.height()) / 2; 948 y = centerYOffset + getYForDeltaP(tmpP, 0); 949 z = mMaxTranslationZ; 950 dimAlpha = 0f; 951 viewOutlineAlpha = OUTLINE_ALPHA_MIN_VALUE + 952 (OUTLINE_ALPHA_MAX_VALUE - OUTLINE_ALPHA_MIN_VALUE) / 2f; 953 954 } else { 955 // Otherwise, update the task to the stack layout 956 int unfocusedY = (int) ((1f - mUnfocusedCurveInterpolator.getInterpolation( 957 unfocusedRangeX)) * mStackRect.height()); 958 int focusedY = (int) ((1f - mFocusedCurveInterpolator.getInterpolation( 959 focusedRangeX)) * mStackRect.height()); 960 float unfocusedDim = mUnfocusedDimCurveInterpolator.getInterpolation( 961 lowerBoundedUnfocusedRangeX); 962 float focusedDim = mFocusedDimCurveInterpolator.getInterpolation( 963 lowerBoundedFocusedRangeX); 964 965 // Special case, because we override the initial task positions differently for small 966 // stacks, we clamp the dim to 0 in the initial position, and then only modulate the 967 // dim when the task is scrolled back towards the top of the screen 968 if (mNumStackTasks <= 2 && nonOverrideTaskProgress == 0f) { 969 if (boundedScrollUnfocusedRangeX >= 0.5f) { 970 unfocusedDim = 0f; 971 } else { 972 float offset = mUnfocusedDimCurveInterpolator.getInterpolation(0.5f); 973 unfocusedDim -= offset; 974 unfocusedDim *= MAX_DIM / (MAX_DIM - offset); 975 } 976 } 977 978 y = (mStackRect.top - mTaskRect.top) + 979 (int) Utilities.mapRange(focusState, unfocusedY, focusedY); 980 z = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedNonOverrideRangeX), 981 mMinTranslationZ, mMaxTranslationZ); 982 dimAlpha = Utilities.mapRange(focusState, unfocusedDim, focusedDim); 983 viewOutlineAlpha = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedRangeX), 984 OUTLINE_ALPHA_MIN_VALUE, OUTLINE_ALPHA_MAX_VALUE); 985 } 986 987 // Fill out the transform 988 transformOut.scale = 1f; 989 transformOut.alpha = 1f; 990 transformOut.translationZ = z; 991 transformOut.dimAlpha = dimAlpha; 992 transformOut.viewOutlineAlpha = viewOutlineAlpha; 993 transformOut.rect.set(mTaskRect); 994 transformOut.rect.offset(x, y); 995 Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale); 996 transformOut.visible = (transformOut.rect.top < mStackRect.bottom) && 997 (frontTransform == null || transformOut.rect.top != frontTransform.rect.top); 998 } 999 1000 /** 1001 * Returns the untransformed task view bounds. 1002 */ 1003 public Rect getUntransformedTaskViewBounds() { 1004 return new Rect(mTaskRect); 1005 } 1006 1007 /** 1008 * Returns the scroll progress to scroll to such that the top of the task is at the top of the 1009 * stack. 1010 */ 1011 float getStackScrollForTask(Task t) { 1012 Float overrideP = mTaskIndexOverrideMap.get(t.key.id, null); 1013 if (overrideP == null) { 1014 return (float) mTaskIndexMap.get(t.key.id, 0); 1015 } 1016 return overrideP; 1017 } 1018 1019 /** 1020 * Returns the original scroll progress to scroll to such that the top of the task is at the top 1021 * of the stack. 1022 */ 1023 float getStackScrollForTaskIgnoreOverrides(Task t) { 1024 return (float) mTaskIndexMap.get(t.key.id, 0); 1025 } 1026 1027 /** 1028 * Returns the scroll progress to scroll to such that the top of the task at the initial top 1029 * offset (which is at the task's brightest point). 1030 */ 1031 float getStackScrollForTaskAtInitialOffset(Task t) { 1032 float normX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 1033 mUnfocusedRange.offset(0f); 1034 return Utilities.clamp((float) mTaskIndexMap.get(t.key.id, 0) - Math.max(0, 1035 mUnfocusedRange.getAbsoluteX(normX)), mMinScrollP, mMaxScrollP); 1036 } 1037 1038 /** 1039 * Maps a movement in screen y, relative to {@param downY}, to a movement in along the arc 1040 * length of the curve. We know the curve is mostly flat, so we just map the length of the 1041 * screen along the arc-length proportionally (1/arclength). 1042 */ 1043 public float getDeltaPForY(int downY, int y) { 1044 float deltaP = (float) (y - downY) / mStackRect.height() * 1045 mUnfocusedCurveInterpolator.getArcLength(); 1046 return -deltaP; 1047 } 1048 1049 /** 1050 * This is the inverse of {@link #getDeltaPForY}. Given a movement along the arc length 1051 * of the curve, map back to the screen y. 1052 */ 1053 public int getYForDeltaP(float downScrollP, float p) { 1054 int y = (int) ((p - downScrollP) * mStackRect.height() * 1055 (1f / mUnfocusedCurveInterpolator.getArcLength())); 1056 return -y; 1057 } 1058 1059 /** 1060 * Returns the task stack bounds in the current orientation. This rect takes into account the 1061 * top and right system insets (but not the bottom inset) and left/right paddings, but _not_ 1062 * the top/bottom padding or insets. 1063 */ 1064 public void getTaskStackBounds(Rect displayRect, Rect windowRect, int topInset, int leftInset, 1065 int rightInset, Rect taskStackBounds) { 1066 taskStackBounds.set(windowRect.left + leftInset, windowRect.top + topInset, 1067 windowRect.right - rightInset, windowRect.bottom); 1068 1069 // Ensure that the new width is at most the smaller display edge size 1070 int sideMargin = getScaleForExtent(windowRect, displayRect, mBaseSideMargin, mMinMargin, 1071 WIDTH); 1072 int targetStackWidth = taskStackBounds.width() - 2 * sideMargin; 1073 if (Utilities.getAppConfiguration(mContext).orientation 1074 == Configuration.ORIENTATION_LANDSCAPE) { 1075 // If we are in landscape, calculate the width of the stack in portrait and ensure that 1076 // we are not larger than that size 1077 Rect portraitDisplayRect = new Rect(0, 0, 1078 Math.min(displayRect.width(), displayRect.height()), 1079 Math.max(displayRect.width(), displayRect.height())); 1080 int portraitSideMargin = getScaleForExtent(portraitDisplayRect, portraitDisplayRect, 1081 mBaseSideMargin, mMinMargin, WIDTH); 1082 targetStackWidth = Math.min(targetStackWidth, 1083 portraitDisplayRect.width() - 2 * portraitSideMargin); 1084 } 1085 taskStackBounds.inset((taskStackBounds.width() - targetStackWidth) / 2, 0); 1086 } 1087 1088 /** 1089 * Retrieves resources that are constant regardless of the current configuration of the device. 1090 */ 1091 public static int getDimensionForDevice(Context ctx, int phoneResId, 1092 int tabletResId, int xlargeTabletResId) { 1093 return getDimensionForDevice(ctx, phoneResId, phoneResId, tabletResId, tabletResId, 1094 xlargeTabletResId, xlargeTabletResId); 1095 } 1096 1097 /** 1098 * Retrieves resources that are constant regardless of the current configuration of the device. 1099 */ 1100 public static int getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId, 1101 int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId, 1102 int xlargeTabletLandResId) { 1103 RecentsConfiguration config = Recents.getConfiguration(); 1104 Resources res = ctx.getResources(); 1105 boolean isLandscape = Utilities.getAppConfiguration(ctx).orientation == 1106 Configuration.ORIENTATION_LANDSCAPE; 1107 if (config.isXLargeScreen) { 1108 return res.getDimensionPixelSize(isLandscape 1109 ? xlargeTabletLandResId 1110 : xlargeTabletPortResId); 1111 } else if (config.isLargeScreen) { 1112 return res.getDimensionPixelSize(isLandscape 1113 ? tabletLandResId 1114 : tabletPortResId); 1115 } else { 1116 return res.getDimensionPixelSize(isLandscape 1117 ? phoneLandResId 1118 : phonePortResId); 1119 } 1120 } 1121 1122 /** 1123 * Returns the normalized x on the unfocused curve given an absolute Y position (relative to the 1124 * stack height). 1125 */ 1126 private float getNormalizedXFromUnfocusedY(float y, @AnchorSide int fromSide) { 1127 float offset = (fromSide == FROM_TOP) 1128 ? mStackRect.height() - y 1129 : y; 1130 float offsetPct = offset / mStackRect.height(); 1131 return mUnfocusedCurveInterpolator.getX(offsetPct); 1132 } 1133 1134 /** 1135 * Returns the normalized x on the focused curve given an absolute Y position (relative to the 1136 * stack height). 1137 */ 1138 private float getNormalizedXFromFocusedY(float y, @AnchorSide int fromSide) { 1139 float offset = (fromSide == FROM_TOP) 1140 ? mStackRect.height() - y 1141 : y; 1142 float offsetPct = offset / mStackRect.height(); 1143 return mFocusedCurveInterpolator.getX(offsetPct); 1144 } 1145 1146 /** 1147 * Creates a new path for the focused curve. 1148 */ 1149 private Path constructFocusedCurve() { 1150 // Initialize the focused curve. This curve is a piecewise curve composed of several 1151 // linear pieces that goes from (0,1) through (0.5, peek height offset), 1152 // (0.5, bottom task offsets), and (1,0). 1153 float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); 1154 float bottomPeekHeightPct = (float) (mStackBottomOffset + mFocusedBottomPeekHeight) / 1155 mStackRect.height(); 1156 float minBottomPeekHeightPct = (float) (mFocusedTopPeekHeight + mTaskRect.height() - 1157 mMinMargin) / mStackRect.height(); 1158 Path p = new Path(); 1159 p.moveTo(0f, 1f); 1160 p.lineTo(0.5f, 1f - topPeekHeightPct); 1161 p.lineTo(1f - (0.5f / mFocusedRange.relativeMax), Math.max(1f - minBottomPeekHeightPct, 1162 bottomPeekHeightPct)); 1163 p.lineTo(1f, 0f); 1164 return p; 1165 } 1166 1167 /** 1168 * Creates a new path for the unfocused curve. 1169 */ 1170 private Path constructUnfocusedCurve() { 1171 // Initialize the unfocused curve. This curve is a piecewise curve composed of two quadradic 1172 // beziers that goes from (0,1) through (0.5, peek height offset) and ends at (1,0). This 1173 // ensures that we match the range, at which 0.5 represents the stack scroll at the current 1174 // task progress. Because the height offset can change depending on a resource, we compute 1175 // the control point of the second bezier such that between it and a first known point, 1176 // there is a tangent at (0.5, peek height offset). 1177 float cpoint1X = 0.4f; 1178 float cpoint1Y = 0.975f; 1179 float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); 1180 float slope = ((1f - topPeekHeightPct) - cpoint1Y) / (0.5f - cpoint1X); 1181 float b = 1f - slope * cpoint1X; 1182 float cpoint2X = 0.65f; 1183 float cpoint2Y = slope * cpoint2X + b; 1184 Path p = new Path(); 1185 p.moveTo(0f, 1f); 1186 p.cubicTo(0f, 1f, cpoint1X, cpoint1Y, 0.5f, 1f - topPeekHeightPct); 1187 p.cubicTo(0.5f, 1f - topPeekHeightPct, cpoint2X, cpoint2Y, 1f, 0f); 1188 return p; 1189 } 1190 1191 /** 1192 * Creates a new path for the focused dim curve. 1193 */ 1194 private Path constructFocusedDimCurve() { 1195 Path p = new Path(); 1196 // The focused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused 1197 // task), then goes back to max dim at the next task 1198 p.moveTo(0f, MAX_DIM); 1199 p.lineTo(0.5f, 0f); 1200 p.lineTo(0.5f + (0.5f / mFocusedRange.relativeMax), MAX_DIM); 1201 p.lineTo(1f, MAX_DIM); 1202 return p; 1203 } 1204 1205 /** 1206 * Creates a new path for the unfocused dim curve. 1207 */ 1208 private Path constructUnfocusedDimCurve() { 1209 float focusX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); 1210 float cpoint2X = focusX + (1f - focusX) / 2; 1211 Path p = new Path(); 1212 // The unfocused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused 1213 // task), then goes back to max dim towards the front of the stack 1214 p.moveTo(0f, MAX_DIM); 1215 p.cubicTo(focusX * 0.5f, MAX_DIM, focusX * 0.75f, MAX_DIM * 0.75f, focusX, 0f); 1216 p.cubicTo(cpoint2X, 0f, cpoint2X, MED_DIM, 1f, MED_DIM); 1217 return p; 1218 } 1219 1220 /** 1221 * Scales the given {@param value} to the scale of the {@param instance} rect relative to the 1222 * {@param other} rect in the {@param extent} side. 1223 */ 1224 private int getScaleForExtent(Rect instance, Rect other, int value, int minValue, 1225 @Extent int extent) { 1226 if (extent == WIDTH) { 1227 float scale = Utilities.clamp01((float) instance.width() / other.width()); 1228 return Math.max(minValue, (int) (scale * value)); 1229 } else if (extent == HEIGHT) { 1230 float scale = Utilities.clamp01((float) instance.height() / other.height()); 1231 return Math.max(minValue, (int) (scale * value)); 1232 } 1233 return value; 1234 } 1235 1236 /** 1237 * Updates the current transforms that would put a TaskView at the front and back of the stack. 1238 */ 1239 private void updateFrontBackTransforms() { 1240 // Return early if we have not yet initialized 1241 if (mStackRect.isEmpty()) { 1242 return; 1243 } 1244 1245 float min = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMin, 1246 mFocusedRange.relativeMin); 1247 float max = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMax, 1248 mFocusedRange.relativeMax); 1249 getStackTransform(min, min, 0f, mFocusState, mBackOfStackTransform, null, 1250 true /* ignoreSingleTaskCase */, true /* forceUpdate */); 1251 getStackTransform(max, max, 0f, mFocusState, mFrontOfStackTransform, null, 1252 true /* ignoreSingleTaskCase */, true /* forceUpdate */); 1253 mBackOfStackTransform.visible = true; 1254 mFrontOfStackTransform.visible = true; 1255 } 1256 1257 public void dump(String prefix, PrintWriter writer) { 1258 String innerPrefix = prefix + " "; 1259 1260 writer.print(prefix); writer.print(TAG); 1261 writer.write(" numStackTasks="); writer.print(mNumStackTasks); 1262 writer.println(); 1263 1264 writer.print(innerPrefix); 1265 writer.print("insets="); writer.print(Utilities.dumpRect(mSystemInsets)); 1266 writer.print(" stack="); writer.print(Utilities.dumpRect(mStackRect)); 1267 writer.print(" task="); writer.print(Utilities.dumpRect(mTaskRect)); 1268 writer.print(" freeform="); writer.print(Utilities.dumpRect(mFreeformRect)); 1269 writer.print(" actionButton="); writer.print(Utilities.dumpRect(mStackActionButtonRect)); 1270 writer.println(); 1271 1272 writer.print(innerPrefix); 1273 writer.print("minScroll="); writer.print(mMinScrollP); 1274 writer.print(" maxScroll="); writer.print(mMaxScrollP); 1275 writer.print(" initialScroll="); writer.print(mInitialScrollP); 1276 writer.println(); 1277 1278 writer.print(innerPrefix); 1279 writer.print("focusState="); writer.print(mFocusState); 1280 writer.println(); 1281 1282 if (mTaskIndexOverrideMap.size() > 0) { 1283 for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) { 1284 int taskId = mTaskIndexOverrideMap.keyAt(i); 1285 float x = mTaskIndexMap.get(taskId); 1286 float overrideX = mTaskIndexOverrideMap.get(taskId, 0f); 1287 1288 writer.print(innerPrefix); 1289 writer.print("taskId= "); writer.print(taskId); 1290 writer.print(" x= "); writer.print(x); 1291 writer.print(" overrideX= "); writer.print(overrideX); 1292 writer.println(); 1293 } 1294 } 1295 } 1296 }