blob: f599f524ec14e40994e2c37398f0d5f3f2179242 [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.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.FloatProperty;
import android.util.Log;
import android.util.Property;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import com.android.systemui.R;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsActivityLaunchState;
import com.android.systemui.recents.RecentsConfiguration;
import com.android.systemui.recents.RecentsDebugFlags;
import com.android.systemui.recents.misc.FreePathInterpolator;
import com.android.systemui.recents.misc.SystemServicesProxy;
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;
/**
* Used to describe a visible range that can be normalized to [0, 1].
*/
class Range {
final float relativeMin;
final float relativeMax;
float origin;
float min;
float max;
public Range(float relMin, float relMax) {
min = relativeMin = relMin;
max = relativeMax = relMax;
}
/**
* Offsets this range to a given absolute position.
*/
public void offset(float x) {
this.origin = x;
min = x + relativeMin;
max = x + relativeMax;
}
/**
* Returns x normalized to the range 0 to 1 such that 0 = min, 0.5 = origin and 1 = max
*
* @param x is an absolute value in the same domain as origin
*/
public float getNormalizedX(float x) {
if (x < origin) {
return 0.5f + 0.5f * (x - origin) / -relativeMin;
} else {
return 0.5f + 0.5f * (x - origin) / relativeMax;
}
}
/**
* Given a normalized {@param x} value in this range, projected onto the full range to get an
* absolute value about the given {@param origin}.
*/
public float getAbsoluteX(float normX) {
if (normX < 0.5f) {
return (normX - 0.5f) / 0.5f * -relativeMin;
} else {
return (normX - 0.5f) / 0.5f * relativeMax;
}
}
/**
* Returns whether a value at an absolute x would be within range.
*/
public boolean isInRange(float absX) {
return (absX >= Math.floor(min)) && (absX <= Math.ceil(max));
}
}
/**
* The layout logic for a TaskStackView. This layout can have two states focused and unfocused,
* and in the focused state, there is a task that is displayed more prominently in the stack.
*/
public class TaskStackLayoutAlgorithm {
private static final String TAG = "TaskStackViewLayoutAlgorithm";
private static final boolean DEBUG = false;
// The scale factor to apply to the user movement in the stack to unfocus it
private static final float UNFOCUS_MULTIPLIER = 0.8f;
// The various focus states
public static final float STATE_FOCUSED = 1f;
public static final float STATE_UNFOCUSED = 0f;
/**
* The various stack/freeform states.
*/
public static class StackState {
public static final StackState FREEFORM_ONLY = new StackState(1f, 0);
public static final StackState STACK_ONLY = new StackState(0f, 0);
public static final StackState SPLIT = new StackState(0.5f, 255);
public final float freeformHeightPct;
public final int freeformBackgroundAlpha;
/**
* @param freeformHeightPct the percentage of the stack height (not including paddings) to
* allocate to the freeform workspace
* @param freeformBackgroundAlpha the background alpha for the freeform workspace
*/
StackState(float freeformHeightPct, int freeformBackgroundAlpha) {
this.freeformHeightPct = freeformHeightPct;
this.freeformBackgroundAlpha = freeformBackgroundAlpha;
}
/**
* Resolves the stack state for the layout given a task stack.
*/
public static StackState getStackStateForStack(TaskStack stack) {
SystemServicesProxy ssp = Recents.getSystemServices();
boolean hasFreeformWorkspaces = ssp.hasFreeformWorkspaceSupport();
int taskCount = stack.getStackTaskCount();
int freeformCount = stack.getStackTaskFreeformCount();
int stackCount = taskCount - freeformCount;
if (hasFreeformWorkspaces && stackCount > 0 && freeformCount > 0) {
return SPLIT;
} else if (hasFreeformWorkspaces && freeformCount > 0) {
return FREEFORM_ONLY;
} else {
return STACK_ONLY;
}
}
/**
* Computes the freeform and stack rect for this state.
*
* @param freeformRectOut the freeform rect to be written out
* @param stackRectOut the stack rect, we only write out the top of the stack
* @param taskStackBounds the full rect that the freeform rect can take up
*/
public void computeRects(Rect freeformRectOut, Rect stackRectOut,
Rect taskStackBounds, int widthPadding, int heightPadding, int stackBottomOffset) {
int availableHeight = taskStackBounds.height() - stackBottomOffset;
int ffPaddedHeight = (int) (availableHeight * freeformHeightPct);
int ffHeight = Math.max(0, ffPaddedHeight - (2 * heightPadding));
freeformRectOut.set(taskStackBounds.left + widthPadding,
taskStackBounds.top + heightPadding,
taskStackBounds.right - widthPadding,
taskStackBounds.top + heightPadding + ffHeight);
stackRectOut.set(taskStackBounds.left + widthPadding,
taskStackBounds.top,
taskStackBounds.right - widthPadding,
taskStackBounds.bottom);
if (ffPaddedHeight > 0) {
stackRectOut.top += ffPaddedHeight;
} else {
stackRectOut.top += heightPadding;
}
}
}
/**
* A Property wrapper around the <code>focusState</code> functionality handled by the
* {@link TaskStackLayoutAlgorithm#setFocusState(float)} and
* {@link TaskStackLayoutAlgorithm#getFocusState()} methods.
*/
private static final Property<TaskStackLayoutAlgorithm, Float> FOCUS_STATE =
new FloatProperty<TaskStackLayoutAlgorithm>("focusState") {
@Override
public void setValue(TaskStackLayoutAlgorithm object, float value) {
object.setFocusState(value);
}
@Override
public Float get(TaskStackLayoutAlgorithm object) {
return object.getFocusState();
}
};
// 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;
private TaskStackView mStackView;
private Interpolator mLinearOutSlowInInterpolator;
private StackState mState = StackState.SPLIT;
// The task bounds (untransformed) for layout. This rect is anchored at mTaskRoot.
public Rect mTaskRect = new Rect();
// The freeform workspace bounds, inset from the top by the search bar, and is a fixed height
public Rect mFreeformRect = new Rect();
// The stack bounds, inset from the top by the search bar, and runs to
// the bottom of the screen
public Rect mStackRect = new Rect();
// This is the current system insets
public Rect mSystemInsets = new Rect();
// This is the bounds of the history button above the stack rect
public Rect mHistoryButtonRect = new Rect();
// The visible ranges when the stack is focused and unfocused
private Range mUnfocusedRange;
private Range mFocusedRange;
// The offset from the top when scrolled to the top of the stack
private int mFocusedPeekHeight;
// The offset from the top of the stack to the top of the bounds when the stack is scrolled to
// the end
private int mStackTopOffset;
// The offset from the bottom of the stack to the bottom of the bounds when the stack is
// scrolled to the front
private int mStackBottomOffset;
// The paths defining the motion of the tasks when the stack is focused and unfocused
private Path mUnfocusedCurve;
private Path mFocusedCurve;
private FreePathInterpolator mUnfocusedCurveInterpolator;
private FreePathInterpolator mFocusedCurveInterpolator;
// The state of the stack focus (0..1), which controls the transition of the stack from the
// focused to non-focused state
private float mFocusState;
// The animator used to reset the focused state
private ObjectAnimator mFocusStateAnimator;
// 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 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 last computed task counts
int mNumStackTasks;
int mNumFreeformTasks;
// The min/max z translations
int mMinTranslationZ;
int mMaxTranslationZ;
// Optimization, allows for quick lookup of task -> index
private HashMap<Task.TaskKey, Integer> mTaskIndexMap = new HashMap<>();
// The freeform workspace layout
FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
public TaskStackLayoutAlgorithm(Context context, TaskStackView stackView) {
Resources res = context.getResources();
mStackView = stackView;
mFocusedRange = new Range(res.getFloat(R.integer.recents_layout_focused_range_min),
res.getFloat(R.integer.recents_layout_focused_range_max));
mUnfocusedRange = new Range(res.getFloat(R.integer.recents_layout_unfocused_range_min),
res.getFloat(R.integer.recents_layout_unfocused_range_max));
mFocusState = getDefaultFocusState();
mFocusedPeekHeight = res.getDimensionPixelSize(R.dimen.recents_layout_focused_peek_size);
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();
mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
com.android.internal.R.interpolator.linear_out_slow_in);
}
/**
* Resets this layout when the stack view is reset.
*/
public void reset() {
setFocusState(getDefaultFocusState());
}
/**
* Sets the system insets.
*/
public void setSystemInsets(Rect systemInsets) {
mSystemInsets.set(systemInsets);
}
/**
* Sets the focused state.
*/
public void setFocusState(float focusState) {
mFocusState = focusState;
mStackView.requestSynchronizeStackViewsWithModel();
}
/**
* Gets the focused state.
*/
public float getFocusState() {
return mFocusState;
}
/**
* Computes the stack and task rects. The given task stack bounds is the whole bounds not
* including the search bar.
*/
public void initialize(Rect taskStackBounds, StackState state) {
RecentsDebugFlags debugFlags = Recents.getDebugFlags();
RecentsConfiguration config = Recents.getConfiguration();
int widthPadding = (int) (config.taskStackWidthPaddingPct * taskStackBounds.width());
int heightPadding = mContext.getResources().getDimensionPixelSize(
R.dimen.recents_stack_top_padding);
Rect lastStackRect = new Rect(mStackRect);
// The freeform height is the visible height (not including system insets) - padding above
// freeform and below stack - gap between the freeform and stack
mState = state;
mStackTopOffset = mFocusedPeekHeight + heightPadding;
mStackBottomOffset = mSystemInsets.bottom + heightPadding;
state.computeRects(mFreeformRect, mStackRect, taskStackBounds, widthPadding, heightPadding,
mStackBottomOffset);
mHistoryButtonRect.set(mStackRect.left, mStackRect.top - heightPadding,
mStackRect.right, mStackRect.top + mFocusedPeekHeight);
// Anchor the task rect to the top-center of the non-freeform stack rect
float aspect = (float) (taskStackBounds.width() - mSystemInsets.left - mSystemInsets.right)
/ (taskStackBounds.height() - mSystemInsets.bottom);
int width = mStackRect.width();
int minHeight = mStackRect.height() - mFocusedPeekHeight - mStackBottomOffset;
int height = debugFlags.isFullscreenThumbnailsEnabled()
? (int) Math.min(width / aspect, minHeight)
: width;
mTaskRect.set(mStackRect.left, mStackRect.top,
mStackRect.left + width, mStackRect.top + height);
// Short circuit here if the stack rects haven't changed so we don't do all the work below
if (lastStackRect.equals(mStackRect)) {
return;
}
// Reinitialize the focused and unfocused curves
mUnfocusedCurve = constructUnfocusedCurve();
mUnfocusedCurveInterpolator = new FreePathInterpolator(mUnfocusedCurve);
mFocusedCurve = constructFocusedCurve();
mFocusedCurveInterpolator = new FreePathInterpolator(mFocusedCurve);
if (DEBUG) {
Log.d(TAG, "initialize");
Log.d(TAG, "\tmFreeformRect: " + mFreeformRect);
Log.d(TAG, "\tmStackRect: " + mStackRect);
Log.d(TAG, "\tmTaskRect: " + mTaskRect);
Log.d(TAG, "\tmSystemInsets: " + mSystemInsets);
}
}
/**
* Computes the minimum and maximum scroll progress values and the progress values for each task
* in the stack.
*/
void update(TaskStack stack) {
SystemServicesProxy ssp = Recents.getSystemServices();
// Clear the progress map
mTaskIndexMap.clear();
// Return early if we have no tasks
ArrayList<Task> tasks = stack.getStackTasks();
if (tasks.isEmpty()) {
mFrontMostTaskP = 0;
mMinScrollP = mMaxScrollP = 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();
// Put each of the tasks in the progress map at a fixed index (does not need to actually
// map to a scroll position, just by index)
int taskCount = stackTasks.size();
for (int i = 0; i < taskCount; i++) {
Task task = stackTasks.get(i);
mTaskIndexMap.put(task.key, i);
}
// Calculate the min/max scroll
if (getDefaultFocusState() > 0f) {
mMinScrollP = 0;
mMaxScrollP = Math.max(mMinScrollP, mNumStackTasks - 1);
} else {
if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
mMinScrollP = mMaxScrollP = 0;
} else {
float bottomOffsetPct = (float) (mStackBottomOffset + mTaskRect.height()) /
mStackRect.height();
float normX = mUnfocusedCurveInterpolator.getX(bottomOffsetPct);
mMinScrollP = 0;
mMaxScrollP = Math.max(mMinScrollP,
(mNumStackTasks - 1) - Math.max(0, mUnfocusedRange.getAbsoluteX(normX)));
}
}
if (!freeformTasks.isEmpty()) {
mFreeformLayoutAlgorithm.update(freeformTasks, this);
mInitialScrollP = mMaxScrollP;
} else {
if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
mInitialScrollP = mMinScrollP;
} else if (getDefaultFocusState() > 0f) {
RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
if (launchState.launchedFromHome) {
mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 1);
} else {
mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 2);
}
} else {
float offsetPct = (float) (mTaskRect.height() / 2) / mStackRect.height();
float normX = mUnfocusedCurveInterpolator.getX(offsetPct);
mInitialScrollP = (mNumStackTasks - 1) - mUnfocusedRange.getAbsoluteX(normX);
}
}
if (DEBUG) {
Log.d(TAG, "mNumStackTasks: " + mNumStackTasks);
Log.d(TAG, "mNumFreeformTasks: " + mNumFreeformTasks);
Log.d(TAG, "mMinScrollP: " + mMinScrollP);
Log.d(TAG, "mMaxScrollP: " + mMaxScrollP);
}
}
/**
* Updates this stack when a scroll happens.
*/
public void updateFocusStateOnScroll(int yMovement) {
Utilities.cancelAnimationWithoutCallbacks(mFocusStateAnimator);
if (mFocusState > STATE_UNFOCUSED) {
float delta = (float) yMovement / (UNFOCUS_MULTIPLIER * mStackRect.height());
mFocusState -= Math.min(mFocusState, Math.abs(delta));
}
}
/**
* Aniamtes the focused state back to its orginal state.
*/
public void animateFocusState(float newState) {
Utilities.cancelAnimationWithoutCallbacks(mFocusStateAnimator);
if (Float.compare(newState, getFocusState()) != 0) {
mFocusStateAnimator = ObjectAnimator.ofFloat(this, FOCUS_STATE, getFocusState(),
newState);
mFocusStateAnimator.setDuration(mContext.getResources().getInteger(
R.integer.recents_animate_task_stack_scroll_duration));
mFocusStateAnimator.setInterpolator(mLinearOutSlowInInterpolator);
mFocusStateAnimator.start();
}
}
/**
* Returns the default focus state.
*/
public float getDefaultFocusState() {
RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
RecentsDebugFlags debugFlags = Recents.getDebugFlags();
if (launchState.launchedWithAltTab ||
(debugFlags.isPageOnToggleEnabled() && debugFlags.isInitialStatePaging())) {
return 1f;
}
return 0f;
}
/**
* Returns the task progress that would put the task just off the back of the stack.
*/
public float getStackBackTaskProgress(float stackScroll) {
float min = mUnfocusedRange.relativeMin +
mFocusState * (mFocusedRange.relativeMin - mUnfocusedRange.relativeMin);
return stackScroll + min;
}
/**
* Returns the task progress that would put the task just off the front of the stack.
*/
public float getStackFrontTaskProgress(float stackScroll) {
float max = mUnfocusedRange.relativeMax +
mFocusState * (mFocusedRange.relativeMax - mUnfocusedRange.relativeMax);
return stackScroll + max;
}
/**
*
* Returns the current stack state.
*/
public StackState getStackState() {
return mState;
}
/**
* Computes the maximum number of visible tasks and thumbnails when the scroll is at the initial
* stack scroll. 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);
}
// Quick return when there are no stack tasks
if (mNumStackTasks == 0) {
return new VisibilityReport(Math.max(mNumFreeformTasks, 1),
Math.max(mNumFreeformTasks, 1));
}
// Otherwise, walk backwards in the stack and count the number of tasks and visible
// thumbnails and add that to the total freeform task count
TaskViewTransform tmpTransform = new TaskViewTransform();
Range currentRange = getDefaultFocusState() > 0f ? mFocusedRange : mUnfocusedRange;
currentRange.offset(mInitialScrollP);
int taskBarHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.recents_task_bar_height);
int numVisibleTasks = Math.max(mNumFreeformTasks, 1);
int numVisibleThumbnails = Math.max(mNumFreeformTasks, 1);
float prevScreenY = Integer.MAX_VALUE;
for (int i = tasks.size() - 1; i >= 0; i--) {
Task task = tasks.get(i);
// Skip freeform
if (task.isFreeformTask()) {
continue;
}
// Skip invisible
float taskProgress = getStackScrollForTask(task);
if (!currentRange.isInRange(taskProgress)) {
continue;
}
boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task);
if (isFrontMostTaskInGroup) {
getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null);
float screenY = tmpTransform.rect.top;
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,
// walk through remaining visible set
for (int j = i; j >= 0; j--) {
numVisibleTasks++;
taskProgress = getStackScrollForTask(tasks.get(j));
if (!currentRange.isInRange(taskProgress)) {
continue;
}
}
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 frontTransform) {
if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
mFreeformLayoutAlgorithm.getTransform(task, transformOut, this);
return transformOut;
} else {
// Return early if we have an invalid index
if (task == null || !mTaskIndexMap.containsKey(task.key)) {
transformOut.reset();
return transformOut;
}
getStackTransform(mTaskIndexMap.get(task.key), stackScroll, transformOut,
frontTransform);
if (task.thumbnail != null) {
transformOut.thumbnailScale = (float) mTaskRect.width() / task.thumbnail.getWidth();
}
if (DEBUG) {
Log.d(TAG, "getTransform: " + task.key + ", " + transformOut);
}
return transformOut;
}
}
/** Update/get the transform */
public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
TaskViewTransform transformOut, TaskViewTransform frontTransform) {
SystemServicesProxy ssp = Recents.getSystemServices();
// Compute the focused and unfocused offset
mUnfocusedRange.offset(stackScroll);
float p = mUnfocusedRange.getNormalizedX(taskProgress);
float yp = mUnfocusedCurveInterpolator.getInterpolation(p);
float unfocusedP = p;
int unFocusedY = (int) (Math.max(0f, (1f - yp)) * mStackRect.height());
boolean unfocusedVisible = mUnfocusedRange.isInRange(taskProgress);
int focusedY = 0;
boolean focusedVisible = true;
if (mFocusState > 0f) {
mFocusedRange.offset(stackScroll);
p = mFocusedRange.getNormalizedX(taskProgress);
yp = mFocusedCurveInterpolator.getInterpolation(p);
focusedY = (int) (Math.max(0f, (1f - yp)) * mStackRect.height());
focusedVisible = mFocusedRange.isInRange(taskProgress);
}
// Skip if the task is not visible
if (!unfocusedVisible && !focusedVisible) {
transformOut.reset();
return transformOut;
}
int x = (mStackRect.width() - mTaskRect.width()) / 2;
int y;
float z;
float relP;
if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
// When there is exactly one task, then decouple the task from the stack and just move
// in screen space
p = (mMinScrollP - stackScroll) / mNumStackTasks;
int centerYOffset = (mStackRect.top - mTaskRect.top) +
(mStackRect.height() - mTaskRect.height()) / 2;
y = centerYOffset + getYForDeltaP(p, 0);
z = mMaxTranslationZ;
relP = 1f;
} else {
// Otherwise, update the task to the stack layout
y = unFocusedY + (int) (mFocusState * (focusedY - unFocusedY));
y += (mStackRect.top - mTaskRect.top);
z = Math.max(mMinTranslationZ, Math.min(mMaxTranslationZ,
mMinTranslationZ + (p * (mMaxTranslationZ - mMinTranslationZ))));
relP = unfocusedP;
}
// Fill out the transform
transformOut.scale = 1f;
transformOut.alpha = 1f;
transformOut.translationZ = z;
transformOut.rect.set(mTaskRect);
transformOut.rect.offset(x, y);
Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
transformOut.visible = (transformOut.rect.top < mStackRect.bottom) &&
(frontTransform == null || transformOut.rect.top != frontTransform.rect.top);
transformOut.thumbnailScale = 1f;
transformOut.p = relP;
return transformOut;
}
/**
* 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 (!mTaskIndexMap.containsKey(t.key)) return 0f;
return mTaskIndexMap.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() *
mUnfocusedCurveInterpolator.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() *
(1f / mUnfocusedCurveInterpolator.getArcLength()));
return -y;
}
/**
* Creates a new path for the focused curve.
*/
private Path constructFocusedCurve() {
int taskBarHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.recents_task_bar_height);
// Initialize the focused curve. This curve is a piecewise curve composed of several
// quadradic beziers that goes from (0,1) through (0.5, peek height offset),
// (0.667, next task offset), (0.833, bottom task offset), and (1,0).
float peekHeightPct = (float) mFocusedPeekHeight / mStackRect.height();
Path p = new Path();
p.moveTo(0f, 1f);
p.lineTo(0.5f, 1f - peekHeightPct);
p.lineTo(0.66666667f, (float) (taskBarHeight * 3) / mStackRect.height());
p.lineTo(0.83333333f, (float) (taskBarHeight / 2) / mStackRect.height());
p.lineTo(1f, 0f);
return p;
}
/**
* Creates a new path for the unfocused curve.
*/
private Path constructUnfocusedCurve() {
// Initialize the unfocused curve. This curve is a piecewise curve composed of two quadradic
// beziers that goes from (0,1) through (0.5, peek height offset) and ends at (1,0). This
// ensures that we match the range, at which 0.5 represents the stack scroll at the current
// task progress. Because the height offset can change depending on a resource, we compute
// the control point of the second bezier such that between it and a first known point,
// there is a tangent at (0.5, peek height offset).
float cpoint1X = 0.4f;
float cpoint1Y = 1f;
float peekHeightPct = (float) mFocusedPeekHeight / mStackRect.height();
float slope = ((1f - peekHeightPct) - cpoint1Y) / (0.5f - cpoint1X);
float b = 1f - slope * cpoint1X;
float cpoint2X = 0.75f;
float cpoint2Y = slope * cpoint2X + b;
Path p = new Path();
p.moveTo(0f, 1f);
p.cubicTo(0f, 1f, cpoint1X, 1f, 0.5f, 1f - peekHeightPct);
p.cubicTo(0.5f, 1f - peekHeightPct, cpoint2X, cpoint2Y, 1f, 0f);
return p;
}
}