/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.recents.views; import android.graphics.Rect; import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.Task; import java.util.ArrayList; import java.util.HashMap; /* The layout logic for a TaskStackView. * * We are using a curve that defines the curve of the tasks as that go back in the recents list. * The curve is defined such that at curve progress p = 0 is the end of the curve (the top of the * stack rect), and p = 1 at the start of the curve and the bottom of the stack rect. */ public class TaskStackViewLayoutAlgorithm { // These are all going to change static final float StackPeekMinScale = 0.8f; // The min scale of the last card in the peek area // A report of the visibility state of the stack public class VisibilityReport { public int numVisibleTasks; public int numVisibleThumbnails; /** Package level ctor */ VisibilityReport(int tasks, int thumbnails) { numVisibleTasks = tasks; numVisibleThumbnails = thumbnails; } } RecentsConfiguration mConfig; // The various rects that define the stack view Rect mViewRect = new Rect(); Rect mStackVisibleRect = new Rect(); Rect mStackRect = new Rect(); Rect mTaskRect = new Rect(); // The min/max scroll progress float mMinScrollP; float mMaxScrollP; float mInitialScrollP; int mWithinAffiliationOffset; int mBetweenAffiliationOffset; HashMap<Task.TaskKey, Float> mTaskProgressMap = new HashMap<Task.TaskKey, Float>(); // Log function static final float XScale = 1.75f; // The large the XScale, the longer the flat area of the curve static final float LogBase = 3000; static final int PrecisionSteps = 250; static float[] xp; static float[] px; public TaskStackViewLayoutAlgorithm(RecentsConfiguration config) { mConfig = config; // Precompute the path initializeCurve(); } /** Computes the stack and task rects */ public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds) { // Compute the stack rects mViewRect.set(0, 0, windowWidth, windowHeight); mStackRect.set(taskStackBounds); mStackVisibleRect.set(taskStackBounds); mStackVisibleRect.bottom = mViewRect.bottom; int widthPadding = (int) (mConfig.taskStackWidthPaddingPct * mStackRect.width()); int heightPadding = mConfig.taskStackTopPaddingPx; mStackRect.inset(widthPadding, heightPadding); // Compute the task rect int size = mStackRect.width(); int left = mStackRect.left + (mStackRect.width() - size) / 2; mTaskRect.set(left, mStackRect.top, left + size, mStackRect.top + size); // Update the affiliation offsets float visibleTaskPct = 0.5f; mWithinAffiliationOffset = mConfig.taskBarHeight; mBetweenAffiliationOffset = (int) (visibleTaskPct * mTaskRect.height()); } /** Computes the minimum and maximum scroll progress values. This method may be called before * the RecentsConfiguration is set, so we need to pass in the alt-tab state. */ void computeMinMaxScroll(ArrayList<Task> tasks, boolean launchedWithAltTab, boolean launchedFromHome) { // Clear the progress map mTaskProgressMap.clear(); // Return early if we have no tasks if (tasks.isEmpty()) { mMinScrollP = mMaxScrollP = 0; return; } // Note that we should account for the scale difference of the offsets at the screen bottom int taskHeight = mTaskRect.height(); float pAtBottomOfStackRect = screenYToCurveProgress(mStackVisibleRect.bottom); float pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom - mWithinAffiliationOffset); float scale = curveProgressToScale(pWithinAffiliateTop); int scaleYOffset = (int) (((1f - scale) * taskHeight) / 2); pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom - mWithinAffiliationOffset + scaleYOffset); float pWithinAffiliateOffset = pAtBottomOfStackRect - pWithinAffiliateTop; float pBetweenAffiliateOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - mBetweenAffiliationOffset); float pTaskHeightOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - taskHeight); float pNavBarOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom - mStackRect.bottom)); float pDismissAllButtonOffset = 0f; if (Constants.DebugFlags.App.EnableDismissAll) { pDismissAllButtonOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - mConfig.dismissAllButtonSizePx); } // Update the task offsets float pAtBackMostCardTop = 0.5f; float pAtFrontMostCardTop = pAtBackMostCardTop; int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); mTaskProgressMap.put(task.key, pAtFrontMostCardTop); if (i < (taskCount - 1)) { // Increment the peek height float pPeek = task.group.isFrontMostTask(task) ? pBetweenAffiliateOffset : pWithinAffiliateOffset; pAtFrontMostCardTop += pPeek; } } mMaxScrollP = pAtFrontMostCardTop + pDismissAllButtonOffset - ((1f - pTaskHeightOffset - pNavBarOffset)); mMinScrollP = tasks.size() == 1 ? Math.max(mMaxScrollP, 0f) : 0f; if (launchedWithAltTab && launchedFromHome) { // Center the top most task, since that will be focused first mInitialScrollP = mMaxScrollP; } else { mInitialScrollP = pAtFrontMostCardTop - 0.825f; } mInitialScrollP = Math.min(mMaxScrollP, Math.max(0, mInitialScrollP)); } /** * Computes the maximum number of visible tasks and thumbnails. Requires that * computeMinMaxScroll() is called first. */ public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) { if (tasks.size() <= 1) { return new VisibilityReport(1, 1); } // Walk backwards in the task stack and count the number of tasks and visible thumbnails int taskHeight = mTaskRect.height(); int numVisibleTasks = 1; int numVisibleThumbnails = 1; float progress = mTaskProgressMap.get(tasks.get(tasks.size() - 1).key) - mInitialScrollP; int prevScreenY = curveProgressToScreenY(progress); for (int i = tasks.size() - 2; i >= 0; i--) { Task task = tasks.get(i); progress = mTaskProgressMap.get(task.key) - mInitialScrollP; if (progress < 0) { break; } boolean isFrontMostTaskInGroup = task.group.isFrontMostTask(task); if (isFrontMostTaskInGroup) { float scaleAtP = curveProgressToScale(progress); int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2); int screenY = curveProgressToScreenY(progress) + scaleYOffsetAtP; boolean hasVisibleThumbnail = (prevScreenY - screenY) > mConfig.taskBarHeight; if (hasVisibleThumbnail) { numVisibleThumbnails++; numVisibleTasks++; prevScreenY = screenY; } else { // Once we hit the next front most task that does not have a visible thumbnail, // walk through remaining visible set for (int j = i; j >= 0; j--) { numVisibleTasks++; progress = mTaskProgressMap.get(tasks.get(j).key) - mInitialScrollP; if (progress < 0) { break; } } break; } } else if (!isFrontMostTaskInGroup) { // Affiliated task, no thumbnail numVisibleTasks++; } } return new VisibilityReport(numVisibleTasks, numVisibleThumbnails); } /** Update/get the transform */ public TaskViewTransform getStackTransform(Task task, float stackScroll, TaskViewTransform transformOut, TaskViewTransform prevTransform) { // Return early if we have an invalid index if (task == null || !mTaskProgressMap.containsKey(task.key)) { transformOut.reset(); return transformOut; } return getStackTransform(mTaskProgressMap.get(task.key), stackScroll, transformOut, prevTransform); } /** Update/get the transform */ public TaskViewTransform getStackTransform(float taskProgress, float stackScroll, TaskViewTransform transformOut, TaskViewTransform prevTransform) { float pTaskRelative = taskProgress - stackScroll; float pBounded = Math.max(0, Math.min(pTaskRelative, 1f)); // If the task top is outside of the bounds below the screen, then immediately reset it if (pTaskRelative > 1f) { transformOut.reset(); transformOut.rect.set(mTaskRect); return transformOut; } // The check for the top is trickier, since we want to show the next task if it is at all // visible, even if p < 0. if (pTaskRelative < 0f) { if (prevTransform != null && Float.compare(prevTransform.p, 0f) <= 0) { transformOut.reset(); transformOut.rect.set(mTaskRect); return transformOut; } } float scale = curveProgressToScale(pBounded); int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2); int minZ = mConfig.taskViewTranslationZMinPx; int maxZ = mConfig.taskViewTranslationZMaxPx; transformOut.scale = scale; transformOut.translationY = curveProgressToScreenY(pBounded) - mStackVisibleRect.top - scaleYOffset; transformOut.translationZ = Math.max(minZ, minZ + (pBounded * (maxZ - minZ))); transformOut.rect.set(mTaskRect); transformOut.rect.offset(0, transformOut.translationY); Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale); transformOut.visible = true; transformOut.p = pTaskRelative; return transformOut; } /** Returns the untransformed task view size. */ public Rect getUntransformedTaskViewSize() { Rect tvSize = new Rect(mTaskRect); tvSize.offsetTo(0, 0); return tvSize; } /** Returns the scroll to such task top = 1f; */ float getStackScrollForTask(Task t) { if (!mTaskProgressMap.containsKey(t.key)) return 0f; return mTaskProgressMap.get(t.key); } /** Initializes the curve. */ public static void initializeCurve() { if (xp != null && px != null) return; xp = new float[PrecisionSteps + 1]; px = new float[PrecisionSteps + 1]; // Approximate f(x) float[] fx = new float[PrecisionSteps + 1]; float step = 1f / PrecisionSteps; float x = 0; for (int xStep = 0; xStep <= PrecisionSteps; xStep++) { fx[xStep] = logFunc(x); x += step; } // Calculate the arc length for x:1->0 float pLength = 0; float[] dx = new float[PrecisionSteps + 1]; dx[0] = 0; for (int xStep = 1; xStep < PrecisionSteps; xStep++) { dx[xStep] = (float) Math.sqrt(Math.pow(fx[xStep] - fx[xStep - 1], 2) + Math.pow(step, 2)); pLength += dx[xStep]; } // Approximate p(x), a function of cumulative progress with x, normalized to 0..1 float p = 0; px[0] = 0f; px[PrecisionSteps] = 1f; for (int xStep = 1; xStep <= PrecisionSteps; xStep++) { p += Math.abs(dx[xStep] / pLength); px[xStep] = p; } // Given p(x), calculate the inverse function x(p). This assumes that x(p) is also a valid // function. int xStep = 0; p = 0; xp[0] = 0f; xp[PrecisionSteps] = 1f; for (int pStep = 0; pStep < PrecisionSteps; pStep++) { // Walk forward in px and find the x where px <= p && p < px+1 while (xStep < PrecisionSteps) { if (px[xStep] > p) break; xStep++; } // Now, px[xStep-1] <= p < px[xStep] if (xStep == 0) { xp[pStep] = 0; } else { // Find x such that proportionally, x is correct float fraction = (p - px[xStep - 1]) / (px[xStep] - px[xStep - 1]); x = (xStep - 1 + fraction) * step; xp[pStep] = x; } p += step; } } /** Reverses and scales out x. */ static float reverse(float x) { return (-x * XScale) + 1; } /** The log function describing the curve. */ static float logFunc(float x) { return 1f - (float) (Math.pow(LogBase, reverse(x))) / (LogBase); } /** The inverse of the log function describing the curve. */ float invLogFunc(float y) { return (float) (Math.log((1f - reverse(y)) * (LogBase - 1) + 1) / Math.log(LogBase)); } /** Converts from the progress along the curve to a screen coordinate. */ int curveProgressToScreenY(float p) { if (p < 0 || p > 1) return mStackVisibleRect.top + (int) (p * mStackVisibleRect.height()); float pIndex = p * PrecisionSteps; int pFloorIndex = (int) Math.floor(pIndex); int pCeilIndex = (int) Math.ceil(pIndex); float xFraction = 0; if (pFloorIndex < PrecisionSteps && (pCeilIndex != pFloorIndex)) { float pFraction = (pIndex - pFloorIndex) / (pCeilIndex - pFloorIndex); xFraction = (xp[pCeilIndex] - xp[pFloorIndex]) * pFraction; } float x = xp[pFloorIndex] + xFraction; return mStackVisibleRect.top + (int) (x * mStackVisibleRect.height()); } /** Converts from the progress along the curve to a scale. */ float curveProgressToScale(float p) { if (p < 0) return StackPeekMinScale; if (p > 1) return 1f; float scaleRange = (1f - StackPeekMinScale); float scale = StackPeekMinScale + (p * scaleRange); return scale; } /** Converts from a screen coordinate to the progress along the curve. */ float screenYToCurveProgress(int screenY) { float x = (float) (screenY - mStackVisibleRect.top) / mStackVisibleRect.height(); if (x < 0 || x > 1) return x; float xIndex = x * PrecisionSteps; int xFloorIndex = (int) Math.floor(xIndex); int xCeilIndex = (int) Math.ceil(xIndex); float pFraction = 0; if (xFloorIndex < PrecisionSteps && (xCeilIndex != xFloorIndex)) { float xFraction = (xIndex - xFloorIndex) / (xCeilIndex - xFloorIndex); pFraction = (px[xCeilIndex] - px[xFloorIndex]) * xFraction; } return px[xFloorIndex] + pFraction; } }