blob: ff02e03cd335854986e33a09abaac48b90d9fe40 [file] [log] [blame]
/*
* 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.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.Log;
import com.android.systemui.R;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsConfiguration;
import com.android.systemui.recents.misc.ParametricCurve;
import com.android.systemui.recents.misc.Utilities;
import com.android.systemui.recents.model.Task;
import com.android.systemui.recents.model.TaskStack;
import java.util.ArrayList;
import java.util.HashMap;
/**
* The layout logic for a TaskStackView.
*/
public class TaskStackLayoutAlgorithm {
private static final String TAG = "TaskStackViewLayoutAlgorithm";
private static final boolean DEBUG = false;
// The min scale of the last task at the top of the curve
private static final float STACK_PEEK_MIN_SCALE = 0.85f;
// The scale of the last task
private static final float SINGLE_TASK_SCALE = 0.95f;
// The percentage of height of task to show between tasks
private static final float VISIBLE_TASK_HEIGHT_BETWEEN_TASKS = 0.5f;
// The percentage between the maxStackScroll and the maxScroll where a given scroll will still
// snap back to the maxStackScroll instead of to the maxScroll (which shows the freeform
// workspace)
private static final float SNAP_TO_MAX_STACK_SCROLL_FACTOR = 0.3f;
// 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;
}
}
Context mContext;
// This is the view bounds inset exactly by the search bar, but without the bottom inset
// see RecentsConfiguration.getTaskStackBounds()
public Rect mStackRect = new Rect();
// This is the task view bounds for layout (untransformed), the rect is top-aligned to the top
// of the stack rect
public Rect mTaskRect = new Rect();
// The bounds of the freeform workspace, the rect is top-aligned to the top of the stack rect
public Rect mFreeformRect = new Rect();
// This is the current system insets
public Rect mSystemInsets = new Rect();
// The smallest scroll progress, at this value, the back most task will be visible
float mMinScrollP;
// The largest scroll progress, at this value, the front most task will be visible above the
// navigation bar
float mMaxScrollP;
// The scroll progress at which bottom of the first task of the stack is aligned with the bottom
// of the stack
float mStackEndScrollP;
// The scroll progress that we actually want to scroll the user to when they want to go to the
// end of the stack (it accounts for the nav bar, so that the bottom of the task is offset from
// the bottom of the stack)
float mPreferredStackEndScrollP;
// The initial progress that the scroller is set when you first enter recents
float mInitialScrollP;
// The task progress for the front-most task in the stack
float mFrontMostTaskP;
// The relative progress to ensure that the height between affiliated tasks is respected
float mWithinAffiliationPOffset;
// The relative progress to ensure that the height between non-affiliated tasks is
// respected
float mBetweenAffiliationPOffset;
// The relative progress to ensure that the task height is respected
float mTaskHeightPOffset;
// The relative progress to ensure that the half task height is respected
float mTaskHalfHeightPOffset;
// The front-most task bottom offset
int mStackBottomOffset;
// The relative progress to ensure that the offset from the bottom of the stack to the bottom
// of the task is respected
float mStackBottomPOffset;
// The freeform workspace gap
int mFreeformWorkspaceGapOffset;
float mFreeformWorkspaceGapPOffset;
// The relative progress to ensure that the freeform workspace height + gap + stack bottom
// padding is respected
int mFreeformWorkspaceOffset;
float mFreeformWorkspacePOffset;
// The last computed task counts
int mNumStackTasks;
int mNumFreeformTasks;
// The min/max z translations
int mMinTranslationZ;
int mMaxTranslationZ;
// Optimization, allows for quick lookup of task -> progress
HashMap<Task.TaskKey, Float> mTaskProgressMap = new HashMap<>();
// The freeform workspace layout
FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
// Temporary task view transform
TaskViewTransform mTmpTransform = new TaskViewTransform();
// Log function
static ParametricCurve sCurve;
public TaskStackLayoutAlgorithm(Context context) {
Resources res = context.getResources();
mMinTranslationZ = res.getDimensionPixelSize(R.dimen.recents_task_view_z_min);
mMaxTranslationZ = res.getDimensionPixelSize(R.dimen.recents_task_view_z_max);
mContext = context;
mFreeformLayoutAlgorithm = new FreeformWorkspaceLayoutAlgorithm();
if (sCurve == null) {
sCurve = new ParametricCurve(new ParametricCurve.CurveFunction() {
// The large the XScale, the longer the flat area of the curve
private static final float XScale = 1.75f;
private static final float LogBase = 3000;
float reverse(float x) {
return (-x * XScale) + 1;
}
@Override
public float f(float x) {
return 1f - (float) (Math.pow(LogBase, reverse(x))) / (LogBase);
}
@Override
public float invF(float y) {
return (float) (Math.log(1f - reverse(y)) / (-Math.log(LogBase) * XScale));
}
}, new ParametricCurve.ParametricCurveFunction() {
@Override
public float f(float p) {
// Don't scale when there are freeform tasks
if (mNumFreeformTasks > 0) {
return 1f;
}
if (p < 0) return STACK_PEEK_MIN_SCALE;
if (p > 1) return 1f;
float scaleRange = (1f - STACK_PEEK_MIN_SCALE);
float scale = STACK_PEEK_MIN_SCALE + (p * scaleRange);
return scale;
}
});
}
}
/**
* Sets the system insets.
*/
public void setSystemInsets(Rect systemInsets) {
mSystemInsets.set(systemInsets);
if (DEBUG) {
Log.d(TAG, "setSystemInsets: " + systemInsets);
}
}
/**
* Computes the stack and task rects.
*/
public void initialize(Rect taskStackBounds) {
RecentsConfiguration config = Recents.getConfiguration();
int widthPadding = (int) (config.taskStackWidthPaddingPct * taskStackBounds.width());
int heightPadding = mContext.getResources().getDimensionPixelSize(
R.dimen.recents_stack_top_padding);
// Compute the stack rect, inset from the given task stack bounds
mStackRect.set(taskStackBounds.left + widthPadding, taskStackBounds.top + heightPadding,
taskStackBounds.right - widthPadding, taskStackBounds.bottom);
mStackBottomOffset = mSystemInsets.bottom + heightPadding;
// Compute the task rect, align it to the top-center square in the stack rect
int size = Math.min(mStackRect.width(), mStackRect.height() - mStackBottomOffset);
int xOffset = (mStackRect.width() - size) / 2;
mTaskRect.set(mStackRect.left + xOffset, mStackRect.top,
mStackRect.right - xOffset, mStackRect.top + size);
// Compute the freeform rect, align it to the top-left of the stack rect
mFreeformRect.set(mStackRect);
mFreeformRect.bottom = taskStackBounds.bottom - mStackBottomOffset;
// Compute the progress offsets
int withinAffiliationOffset = mContext.getResources().getDimensionPixelSize(
R.dimen.recents_task_bar_height);
int betweenAffiliationOffset = (int) (VISIBLE_TASK_HEIGHT_BETWEEN_TASKS * mTaskRect.height());
mWithinAffiliationPOffset = sCurve.computePOffsetForScaledHeight(withinAffiliationOffset,
mStackRect);
mBetweenAffiliationPOffset = sCurve.computePOffsetForScaledHeight(betweenAffiliationOffset,
mStackRect);
mTaskHeightPOffset = sCurve.computePOffsetForScaledHeight(mTaskRect.height(),
mStackRect);
mTaskHalfHeightPOffset = sCurve.computePOffsetForScaledHeight(mTaskRect.height() / 2,
mStackRect);
mStackBottomPOffset = sCurve.computePOffsetForHeight(mStackBottomOffset, mStackRect);
mFreeformWorkspaceGapOffset = mStackBottomOffset;
mFreeformWorkspaceGapPOffset = sCurve.computePOffsetForHeight(mFreeformWorkspaceGapOffset,
mStackRect);
mFreeformWorkspaceOffset = mFreeformWorkspaceGapOffset + mFreeformRect.height() +
mStackBottomOffset;
mFreeformWorkspacePOffset = sCurve.computePOffsetForHeight(mFreeformWorkspaceOffset,
mStackRect);
if (DEBUG) {
Log.d(TAG, "initialize");
Log.d(TAG, "\tarclength: " + sCurve.getArcLength());
Log.d(TAG, "\tmStackRect: " + mStackRect);
Log.d(TAG, "\tmTaskRect: " + mTaskRect);
Log.d(TAG, "\tmSystemInsets: " + mSystemInsets);
Log.d(TAG, "\tpWithinAffiliateOffset: " + mWithinAffiliationPOffset);
Log.d(TAG, "\tpBetweenAffiliateOffset: " + mBetweenAffiliationPOffset);
Log.d(TAG, "\tmTaskHeightPOffset: " + mTaskHeightPOffset);
Log.d(TAG, "\tmTaskHalfHeightPOffset: " + mTaskHalfHeightPOffset);
Log.d(TAG, "\tmStackBottomPOffset: " + mStackBottomPOffset);
Log.d(TAG, "\tmFreeformWorkspacePOffset: " + mFreeformWorkspacePOffset);
Log.d(TAG, "\tmFreeformWorkspaceGapPOffset: " + mFreeformWorkspaceGapPOffset);
Log.d(TAG, "\ty at p=0: " + sCurve.pToX(0f, mStackRect));
Log.d(TAG, "\ty at p=1: " + sCurve.pToX(1f, mStackRect));
for (int height = 0; height <= 2000; height += 50) {
float p = sCurve.computePOffsetForScaledHeight(height, mStackRect);
float p2 = sCurve.computePOffsetForHeight(height, mStackRect);
Log.d(TAG, "offset: " + height + ", " +
p + " => " + (mStackRect.bottom - sCurve.pToX(1f - p, mStackRect)) /
sCurve.pToScale(1f - p) + ", " +
p2 + " => " + (mStackRect.bottom - sCurve.pToX(1f - p2, mStackRect)));
}
}
}
/**
* Computes the minimum and maximum scroll progress values and the progress values for each task
* in the stack.
*/
void update(TaskStack stack) {
if (DEBUG) {
Log.d(TAG, "update");
}
// Clear the progress map
mTaskProgressMap.clear();
// Return early if we have no tasks
ArrayList<Task> tasks = stack.getTasks();
if (tasks.isEmpty()) {
mFrontMostTaskP = 0;
mMinScrollP = mMaxScrollP = mStackEndScrollP = mPreferredStackEndScrollP = 0;
mNumStackTasks = mNumFreeformTasks = 0;
return;
}
// Filter the set of freeform and stack tasks
ArrayList<Task> freeformTasks = new ArrayList<>();
ArrayList<Task> stackTasks = new ArrayList<>();
for (int i = 0; i < tasks.size(); i++) {
Task task = tasks.get(i);
if (task.isFreeformTask()) {
freeformTasks.add(task);
} else {
stackTasks.add(task);
}
}
mNumStackTasks = stackTasks.size();
mNumFreeformTasks = freeformTasks.size();
if (!stackTasks.isEmpty()) {
// Update the for each task from back to front.
float pAtBackMostTaskTop = 0;
float pAtFrontMostTaskTop = pAtBackMostTaskTop;
int taskCount = stackTasks.size();
for (int i = 0; i < taskCount; i++) {
Task task = stackTasks.get(i);
mTaskProgressMap.put(task.key, pAtFrontMostTaskTop);
if (i < (taskCount - 1)) {
// Increment the peek height
float pPeek = task.group.isFrontMostTask(task) ?
mBetweenAffiliationPOffset : mWithinAffiliationPOffset;
pAtFrontMostTaskTop += pPeek;
}
}
mFrontMostTaskP = pAtFrontMostTaskTop;
// Set the stack end scroll progress to the point at which the bottom of the front-most
// task is aligned to the bottom of the stack
mStackEndScrollP = alignToStackBottom(pAtFrontMostTaskTop,
mTaskHeightPOffset);
// Set the preferred stack end scroll progress to the point where the bottom of the
// front-most task is offset by the navbar and padding from the bottom of the stack
mPreferredStackEndScrollP = mStackEndScrollP + mStackBottomPOffset;
// Basically align the back-most task such that its progress is the same as the top of
// the front most task at the max stack scroll
mMinScrollP = alignToStackBottom(pAtBackMostTaskTop,
mStackBottomPOffset + mTaskHeightPOffset);
} else {
// TODO: In the case where there is only freeform tasks, then the scrolls should be
// set to zero
}
if (!freeformTasks.isEmpty()) {
// The max scroll includes the freeform workspace offset. As the scroll progress exceeds
// mStackEndScrollP up to mMaxScrollP, the stack will translate upwards and the freeform
// workspace will be visible
mFreeformLayoutAlgorithm.update(freeformTasks, this);
mMaxScrollP = mStackEndScrollP + mFreeformWorkspacePOffset;
mInitialScrollP = isInitialStateFreeform(stack) ?
mMaxScrollP : mPreferredStackEndScrollP;
} else {
mMaxScrollP = mPreferredStackEndScrollP;
mInitialScrollP = Math.max(mMinScrollP, mMaxScrollP - mTaskHalfHeightPOffset);
}
if (DEBUG) {
Log.d(TAG, "mNumStackTasks: " + mNumStackTasks);
Log.d(TAG, "mNumFreeformTasks: " + mNumFreeformTasks);
Log.d(TAG, "mMinScrollP: " + mMinScrollP);
Log.d(TAG, "mStackEndScrollP: " + mStackEndScrollP);
Log.d(TAG, "mMaxScrollP: " + mMaxScrollP);
}
}
/**
* Computes the maximum number of visible tasks and thumbnails. Requires that
* update() is called first.
*/
public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) {
// Ensure minimum visibility count
if (tasks.size() <= 1) {
return new VisibilityReport(1, 1);
}
// If there are freeform tasks, then they will be the only ones visible
int freeformTaskCount = 0;
for (Task t : tasks) {
if (t.isFreeformTask()) {
freeformTaskCount++;
}
}
if (freeformTaskCount > 0) {
return new VisibilityReport(freeformTaskCount, freeformTaskCount);
}
// Otherwise, walk backwards in the stack and count the number of tasks and visible
// thumbnails
int taskHeight = mTaskRect.height();
int taskBarHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.recents_task_bar_height);
int numVisibleTasks = 1;
int numVisibleThumbnails = 1;
float progress = mTaskProgressMap.get(tasks.get(tasks.size() - 1).key) - mInitialScrollP;
int prevScreenY = sCurve.pToX(progress, mStackRect);
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 = sCurve.pToScale(progress);
int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2);
int screenY = sCurve.pToX(progress, mStackRect) + scaleYOffsetAtP;
boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight;
if (hasVisibleThumbnail) {
numVisibleThumbnails++;
numVisibleTasks++;
prevScreenY = screenY;
} else {
// Once we hit the next front most task that does not have a visible thumbnail,
// w alk 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);
}
/**
* Returns the transform for the given task. This transform is relative to the mTaskRect, which
* is what the view is measured and laid out with.
*/
public TaskViewTransform getStackTransform(Task task, float stackScroll,
TaskViewTransform transformOut, TaskViewTransform prevTransform) {
if (mFreeformLayoutAlgorithm.isTransformAvailable(task, stackScroll, this)) {
mFreeformLayoutAlgorithm.getTransform(task, stackScroll, transformOut, this);
if (transformOut.visible) {
getFreeformWorkspaceBounds(stackScroll, mTmpTransform);
transformOut.translationY += mTmpTransform.translationY;
transformOut.translationZ = mMaxTranslationZ;
transformOut.rect.set(mTaskRect);
transformOut.rect.offset(0, transformOut.translationY);
Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
transformOut.p = 0;
}
return transformOut;
} else {
// 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) {
if (mNumStackTasks == 1) {
// Center the task in the stack, changing the scale will not follow the curve, but just
// modulate some values directly
float pTaskRelative = mMinScrollP - stackScroll;
float scale = SINGLE_TASK_SCALE;
int topOffset = (mStackRect.height() - mTaskRect.height()) / 2;
transformOut.scale = scale;
transformOut.translationX = 0;
transformOut.translationY = (int) (topOffset + (pTaskRelative * mStackRect.height()));
transformOut.translationZ = mMaxTranslationZ;
transformOut.rect.set(mTaskRect);
transformOut.rect.offset(0, transformOut.translationY);
Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
transformOut.visible = true;
transformOut.p = pTaskRelative;
return transformOut;
} else {
// Once we scroll past the preferred stack end scroll, then we should start translating
// the cards in screen space and lock their final state at the end stack progress
int overscrollYOffset = 0;
if (mNumFreeformTasks > 0 && stackScroll > mStackEndScrollP) {
float stackOverscroll = (stackScroll - mPreferredStackEndScrollP) /
(mFreeformWorkspacePOffset - mFreeformWorkspaceGapPOffset);
overscrollYOffset = (int) (Math.max(0, stackOverscroll) *
(mFreeformWorkspaceOffset - mFreeformWorkspaceGapPOffset));
stackScroll = Math.min(mPreferredStackEndScrollP, stackScroll);
}
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 = sCurve.pToScale(pBounded);
int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2);
transformOut.scale = scale;
transformOut.translationX = 0;
transformOut.translationY = sCurve.pToX(pBounded, mStackRect) - mStackRect.top -
scaleYOffset - overscrollYOffset;
transformOut.translationZ = Math.max(mMinTranslationZ,
mMinTranslationZ + (pBounded * (mMaxTranslationZ - mMinTranslationZ)));
transformOut.rect.set(mTaskRect);
transformOut.rect.offset(0, transformOut.translationY);
Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
transformOut.visible = true;
transformOut.p = pTaskRelative;
if (DEBUG) {
Log.d(TAG, "getStackTransform (normal): " + taskProgress + ", " + stackScroll);
Log.d(TAG, "\t" + transformOut);
}
return transformOut;
}
}
/**
* Returns whether this stack should be initialized to show the freeform workspace or not.
*/
public boolean isInitialStateFreeform(TaskStack stack) {
Task launchTarget = stack.getLaunchTarget();
if (launchTarget != null) {
return launchTarget.isFreeformTask();
}
Task frontTask = stack.getFrontMostTask();
if (frontTask != null) {
return frontTask.isFreeformTask();
}
return false;
}
/**
* Update/get the transform
*/
public TaskViewTransform getFreeformWorkspaceBounds(float stackScroll,
TaskViewTransform transformOut) {
transformOut.reset();
if (mNumFreeformTasks == 0) {
return transformOut;
}
if (stackScroll > mStackEndScrollP) {
// mStackEndScroll is the point at which the first stack task is bottom aligned with the
// stack, so we offset from on the stack rect height.
float stackOverscroll = (Math.max(0, stackScroll - mStackEndScrollP)) /
mFreeformWorkspacePOffset;
int overscrollYOffset = (int) (stackOverscroll * mFreeformWorkspaceOffset);
transformOut.scale = 1f;
transformOut.alpha = 1f;
transformOut.translationY = mStackRect.height() + mFreeformWorkspaceGapOffset -
overscrollYOffset;
transformOut.rect.set(mFreeformRect);
transformOut.rect.offset(0, transformOut.translationY);
Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
transformOut.visible = true;
}
return transformOut;
}
/**
* Returns the preferred maximum scroll position for a stack at the given {@param scroll}.
*/
public float getPreferredMaxScrollPosition(float scroll) {
float maxStackScrollBounds = mStackEndScrollP + SNAP_TO_MAX_STACK_SCROLL_FACTOR *
(mMaxScrollP - mStackEndScrollP);
if (scroll < maxStackScrollBounds) {
return mPreferredStackEndScrollP;
}
return mMaxScrollP;
}
/**
* Returns the untransformed task view bounds.
*/
public Rect getUntransformedTaskViewBounds() {
return new Rect(mTaskRect);
}
/**
* Returns the scroll progress to scroll to such that the top of the task is at the top of the
* stack.
*/
float getStackScrollForTask(Task t) {
if (!mTaskProgressMap.containsKey(t.key)) return 0f;
return mTaskProgressMap.get(t.key);
}
/**
* Maps a movement in screen y, relative to {@param downY}, to a movement in along the arc
* length of the curve. We know the curve is mostly flat, so we just map the length of the
* screen along the arc-length proportionally (1/arclength).
*/
public float getDeltaPForY(int downY, int y) {
float deltaP = (float) (y - downY) / mStackRect.height() * (1f / sCurve.getArcLength());
return -deltaP;
}
/**
* This is the inverse of {@link #getDeltaPForY}. Given a movement along the arc length
* of the curve, map back to the screen y.
*/
public int getYForDeltaP(float downScrollP, float p) {
int y = (int) ((p - downScrollP) * mStackRect.height() * sCurve.getArcLength());
return -y;
}
private float alignToStackTop(float p) {
// At scroll progress == p, then p is at the top of the stack
return p;
}
private float alignToStackBottom(float p, float pOffsetFromBottom) {
// At scroll progress == p, then p is at the top of the stack
// At scroll progress == p + 1, then p is at the bottom of the stack
return p - (1 - pOffsetFromBottom);
}
}