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