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