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.graphics.Rect;
20 import com.android.systemui.recents.RecentsConfiguration;
21 import com.android.systemui.recents.misc.Utilities;
22 import com.android.systemui.recents.model.Task;
23 
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 
27 /* The layout logic for a TaskStackView.
28  *
29  * We are using a curve that defines the curve of the tasks as that go back in the recents list.
30  * The curve is defined such that at curve progress p = 0 is the end of the curve (the top of the
31  * stack rect), and p = 1 at the start of the curve and the bottom of the stack rect.
32  */
33 public class TaskStackViewLayoutAlgorithm {
34 
35     // These are all going to change
36     static final float StackPeekMinScale = 0.8f; // The min scale of the last card in the peek area
37 
38     // A report of the visibility state of the stack
39     public class VisibilityReport {
40         public int numVisibleTasks;
41         public int numVisibleThumbnails;
42 
43         /** Package level ctor */
VisibilityReport(int tasks, int thumbnails)44         VisibilityReport(int tasks, int thumbnails) {
45             numVisibleTasks = tasks;
46             numVisibleThumbnails = thumbnails;
47         }
48     }
49 
50     RecentsConfiguration mConfig;
51 
52     // The various rects that define the stack view
53     Rect mViewRect = new Rect();
54     Rect mStackVisibleRect = new Rect();
55     Rect mStackRect = new Rect();
56     Rect mTaskRect = new Rect();
57 
58     // The min/max scroll progress
59     float mMinScrollP;
60     float mMaxScrollP;
61     float mInitialScrollP;
62     int mWithinAffiliationOffset;
63     int mBetweenAffiliationOffset;
64     HashMap<Task.TaskKey, Float> mTaskProgressMap = new HashMap<Task.TaskKey, Float>();
65 
66     // Log function
67     static final float XScale = 1.75f;  // The large the XScale, the longer the flat area of the curve
68     static final float LogBase = 3000;
69     static final int PrecisionSteps = 250;
70     static float[] xp;
71     static float[] px;
72 
TaskStackViewLayoutAlgorithm(RecentsConfiguration config)73     public TaskStackViewLayoutAlgorithm(RecentsConfiguration config) {
74         mConfig = config;
75 
76         // Precompute the path
77         initializeCurve();
78     }
79 
80     /** Computes the stack and task rects */
computeRects(int windowWidth, int windowHeight, Rect taskStackBounds)81     public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds) {
82         // Compute the stack rects
83         mViewRect.set(0, 0, windowWidth, windowHeight);
84         mStackRect.set(taskStackBounds);
85         mStackVisibleRect.set(taskStackBounds);
86         mStackVisibleRect.bottom = mViewRect.bottom;
87 
88         int widthPadding = (int) (mConfig.taskStackWidthPaddingPct * mStackRect.width());
89         int heightPadding = mConfig.taskStackTopPaddingPx;
90         mStackRect.inset(widthPadding, heightPadding);
91 
92         // Compute the task rect
93         int size = mStackRect.width();
94         int left = mStackRect.left + (mStackRect.width() - size) / 2;
95         mTaskRect.set(left, mStackRect.top,
96                 left + size, mStackRect.top + size);
97 
98         // Update the affiliation offsets
99         float visibleTaskPct = 0.5f;
100         mWithinAffiliationOffset = mConfig.taskBarHeight;
101         mBetweenAffiliationOffset = (int) (visibleTaskPct * mTaskRect.height());
102     }
103 
104     /** Computes the minimum and maximum scroll progress values.  This method may be called before
105      * the RecentsConfiguration is set, so we need to pass in the alt-tab state. */
computeMinMaxScroll(ArrayList<Task> tasks, boolean launchedWithAltTab, boolean launchedFromHome)106     void computeMinMaxScroll(ArrayList<Task> tasks, boolean launchedWithAltTab,
107             boolean launchedFromHome) {
108         // Clear the progress map
109         mTaskProgressMap.clear();
110 
111         // Return early if we have no tasks
112         if (tasks.isEmpty()) {
113             mMinScrollP = mMaxScrollP = 0;
114             return;
115         }
116 
117         // Note that we should account for the scale difference of the offsets at the screen bottom
118         int taskHeight = mTaskRect.height();
119         float pAtBottomOfStackRect = screenYToCurveProgress(mStackVisibleRect.bottom);
120         float pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom -
121                 mWithinAffiliationOffset);
122         float scale = curveProgressToScale(pWithinAffiliateTop);
123         int scaleYOffset = (int) (((1f - scale) * taskHeight) / 2);
124         pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom -
125                 mWithinAffiliationOffset + scaleYOffset);
126         float pWithinAffiliateOffset = pAtBottomOfStackRect - pWithinAffiliateTop;
127         float pBetweenAffiliateOffset = pAtBottomOfStackRect -
128                 screenYToCurveProgress(mStackVisibleRect.bottom - mBetweenAffiliationOffset);
129         float pTaskHeightOffset = pAtBottomOfStackRect -
130                 screenYToCurveProgress(mStackVisibleRect.bottom - taskHeight);
131         float pNavBarOffset = pAtBottomOfStackRect -
132                 screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom -
133                         mStackRect.bottom));
134 
135         // Update the task offsets
136         float pAtBackMostCardTop = 0.5f;
137         float pAtFrontMostCardTop = pAtBackMostCardTop;
138         int taskCount = tasks.size();
139         for (int i = 0; i < taskCount; i++) {
140             Task task = tasks.get(i);
141             mTaskProgressMap.put(task.key, pAtFrontMostCardTop);
142 
143             if (i < (taskCount - 1)) {
144                 // Increment the peek height
145                 float pPeek = task.group.isFrontMostTask(task) ?
146                         pBetweenAffiliateOffset : pWithinAffiliateOffset;
147                 pAtFrontMostCardTop += pPeek;
148             }
149         }
150 
151         mMaxScrollP = pAtFrontMostCardTop - ((1f - pTaskHeightOffset - pNavBarOffset));
152         mMinScrollP = tasks.size() == 1 ? Math.max(mMaxScrollP, 0f) : 0f;
153         if (launchedWithAltTab && launchedFromHome) {
154             // Center the top most task, since that will be focused first
155             mInitialScrollP = mMaxScrollP;
156         } else {
157             mInitialScrollP = pAtFrontMostCardTop - 0.825f;
158         }
159         mInitialScrollP = Math.min(mMaxScrollP, Math.max(0, mInitialScrollP));
160     }
161 
162     /**
163      * Computes the maximum number of visible tasks and thumbnails.  Requires that
164      * computeMinMaxScroll() is called first.
165      */
computeStackVisibilityReport(ArrayList<Task> tasks)166     public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) {
167         if (tasks.size() <= 1) {
168             return new VisibilityReport(1, 1);
169         }
170 
171         // Walk backwards in the task stack and count the number of tasks and visible thumbnails
172         int taskHeight = mTaskRect.height();
173         int numVisibleTasks = 1;
174         int numVisibleThumbnails = 1;
175         float progress = mTaskProgressMap.get(tasks.get(tasks.size() - 1).key) - mInitialScrollP;
176         int prevScreenY = curveProgressToScreenY(progress);
177         for (int i = tasks.size() - 2; i >= 0; i--) {
178             Task task = tasks.get(i);
179             progress = mTaskProgressMap.get(task.key) - mInitialScrollP;
180             if (progress < 0) {
181                 break;
182             }
183             boolean isFrontMostTaskInGroup = task.group.isFrontMostTask(task);
184             if (isFrontMostTaskInGroup) {
185                 float scaleAtP = curveProgressToScale(progress);
186                 int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2);
187                 int screenY = curveProgressToScreenY(progress) + scaleYOffsetAtP;
188                 boolean hasVisibleThumbnail = (prevScreenY - screenY) > mConfig.taskBarHeight;
189                 if (hasVisibleThumbnail) {
190                     numVisibleThumbnails++;
191                     numVisibleTasks++;
192                     prevScreenY = screenY;
193                 } else {
194                     // Once we hit the next front most task that does not have a visible thumbnail,
195                     // walk through remaining visible set
196                     for (int j = i; j >= 0; j--) {
197                         numVisibleTasks++;
198                         progress = mTaskProgressMap.get(tasks.get(j).key) - mInitialScrollP;
199                         if (progress < 0) {
200                             break;
201                         }
202                     }
203                     break;
204                 }
205             } else if (!isFrontMostTaskInGroup) {
206                 // Affiliated task, no thumbnail
207                 numVisibleTasks++;
208             }
209         }
210         return new VisibilityReport(numVisibleTasks, numVisibleThumbnails);
211     }
212 
213     /** Update/get the transform */
getStackTransform(Task task, float stackScroll, TaskViewTransform transformOut, TaskViewTransform prevTransform)214     public TaskViewTransform getStackTransform(Task task, float stackScroll,
215             TaskViewTransform transformOut, TaskViewTransform prevTransform) {
216         // Return early if we have an invalid index
217         if (task == null || !mTaskProgressMap.containsKey(task.key)) {
218             transformOut.reset();
219             return transformOut;
220         }
221         return getStackTransform(mTaskProgressMap.get(task.key), stackScroll, transformOut,
222                 prevTransform);
223     }
224 
225     /** Update/get the transform */
getStackTransform(float taskProgress, float stackScroll, TaskViewTransform transformOut, TaskViewTransform prevTransform)226     public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
227             TaskViewTransform transformOut, TaskViewTransform prevTransform) {
228         float pTaskRelative = taskProgress - stackScroll;
229         float pBounded = Math.max(0, Math.min(pTaskRelative, 1f));
230         // If the task top is outside of the bounds below the screen, then immediately reset it
231         if (pTaskRelative > 1f) {
232             transformOut.reset();
233             transformOut.rect.set(mTaskRect);
234             return transformOut;
235         }
236         // The check for the top is trickier, since we want to show the next task if it is at all
237         // visible, even if p < 0.
238         if (pTaskRelative < 0f) {
239             if (prevTransform != null && Float.compare(prevTransform.p, 0f) <= 0) {
240                 transformOut.reset();
241                 transformOut.rect.set(mTaskRect);
242                 return transformOut;
243             }
244         }
245         float scale = curveProgressToScale(pBounded);
246         int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2);
247         int minZ = mConfig.taskViewTranslationZMinPx;
248         int maxZ = mConfig.taskViewTranslationZMaxPx;
249         transformOut.scale = scale;
250         transformOut.translationY = curveProgressToScreenY(pBounded) - mStackVisibleRect.top -
251                 scaleYOffset;
252         transformOut.translationZ = Math.max(minZ, minZ + (pBounded * (maxZ - minZ)));
253         transformOut.rect.set(mTaskRect);
254         transformOut.rect.offset(0, transformOut.translationY);
255         Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
256         transformOut.visible = true;
257         transformOut.p = pTaskRelative;
258         return transformOut;
259     }
260 
261     /** Returns the untransformed task view size. */
getUntransformedTaskViewSize()262     public Rect getUntransformedTaskViewSize() {
263         Rect tvSize = new Rect(mTaskRect);
264         tvSize.offsetTo(0, 0);
265         return tvSize;
266     }
267 
268     /** Returns the scroll to such task top = 1f; */
getStackScrollForTask(Task t)269     float getStackScrollForTask(Task t) {
270         if (!mTaskProgressMap.containsKey(t.key)) return 0f;
271         return mTaskProgressMap.get(t.key);
272     }
273 
274     /** Initializes the curve. */
initializeCurve()275     public static void initializeCurve() {
276         if (xp != null && px != null) return;
277         xp = new float[PrecisionSteps + 1];
278         px = new float[PrecisionSteps + 1];
279 
280         // Approximate f(x)
281         float[] fx = new float[PrecisionSteps + 1];
282         float step = 1f / PrecisionSteps;
283         float x = 0;
284         for (int xStep = 0; xStep <= PrecisionSteps; xStep++) {
285             fx[xStep] = logFunc(x);
286             x += step;
287         }
288         // Calculate the arc length for x:1->0
289         float pLength = 0;
290         float[] dx = new float[PrecisionSteps + 1];
291         dx[0] = 0;
292         for (int xStep = 1; xStep < PrecisionSteps; xStep++) {
293             dx[xStep] = (float) Math.sqrt(Math.pow(fx[xStep] - fx[xStep - 1], 2) + Math.pow(step, 2));
294             pLength += dx[xStep];
295         }
296         // Approximate p(x), a function of cumulative progress with x, normalized to 0..1
297         float p = 0;
298         px[0] = 0f;
299         px[PrecisionSteps] = 1f;
300         for (int xStep = 1; xStep <= PrecisionSteps; xStep++) {
301             p += Math.abs(dx[xStep] / pLength);
302             px[xStep] = p;
303         }
304         // Given p(x), calculate the inverse function x(p). This assumes that x(p) is also a valid
305         // function.
306         int xStep = 0;
307         p = 0;
308         xp[0] = 0f;
309         xp[PrecisionSteps] = 1f;
310         for (int pStep = 0; pStep < PrecisionSteps; pStep++) {
311             // Walk forward in px and find the x where px <= p && p < px+1
312             while (xStep < PrecisionSteps) {
313                 if (px[xStep] > p) break;
314                 xStep++;
315             }
316             // Now, px[xStep-1] <= p < px[xStep]
317             if (xStep == 0) {
318                 xp[pStep] = 0;
319             } else {
320                 // Find x such that proportionally, x is correct
321                 float fraction = (p - px[xStep - 1]) / (px[xStep] - px[xStep - 1]);
322                 x = (xStep - 1 + fraction) * step;
323                 xp[pStep] = x;
324             }
325             p += step;
326         }
327     }
328 
329     /** Reverses and scales out x. */
reverse(float x)330     static float reverse(float x) {
331         return (-x * XScale) + 1;
332     }
333     /** The log function describing the curve. */
logFunc(float x)334     static float logFunc(float x) {
335         return 1f - (float) (Math.pow(LogBase, reverse(x))) / (LogBase);
336     }
337     /** The inverse of the log function describing the curve. */
invLogFunc(float y)338     float invLogFunc(float y) {
339         return (float) (Math.log((1f - reverse(y)) * (LogBase - 1) + 1) / Math.log(LogBase));
340     }
341 
342     /** Converts from the progress along the curve to a screen coordinate. */
curveProgressToScreenY(float p)343     int curveProgressToScreenY(float p) {
344         if (p < 0 || p > 1) return mStackVisibleRect.top + (int) (p * mStackVisibleRect.height());
345         float pIndex = p * PrecisionSteps;
346         int pFloorIndex = (int) Math.floor(pIndex);
347         int pCeilIndex = (int) Math.ceil(pIndex);
348         float xFraction = 0;
349         if (pFloorIndex < PrecisionSteps && (pCeilIndex != pFloorIndex)) {
350             float pFraction = (pIndex - pFloorIndex) / (pCeilIndex - pFloorIndex);
351             xFraction = (xp[pCeilIndex] - xp[pFloorIndex]) * pFraction;
352         }
353         float x = xp[pFloorIndex] + xFraction;
354         return mStackVisibleRect.top + (int) (x * mStackVisibleRect.height());
355     }
356 
357     /** Converts from the progress along the curve to a scale. */
curveProgressToScale(float p)358     float curveProgressToScale(float p) {
359         if (p < 0) return StackPeekMinScale;
360         if (p > 1) return 1f;
361         float scaleRange = (1f - StackPeekMinScale);
362         float scale = StackPeekMinScale + (p * scaleRange);
363         return scale;
364     }
365 
366     /** Converts from a screen coordinate to the progress along the curve. */
screenYToCurveProgress(int screenY)367     float screenYToCurveProgress(int screenY) {
368         float x = (float) (screenY - mStackVisibleRect.top) / mStackVisibleRect.height();
369         if (x < 0 || x > 1) return x;
370         float xIndex = x * PrecisionSteps;
371         int xFloorIndex = (int) Math.floor(xIndex);
372         int xCeilIndex = (int) Math.ceil(xIndex);
373         float pFraction = 0;
374         if (xFloorIndex < PrecisionSteps && (xCeilIndex != xFloorIndex)) {
375             float xFraction = (xIndex - xFloorIndex) / (xCeilIndex - xFloorIndex);
376             pFraction = (px[xCeilIndex] - px[xFloorIndex]) * xFraction;
377         }
378         return px[xFloorIndex] + pFraction;
379     }
380 }
381