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