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
17package com.android.systemui.recents.views;
18
19import android.annotation.IntDef;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.graphics.Path;
24import android.graphics.Rect;
25import android.util.ArraySet;
26import android.util.MutableFloat;
27import android.util.SparseArray;
28import android.util.SparseIntArray;
29import android.view.ViewDebug;
30
31import com.android.systemui.R;
32import com.android.systemui.recents.Recents;
33import com.android.systemui.recents.RecentsActivityLaunchState;
34import com.android.systemui.recents.RecentsConfiguration;
35import com.android.systemui.recents.RecentsDebugFlags;
36import com.android.systemui.recents.misc.FreePathInterpolator;
37import com.android.systemui.recents.misc.SystemServicesProxy;
38import com.android.systemui.recents.misc.Utilities;
39import com.android.systemui.recents.model.Task;
40import com.android.systemui.recents.model.TaskStack;
41
42import java.io.PrintWriter;
43import java.lang.annotation.Retention;
44import java.lang.annotation.RetentionPolicy;
45import java.util.ArrayList;
46import java.util.List;
47
48/**
49 * Used to describe a visible range that can be normalized to [0, 1].
50 */
51class Range {
52    final float relativeMin;
53    final float relativeMax;
54    float origin;
55    float min;
56    float max;
57
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     */
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     */
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     */
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     */
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 */
111public 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 {
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         */
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         */
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         */
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 */
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
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     */
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     */
398    public void reset() {
399        mTaskIndexOverrideMap.clear();
400        setFocusState(getInitialFocusState());
401    }
402
403    /**
404     * Sets the system insets.
405     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
658    public void clearUnfocusedTaskOverrides() {
659        mTaskIndexOverrideMap.clear();
660    }
661
662    /**
663     * Updates this stack when a scroll happens.
664     *
665     */
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
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     */
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     */
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     */
732    public TaskViewTransform getFrontOfStackTransform() {
733        return mFrontOfStackTransform;
734    }
735
736    /**
737     * Returns the current stack state.
738     */
739    public StackState getStackState() {
740        return mState;
741    }
742
743    /**
744     * Returns whether this stack layout has been initialized.
745     */
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     */
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     */
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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}