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