/* * 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.annotation.IntDef; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Path; import android.graphics.Rect; import android.util.ArraySet; import android.util.MutableFloat; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.ViewDebug; 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.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** * 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 needs to be able to calculate the stack layout * without an activity-specific context only with the information passed in. 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 = "TaskStackLayoutAlgorithm"; // The distribution of view bounds alpha // XXX: This is a hack because you can currently set the max alpha to be > 1f public static final float OUTLINE_ALPHA_MIN_VALUE = 0f; public static final float OUTLINE_ALPHA_MAX_VALUE = 2f; // The medium/maximum dim on the tasks private static final float MED_DIM = 0.15f; private static final float MAX_DIM = 0.25f; // The various focus states public static final int STATE_FOCUSED = 1; public static final int STATE_UNFOCUSED = 0; // The side that an offset is anchored @Retention(RetentionPolicy.SOURCE) @IntDef({FROM_TOP, FROM_BOTTOM}) public @interface AnchorSide {} private static final int FROM_TOP = 0; private static final int FROM_BOTTOM = 1; // The extent that we care about when calculating fractions @Retention(RetentionPolicy.SOURCE) @IntDef({WIDTH, HEIGHT}) public @interface Extent {} private static final int WIDTH = 0; private static final int HEIGHT = 1; public interface TaskStackLayoutAlgorithmCallbacks { void onFocusStateChanged(int prevFocusState, int curFocusState); } /** * The various stack/freeform states. */ public static class StackState { public static final StackState FREEFORM_ONLY = new StackState(1f, 255); 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 */ private 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 freeformCount = stack.getFreeformTaskCount(); int stackCount = stack.getStackTaskCount(); 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 topMargin, int freeformGap, int stackBottomOffset) { // The freeform height is the visible height (not including system insets) - padding // above freeform and below stack - gap between the freeform and stack int availableHeight = taskStackBounds.height() - topMargin - stackBottomOffset; int ffPaddedHeight = (int) (availableHeight * freeformHeightPct); int ffHeight = Math.max(0, ffPaddedHeight - freeformGap); freeformRectOut.set(taskStackBounds.left, taskStackBounds.top + topMargin, taskStackBounds.right, taskStackBounds.top + topMargin + ffHeight); stackRectOut.set(taskStackBounds.left, taskStackBounds.top, taskStackBounds.right, taskStackBounds.bottom); if (ffPaddedHeight > 0) { stackRectOut.top += ffPaddedHeight; } else { stackRectOut.top += topMargin; } } } // 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 StackState mState = StackState.SPLIT; private TaskStackLayoutAlgorithmCallbacks mCb; // The task bounds (untransformed) for layout. This rect is anchored at mTaskRoot. @ViewDebug.ExportedProperty(category="recents") public Rect mTaskRect = new Rect(); // The freeform workspace bounds, inset by the top system insets and is a fixed height @ViewDebug.ExportedProperty(category="recents") public Rect mFreeformRect = new Rect(); // The stack bounds, inset from the top system insets, and runs to the bottom of the screen @ViewDebug.ExportedProperty(category="recents") public Rect mStackRect = new Rect(); // This is the current system insets @ViewDebug.ExportedProperty(category="recents") public Rect mSystemInsets = new Rect(); // This is the bounds of the stack action above the stack rect @ViewDebug.ExportedProperty(category="recents") public Rect mStackActionButtonRect = new Rect(); // The visible ranges when the stack is focused and unfocused private Range mUnfocusedRange; private Range mFocusedRange; // The base top margin for the stack from the system insets @ViewDebug.ExportedProperty(category="recents") private int mBaseTopMargin; // The base side margin for the stack from the system insets @ViewDebug.ExportedProperty(category="recents") private int mBaseSideMargin; // The base bottom margin for the stack from the system insets @ViewDebug.ExportedProperty(category="recents") private int mBaseBottomMargin; private int mMinMargin; // The gap between the freeform and stack layouts @ViewDebug.ExportedProperty(category="recents") private int mFreeformStackGap; // The initial offset that the focused task is from the top @ViewDebug.ExportedProperty(category="recents") private int mInitialTopOffset; private int mBaseInitialTopOffset; // The initial offset that the launch-from task is from the bottom @ViewDebug.ExportedProperty(category="recents") private int mInitialBottomOffset; private int mBaseInitialBottomOffset; // The height between the top margin and the top of the focused task @ViewDebug.ExportedProperty(category="recents") private int mFocusedTopPeekHeight; // The height between the bottom margin and the top of task in front of the focused task @ViewDebug.ExportedProperty(category="recents") private int mFocusedBottomPeekHeight; // The offset from the bottom of the stack to the bottom of the bounds when the stack is // scrolled to the front @ViewDebug.ExportedProperty(category="recents") 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 paths defining the distribution of the dim to apply to tasks in the stack when focused // and unfocused private Path mUnfocusedDimCurve; private Path mFocusedDimCurve; private FreePathInterpolator mUnfocusedDimCurveInterpolator; private FreePathInterpolator mFocusedDimCurveInterpolator; // The state of the stack focus (0..1), which controls the transition of the stack from the // focused to non-focused state @ViewDebug.ExportedProperty(category="recents") private int mFocusState; // The smallest scroll progress, at this value, the back most task will be visible @ViewDebug.ExportedProperty(category="recents") float mMinScrollP; // The largest scroll progress, at this value, the front most task will be visible above the // navigation bar @ViewDebug.ExportedProperty(category="recents") float mMaxScrollP; // The initial progress that the scroller is set when you first enter recents @ViewDebug.ExportedProperty(category="recents") float mInitialScrollP; // The task progress for the front-most task in the stack @ViewDebug.ExportedProperty(category="recents") float mFrontMostTaskP; // The last computed task counts @ViewDebug.ExportedProperty(category="recents") int mNumStackTasks; @ViewDebug.ExportedProperty(category="recents") int mNumFreeformTasks; // The min/max z translations @ViewDebug.ExportedProperty(category="recents") int mMinTranslationZ; @ViewDebug.ExportedProperty(category="recents") int mMaxTranslationZ; // Optimization, allows for quick lookup of task -> index private SparseIntArray mTaskIndexMap = new SparseIntArray(); private SparseArray mTaskIndexOverrideMap = new SparseArray<>(); // The freeform workspace layout FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm; // The transform to place TaskViews at the front and back of the stack respectively TaskViewTransform mBackOfStackTransform = new TaskViewTransform(); TaskViewTransform mFrontOfStackTransform = new TaskViewTransform(); public TaskStackLayoutAlgorithm(Context context, TaskStackLayoutAlgorithmCallbacks cb) { Resources res = context.getResources(); mContext = context; mCb = cb; mFreeformLayoutAlgorithm = new FreeformWorkspaceLayoutAlgorithm(context); mMinMargin = res.getDimensionPixelSize(R.dimen.recents_layout_min_margin); mBaseTopMargin = getDimensionForDevice(context, R.dimen.recents_layout_top_margin_phone, R.dimen.recents_layout_top_margin_tablet, R.dimen.recents_layout_top_margin_tablet_xlarge); mBaseSideMargin = getDimensionForDevice(context, R.dimen.recents_layout_side_margin_phone, R.dimen.recents_layout_side_margin_tablet, R.dimen.recents_layout_side_margin_tablet_xlarge); mBaseBottomMargin = res.getDimensionPixelSize(R.dimen.recents_layout_bottom_margin); mFreeformStackGap = res.getDimensionPixelSize(R.dimen.recents_freeform_layout_bottom_margin); reloadOnConfigurationChange(context); } /** * Reloads the layout for the current configuration. */ public void reloadOnConfigurationChange(Context context) { Resources res = context.getResources(); 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 = getInitialFocusState(); mFocusedTopPeekHeight = res.getDimensionPixelSize(R.dimen.recents_layout_top_peek_size); mFocusedBottomPeekHeight = res.getDimensionPixelSize(R.dimen.recents_layout_bottom_peek_size); mMinTranslationZ = res.getDimensionPixelSize(R.dimen.recents_layout_z_min); mMaxTranslationZ = res.getDimensionPixelSize(R.dimen.recents_layout_z_max); mBaseInitialTopOffset = getDimensionForDevice(context, R.dimen.recents_layout_initial_top_offset_phone_port, R.dimen.recents_layout_initial_top_offset_phone_land, R.dimen.recents_layout_initial_top_offset_tablet, R.dimen.recents_layout_initial_top_offset_tablet, R.dimen.recents_layout_initial_top_offset_tablet, R.dimen.recents_layout_initial_top_offset_tablet); mBaseInitialBottomOffset = getDimensionForDevice(context, R.dimen.recents_layout_initial_bottom_offset_phone_port, R.dimen.recents_layout_initial_bottom_offset_phone_land, R.dimen.recents_layout_initial_bottom_offset_tablet, R.dimen.recents_layout_initial_bottom_offset_tablet, R.dimen.recents_layout_initial_bottom_offset_tablet, R.dimen.recents_layout_initial_bottom_offset_tablet); mFreeformLayoutAlgorithm.reloadOnConfigurationChange(context); } /** * Resets this layout when the stack view is reset. */ public void reset() { mTaskIndexOverrideMap.clear(); setFocusState(getInitialFocusState()); } /** * Sets the system insets. */ public boolean setSystemInsets(Rect systemInsets) { boolean changed = !mSystemInsets.equals(systemInsets); mSystemInsets.set(systemInsets); return changed; } /** * Sets the focused state. */ public void setFocusState(int focusState) { int prevFocusState = mFocusState; mFocusState = focusState; updateFrontBackTransforms(); if (mCb != null) { mCb.onFocusStateChanged(prevFocusState, focusState); } } /** * Gets the focused state. */ public int getFocusState() { return mFocusState; } /** * Computes the stack and task rects. The given task stack bounds already has the top/right * insets and left/right padding already applied. */ public void initialize(Rect displayRect, Rect windowRect, Rect taskStackBounds, StackState state) { Rect lastStackRect = new Rect(mStackRect); int topMargin = getScaleForExtent(windowRect, displayRect, mBaseTopMargin, mMinMargin, HEIGHT); int bottomMargin = getScaleForExtent(windowRect, displayRect, mBaseBottomMargin, mMinMargin, HEIGHT); mInitialTopOffset = getScaleForExtent(windowRect, displayRect, mBaseInitialTopOffset, mMinMargin, HEIGHT); mInitialBottomOffset = mBaseInitialBottomOffset; // Compute the stack bounds mState = state; mStackBottomOffset = mSystemInsets.bottom + bottomMargin; state.computeRects(mFreeformRect, mStackRect, taskStackBounds, topMargin, mFreeformStackGap, mStackBottomOffset); // The stack action button will take the full un-padded header space above the stack mStackActionButtonRect.set(mStackRect.left, mStackRect.top - topMargin, mStackRect.right, mStackRect.top + mFocusedTopPeekHeight); // Anchor the task rect top aligned to the stack rect int height = mStackRect.height() - mInitialTopOffset - mStackBottomOffset; mTaskRect.set(mStackRect.left, mStackRect.top, mStackRect.right, 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)) { // Reinitialize the focused and unfocused curves mUnfocusedCurve = constructUnfocusedCurve(); mUnfocusedCurveInterpolator = new FreePathInterpolator(mUnfocusedCurve); mFocusedCurve = constructFocusedCurve(); mFocusedCurveInterpolator = new FreePathInterpolator(mFocusedCurve); mUnfocusedDimCurve = constructUnfocusedDimCurve(); mUnfocusedDimCurveInterpolator = new FreePathInterpolator(mUnfocusedDimCurve); mFocusedDimCurve = constructFocusedDimCurve(); mFocusedDimCurveInterpolator = new FreePathInterpolator(mFocusedDimCurve); updateFrontBackTransforms(); } } /** * Computes the minimum and maximum scroll progress values and the progress values for each task * in the stack. */ void update(TaskStack stack, ArraySet ignoreTasksSet) { SystemServicesProxy ssp = Recents.getSystemServices(); RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); // Clear the progress map mTaskIndexMap.clear(); // Return early if we have no tasks ArrayList tasks = stack.getStackTasks(); if (tasks.isEmpty()) { mFrontMostTaskP = 0; mMinScrollP = mMaxScrollP = mInitialScrollP = 0; mNumStackTasks = mNumFreeformTasks = 0; return; } // Filter the set of freeform and stack tasks ArrayList freeformTasks = new ArrayList<>(); ArrayList stackTasks = new ArrayList<>(); for (int i = 0; i < tasks.size(); i++) { Task task = tasks.get(i); if (ignoreTasksSet.contains(task.key)) { continue; } 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.id, i); } // Update the freeform tasks if (!freeformTasks.isEmpty()) { mFreeformLayoutAlgorithm.update(freeformTasks, this); } // Calculate the min/max/initial scroll Task launchTask = stack.getLaunchTarget(); int launchTaskIndex = launchTask != null ? stack.indexOfStackTask(launchTask) : mNumStackTasks - 1; if (getInitialFocusState() == STATE_FOCUSED) { int maxBottomOffset = mStackBottomOffset + mTaskRect.height(); float maxBottomNormX = getNormalizedXFromFocusedY(maxBottomOffset, FROM_BOTTOM); mFocusedRange.offset(0f); mMinScrollP = 0; mMaxScrollP = Math.max(mMinScrollP, (mNumStackTasks - 1) - Math.max(0, mFocusedRange.getAbsoluteX(maxBottomNormX))); if (launchState.launchedFromHome) { mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); } else { mInitialScrollP = Utilities.clamp(launchTaskIndex - 1, mMinScrollP, mMaxScrollP); } } else if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) { // If there is one stack task, ignore the min/max/initial scroll positions mMinScrollP = 0; mMaxScrollP = 0; mInitialScrollP = 0; } else { // Set the max scroll to be the point where the front most task is visible with the // stack bottom offset int maxBottomOffset = mStackBottomOffset + mTaskRect.height(); float maxBottomNormX = getNormalizedXFromUnfocusedY(maxBottomOffset, FROM_BOTTOM); mUnfocusedRange.offset(0f); mMinScrollP = 0; mMaxScrollP = Math.max(mMinScrollP, (mNumStackTasks - 1) - Math.max(0, mUnfocusedRange.getAbsoluteX(maxBottomNormX))); boolean scrollToFront = launchState.launchedFromHome || launchState.launchedViaDockGesture; if (launchState.launchedWithAltTab) { mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); } else if (scrollToFront) { mInitialScrollP = Utilities.clamp(launchTaskIndex, mMinScrollP, mMaxScrollP); } else { // We are overriding the initial two task positions, so set the initial scroll // position to match the second task (aka focused task) position float initialTopNormX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, (mNumStackTasks - 2)) - Math.max(0, mUnfocusedRange.getAbsoluteX(initialTopNormX))); } } } /** * Creates task overrides to ensure the initial stack layout if necessary. */ public void setTaskOverridesForInitialState(TaskStack stack, boolean ignoreScrollToFront) { RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); mTaskIndexOverrideMap.clear(); boolean scrollToFront = launchState.launchedFromHome || launchState.launchedViaDockGesture; if (getInitialFocusState() == STATE_UNFOCUSED && mNumStackTasks > 1) { if (ignoreScrollToFront || (!launchState.launchedWithAltTab && !scrollToFront)) { // Set the initial scroll to the predefined state (which differs from the stack) float [] initialNormX = null; float minBottomTaskNormX = getNormalizedXFromUnfocusedY(mSystemInsets.bottom + mInitialBottomOffset, FROM_BOTTOM); float maxBottomTaskNormX = getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight + mTaskRect.height() - mMinMargin, FROM_TOP); if (mNumStackTasks <= 2) { // For small stacks, position the tasks so that they are top aligned to under // the action button, but ensure that it is at least a certain offset from the // bottom of the stack initialNormX = new float[] { Math.min(maxBottomTaskNormX, minBottomTaskNormX), getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight, FROM_TOP) }; } else { initialNormX = new float[] { minBottomTaskNormX, getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP) }; } mUnfocusedRange.offset(0f); List tasks = stack.getStackTasks(); int taskCount = tasks.size(); for (int i = taskCount - 1; i >= 0; i--) { int indexFromFront = taskCount - i - 1; if (indexFromFront >= initialNormX.length) { break; } float newTaskProgress = mInitialScrollP + mUnfocusedRange.getAbsoluteX(initialNormX[indexFromFront]); mTaskIndexOverrideMap.put(tasks.get(i).key.id, newTaskProgress); } } } } /** * Adds and override task progress for the given task when transitioning from focused to * unfocused state. */ public void addUnfocusedTaskOverride(Task task, float stackScroll) { if (mFocusState != STATE_UNFOCUSED) { mFocusedRange.offset(stackScroll); mUnfocusedRange.offset(stackScroll); float focusedRangeX = mFocusedRange.getNormalizedX(mTaskIndexMap.get(task.key.id)); float focusedY = mFocusedCurveInterpolator.getInterpolation(focusedRangeX); float unfocusedRangeX = mUnfocusedCurveInterpolator.getX(focusedY); float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX); if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) { mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress); } } } /** * Adds and override task progress for the given task when transitioning from focused to * unfocused state. */ public void addUnfocusedTaskOverride(TaskView taskView, float stackScroll) { mFocusedRange.offset(stackScroll); mUnfocusedRange.offset(stackScroll); Task task = taskView.getTask(); int top = taskView.getTop() - mTaskRect.top; float focusedRangeX = getNormalizedXFromFocusedY(top, FROM_TOP); float unfocusedRangeX = getNormalizedXFromUnfocusedY(top, FROM_TOP); float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX); if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) { mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress); } } public void clearUnfocusedTaskOverrides() { mTaskIndexOverrideMap.clear(); } /** * Updates this stack when a scroll happens. * */ public float updateFocusStateOnScroll(float lastTargetStackScroll, float targetStackScroll, float lastStackScroll) { if (targetStackScroll == lastStackScroll) { return targetStackScroll; } float deltaScroll = targetStackScroll - lastStackScroll; float deltaTargetScroll = targetStackScroll - lastTargetStackScroll; float newScroll = targetStackScroll; mUnfocusedRange.offset(targetStackScroll); for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) { int taskId = mTaskIndexOverrideMap.keyAt(i); float x = mTaskIndexMap.get(taskId); float overrideX = mTaskIndexOverrideMap.get(taskId, 0f); float newOverrideX = overrideX + deltaScroll; if (isInvalidOverrideX(x, overrideX, newOverrideX)) { // Remove the override once we reach the original task index mTaskIndexOverrideMap.removeAt(i); } else if ((overrideX >= x && deltaScroll <= 0f) || (overrideX <= x && deltaScroll >= 0f)) { // Scrolling from override x towards x, then lock the task in place mTaskIndexOverrideMap.put(taskId, newOverrideX); } else { // Scrolling override x away from x, we should still move the scroll towards x newScroll = lastStackScroll; newOverrideX = overrideX - deltaTargetScroll; if (isInvalidOverrideX(x, overrideX, newOverrideX)) { mTaskIndexOverrideMap.removeAt(i); } else{ mTaskIndexOverrideMap.put(taskId, newOverrideX); } } } return newScroll; } private boolean isInvalidOverrideX(float x, float overrideX, float newOverrideX) { boolean outOfBounds = mUnfocusedRange.getNormalizedX(newOverrideX) < 0f || mUnfocusedRange.getNormalizedX(newOverrideX) > 1f; return outOfBounds || (overrideX >= x && x >= newOverrideX) || (overrideX <= x && x <= newOverrideX); } /** * Returns the default focus state. */ public int getInitialFocusState() { RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); RecentsDebugFlags debugFlags = Recents.getDebugFlags(); if (debugFlags.isPagingEnabled() || launchState.launchedWithAltTab) { return STATE_FOCUSED; } else { return STATE_UNFOCUSED; } } /** * Returns the TaskViewTransform that would put the task just off the back of the stack. */ public TaskViewTransform getBackOfStackTransform() { return mBackOfStackTransform; } /** * Returns the TaskViewTransform that would put the task just off the front of the stack. */ public TaskViewTransform getFrontOfStackTransform() { return mFrontOfStackTransform; } /** * Returns the current stack state. */ public StackState getStackState() { return mState; } /** * Returns whether this stack layout has been initialized. */ public boolean isInitialized() { return !mStackRect.isEmpty(); } /** * 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 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 = getInitialFocusState() > 0f ? mFocusedRange : mUnfocusedRange; currentRange.offset(mInitialScrollP); int taskBarHeight = mContext.getResources().getDimensionPixelSize( R.dimen.recents_task_view_header_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, taskProgress, mInitialScrollP, mFocusState, tmpTransform, null, false /* ignoreSingleTaskCase */, false /* forceUpdate */); 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) { return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform, false /* forceUpdate */, false /* ignoreTaskOverrides */); } public TaskViewTransform getStackTransform(Task task, float stackScroll, TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean ignoreTaskOverrides) { return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform, false /* forceUpdate */, ignoreTaskOverrides); } public TaskViewTransform getStackTransform(Task task, float stackScroll, int focusState, TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate, boolean ignoreTaskOverrides) { if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) { mFreeformLayoutAlgorithm.getTransform(task, transformOut, this); return transformOut; } else { // Return early if we have an invalid index int nonOverrideTaskProgress = mTaskIndexMap.get(task.key.id, -1); if (task == null || nonOverrideTaskProgress == -1) { transformOut.reset(); return transformOut; } float taskProgress = ignoreTaskOverrides ? nonOverrideTaskProgress : getStackScrollForTask(task); getStackTransform(taskProgress, nonOverrideTaskProgress, stackScroll, focusState, transformOut, frontTransform, false /* ignoreSingleTaskCase */, forceUpdate); return transformOut; } } /** * Like {@link #getStackTransform}, but in screen coordinates */ public TaskViewTransform getStackTransformScreenCoordinates(Task task, float stackScroll, TaskViewTransform transformOut, TaskViewTransform frontTransform, Rect windowOverrideRect) { TaskViewTransform transform = getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform, true /* forceUpdate */, false /* ignoreTaskOverrides */); return transformToScreenCoordinates(transform, windowOverrideRect); } /** * Transforms the given {@param transformOut} to the screen coordinates, overriding the current * window rectangle with {@param windowOverrideRect} if non-null. */ public TaskViewTransform transformToScreenCoordinates(TaskViewTransform transformOut, Rect windowOverrideRect) { Rect windowRect = windowOverrideRect != null ? windowOverrideRect : Recents.getSystemServices().getWindowRect(); transformOut.rect.offset(windowRect.left, windowRect.top); return transformOut; } /** * Update/get the transform. * * @param ignoreSingleTaskCase When set, will ensure that the transform computed does not take * into account the special single-task case. This is only used * internally to ensure that we can calculate the transform for any * position in the stack. */ public void getStackTransform(float taskProgress, float nonOverrideTaskProgress, float stackScroll, int focusState, TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean ignoreSingleTaskCase, boolean forceUpdate) { SystemServicesProxy ssp = Recents.getSystemServices(); // Ensure that the task is in range mUnfocusedRange.offset(stackScroll); mFocusedRange.offset(stackScroll); boolean unfocusedVisible = mUnfocusedRange.isInRange(taskProgress); boolean focusedVisible = mFocusedRange.isInRange(taskProgress); // Skip if the task is not visible if (!forceUpdate && !unfocusedVisible && !focusedVisible) { transformOut.reset(); return; } // Map the absolute task progress to the normalized x at the stack scroll. We use this to // calculate positions along the curve. mUnfocusedRange.offset(stackScroll); mFocusedRange.offset(stackScroll); float unfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); float focusedRangeX = mFocusedRange.getNormalizedX(taskProgress); // Map the absolute task progress to the normalized x at the bounded stack scroll. We use // this to calculate bounded properties, like translationZ and outline alpha. float boundedStackScroll = Utilities.clamp(stackScroll, mMinScrollP, mMaxScrollP); mUnfocusedRange.offset(boundedStackScroll); mFocusedRange.offset(boundedStackScroll); float boundedScrollUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); float boundedScrollUnfocusedNonOverrideRangeX = mUnfocusedRange.getNormalizedX(nonOverrideTaskProgress); // Map the absolute task progress to the normalized x at the upper bounded stack scroll. // We use this to calculate the dim, which is bounded only on one end. float lowerBoundedStackScroll = Utilities.clamp(stackScroll, -Float.MAX_VALUE, mMaxScrollP); mUnfocusedRange.offset(lowerBoundedStackScroll); mFocusedRange.offset(lowerBoundedStackScroll); float lowerBoundedUnfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress); float lowerBoundedFocusedRangeX = mFocusedRange.getNormalizedX(taskProgress); int x = (mStackRect.width() - mTaskRect.width()) / 2; int y; float z; float dimAlpha; float viewOutlineAlpha; if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) { // When there is exactly one task, then decouple the task from the stack and just move // in screen space float tmpP = (mMinScrollP - stackScroll) / mNumStackTasks; int centerYOffset = (mStackRect.top - mTaskRect.top) + (mStackRect.height() - mSystemInsets.bottom - mTaskRect.height()) / 2; y = centerYOffset + getYForDeltaP(tmpP, 0); z = mMaxTranslationZ; dimAlpha = 0f; viewOutlineAlpha = OUTLINE_ALPHA_MIN_VALUE + (OUTLINE_ALPHA_MAX_VALUE - OUTLINE_ALPHA_MIN_VALUE) / 2f; } else { // Otherwise, update the task to the stack layout int unfocusedY = (int) ((1f - mUnfocusedCurveInterpolator.getInterpolation( unfocusedRangeX)) * mStackRect.height()); int focusedY = (int) ((1f - mFocusedCurveInterpolator.getInterpolation( focusedRangeX)) * mStackRect.height()); float unfocusedDim = mUnfocusedDimCurveInterpolator.getInterpolation( lowerBoundedUnfocusedRangeX); float focusedDim = mFocusedDimCurveInterpolator.getInterpolation( lowerBoundedFocusedRangeX); // Special case, because we override the initial task positions differently for small // stacks, we clamp the dim to 0 in the initial position, and then only modulate the // dim when the task is scrolled back towards the top of the screen if (mNumStackTasks <= 2 && nonOverrideTaskProgress == 0f) { if (boundedScrollUnfocusedRangeX >= 0.5f) { unfocusedDim = 0f; } else { float offset = mUnfocusedDimCurveInterpolator.getInterpolation(0.5f); unfocusedDim -= offset; unfocusedDim *= MAX_DIM / (MAX_DIM - offset); } } y = (mStackRect.top - mTaskRect.top) + (int) Utilities.mapRange(focusState, unfocusedY, focusedY); z = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedNonOverrideRangeX), mMinTranslationZ, mMaxTranslationZ); dimAlpha = Utilities.mapRange(focusState, unfocusedDim, focusedDim); viewOutlineAlpha = Utilities.mapRange(Utilities.clamp01(boundedScrollUnfocusedRangeX), OUTLINE_ALPHA_MIN_VALUE, OUTLINE_ALPHA_MAX_VALUE); } // Fill out the transform transformOut.scale = 1f; transformOut.alpha = 1f; transformOut.translationZ = z; transformOut.dimAlpha = dimAlpha; transformOut.viewOutlineAlpha = viewOutlineAlpha; 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); } /** * 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) { Float overrideP = mTaskIndexOverrideMap.get(t.key.id, null); if (overrideP == null) { return (float) mTaskIndexMap.get(t.key.id, 0); } return overrideP; } /** * Returns the original scroll progress to scroll to such that the top of the task is at the top * of the stack. */ float getStackScrollForTaskIgnoreOverrides(Task t) { return (float) mTaskIndexMap.get(t.key.id, 0); } /** * Returns the scroll progress to scroll to such that the top of the task at the initial top * offset (which is at the task's brightest point). */ float getStackScrollForTaskAtInitialOffset(Task t) { float normX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); mUnfocusedRange.offset(0f); return Utilities.clamp((float) mTaskIndexMap.get(t.key.id, 0) - Math.max(0, mUnfocusedRange.getAbsoluteX(normX)), mMinScrollP, mMaxScrollP); } /** * 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; } /** * Returns the task stack bounds in the current orientation. This rect takes into account the * top and right system insets (but not the bottom inset) and left/right paddings, but _not_ * the top/bottom padding or insets. */ public void getTaskStackBounds(Rect displayRect, Rect windowRect, int topInset, int rightInset, Rect taskStackBounds) { taskStackBounds.set(windowRect.left, windowRect.top + topInset, windowRect.right - rightInset, windowRect.bottom); // Ensure that the new width is at most the smaller display edge size int sideMargin = getScaleForExtent(windowRect, displayRect, mBaseSideMargin, mMinMargin, WIDTH); int targetStackWidth = taskStackBounds.width() - 2 * sideMargin; if (Utilities.getAppConfiguration(mContext).orientation == Configuration.ORIENTATION_LANDSCAPE) { // If we are in landscape, calculate the width of the stack in portrait and ensure that // we are not larger than that size Rect portraitDisplayRect = new Rect(0, 0, Math.min(displayRect.width(), displayRect.height()), Math.max(displayRect.width(), displayRect.height())); int portraitSideMargin = getScaleForExtent(portraitDisplayRect, portraitDisplayRect, mBaseSideMargin, mMinMargin, WIDTH); targetStackWidth = Math.min(targetStackWidth, portraitDisplayRect.width() - 2 * portraitSideMargin); } taskStackBounds.inset((taskStackBounds.width() - targetStackWidth) / 2, 0); } /** * Retrieves resources that are constant regardless of the current configuration of the device. */ public static int getDimensionForDevice(Context ctx, int phoneResId, int tabletResId, int xlargeTabletResId) { return getDimensionForDevice(ctx, phoneResId, phoneResId, tabletResId, tabletResId, xlargeTabletResId, xlargeTabletResId); } /** * Retrieves resources that are constant regardless of the current configuration of the device. */ public static int getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId, int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId, int xlargeTabletLandResId) { RecentsConfiguration config = Recents.getConfiguration(); Resources res = ctx.getResources(); boolean isLandscape = Utilities.getAppConfiguration(ctx).orientation == Configuration.ORIENTATION_LANDSCAPE; if (config.isXLargeScreen) { return res.getDimensionPixelSize(isLandscape ? xlargeTabletLandResId : xlargeTabletPortResId); } else if (config.isLargeScreen) { return res.getDimensionPixelSize(isLandscape ? tabletLandResId : tabletPortResId); } else { return res.getDimensionPixelSize(isLandscape ? phoneLandResId : phonePortResId); } } /** * Returns the normalized x on the unfocused curve given an absolute Y position (relative to the * stack height). */ private float getNormalizedXFromUnfocusedY(float y, @AnchorSide int fromSide) { float offset = (fromSide == FROM_TOP) ? mStackRect.height() - y : y; float offsetPct = offset / mStackRect.height(); return mUnfocusedCurveInterpolator.getX(offsetPct); } /** * Returns the normalized x on the focused curve given an absolute Y position (relative to the * stack height). */ private float getNormalizedXFromFocusedY(float y, @AnchorSide int fromSide) { float offset = (fromSide == FROM_TOP) ? mStackRect.height() - y : y; float offsetPct = offset / mStackRect.height(); return mFocusedCurveInterpolator.getX(offsetPct); } /** * Creates a new path for the focused curve. */ private Path constructFocusedCurve() { // Initialize the focused curve. This curve is a piecewise curve composed of several // linear pieces that goes from (0,1) through (0.5, peek height offset), // (0.5, bottom task offsets), and (1,0). float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); float bottomPeekHeightPct = (float) (mStackBottomOffset + mFocusedBottomPeekHeight) / mStackRect.height(); float minBottomPeekHeightPct = (float) (mFocusedTopPeekHeight + mTaskRect.height() - mMinMargin) / mStackRect.height(); Path p = new Path(); p.moveTo(0f, 1f); p.lineTo(0.5f, 1f - topPeekHeightPct); p.lineTo(1f - (0.5f / mFocusedRange.relativeMax), Math.max(1f - minBottomPeekHeightPct, bottomPeekHeightPct)); 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 = 0.975f; float topPeekHeightPct = (float) mFocusedTopPeekHeight / mStackRect.height(); float slope = ((1f - topPeekHeightPct) - cpoint1Y) / (0.5f - cpoint1X); float b = 1f - slope * cpoint1X; float cpoint2X = 0.65f; float cpoint2Y = slope * cpoint2X + b; Path p = new Path(); p.moveTo(0f, 1f); p.cubicTo(0f, 1f, cpoint1X, cpoint1Y, 0.5f, 1f - topPeekHeightPct); p.cubicTo(0.5f, 1f - topPeekHeightPct, cpoint2X, cpoint2Y, 1f, 0f); return p; } /** * Creates a new path for the focused dim curve. */ private Path constructFocusedDimCurve() { Path p = new Path(); // The focused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused // task), then goes back to max dim at the next task p.moveTo(0f, MAX_DIM); p.lineTo(0.5f, 0f); p.lineTo(0.5f + (0.5f / mFocusedRange.relativeMax), MAX_DIM); p.lineTo(1f, MAX_DIM); return p; } /** * Creates a new path for the unfocused dim curve. */ private Path constructUnfocusedDimCurve() { float focusX = getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP); float cpoint2X = focusX + (1f - focusX) / 2; Path p = new Path(); // The unfocused dim interpolator starts at max dim, reduces to zero at 0.5 (the focused // task), then goes back to max dim towards the front of the stack p.moveTo(0f, MAX_DIM); p.cubicTo(focusX * 0.5f, MAX_DIM, focusX * 0.75f, MAX_DIM * 0.75f, focusX, 0f); p.cubicTo(cpoint2X, 0f, cpoint2X, MED_DIM, 1f, MED_DIM); return p; } /** * Scales the given {@param value} to the scale of the {@param instance} rect relative to the * {@param other} rect in the {@param extent} side. */ private int getScaleForExtent(Rect instance, Rect other, int value, int minValue, @Extent int extent) { if (extent == WIDTH) { float scale = Utilities.clamp01((float) instance.width() / other.width()); return Math.max(minValue, (int) (scale * value)); } else if (extent == HEIGHT) { float scale = Utilities.clamp01((float) instance.height() / other.height()); return Math.max(minValue, (int) (scale * value)); } return value; } /** * Updates the current transforms that would put a TaskView at the front and back of the stack. */ private void updateFrontBackTransforms() { // Return early if we have not yet initialized if (mStackRect.isEmpty()) { return; } float min = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMin, mFocusedRange.relativeMin); float max = Utilities.mapRange(mFocusState, mUnfocusedRange.relativeMax, mFocusedRange.relativeMax); getStackTransform(min, min, 0f, mFocusState, mBackOfStackTransform, null, true /* ignoreSingleTaskCase */, true /* forceUpdate */); getStackTransform(max, max, 0f, mFocusState, mFrontOfStackTransform, null, true /* ignoreSingleTaskCase */, true /* forceUpdate */); mBackOfStackTransform.visible = true; mFrontOfStackTransform.visible = true; } public void dump(String prefix, PrintWriter writer) { String innerPrefix = prefix + " "; writer.print(prefix); writer.print(TAG); writer.write(" numStackTasks="); writer.write(mNumStackTasks); writer.println(); writer.print(innerPrefix); writer.print("insets="); writer.print(Utilities.dumpRect(mSystemInsets)); writer.print(" stack="); writer.print(Utilities.dumpRect(mStackRect)); writer.print(" task="); writer.print(Utilities.dumpRect(mTaskRect)); writer.print(" freeform="); writer.print(Utilities.dumpRect(mFreeformRect)); writer.print(" actionButton="); writer.print(Utilities.dumpRect(mStackActionButtonRect)); writer.println(); writer.print(innerPrefix); writer.print("minScroll="); writer.print(mMinScrollP); writer.print(" maxScroll="); writer.print(mMaxScrollP); writer.print(" initialScroll="); writer.print(mInitialScrollP); writer.println(); writer.print(innerPrefix); writer.print("focusState="); writer.print(mFocusState); writer.println(); if (mTaskIndexOverrideMap.size() > 0) { for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) { int taskId = mTaskIndexOverrideMap.keyAt(i); float x = mTaskIndexMap.get(taskId); float overrideX = mTaskIndexOverrideMap.get(taskId, 0f); writer.print(innerPrefix); writer.print("taskId= "); writer.print(taskId); writer.print(" x= "); writer.print(x); writer.print(" overrideX= "); writer.print(overrideX); writer.println(); } } } }