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