| /* |
| * 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 com.android.systemui.recents.views.grid.TaskGridLayoutAlgorithm; |
| 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; |
| } |
| } |
| } |
| |
| /** |
| * @return True if we should use the grid layout. |
| */ |
| boolean useGridLayout() { |
| return Recents.getConfiguration().isGridEnabled; |
| } |
| |
| // A report of the visibility state of the stack |
| public static class VisibilityReport { |
| public int numVisibleTasks; |
| public int numVisibleThumbnails; |
| |
| public 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(); |
| |
| // The visible ranges when the stack is focused and unfocused |
| private Range mUnfocusedRange; |
| private Range mFocusedRange; |
| |
| // This is the bounds of the stack action above the stack rect |
| @ViewDebug.ExportedProperty(category="recents") |
| private Rect mStackActionButtonRect = new Rect(); |
| // 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 height, in pixels, of each task view's title bar. */ |
| private int mTitleBarHeight; |
| |
| // 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") |
| public int mMaxTranslationZ; |
| |
| // Optimization, allows for quick lookup of task -> index |
| private SparseIntArray mTaskIndexMap = new SparseIntArray(); |
| private SparseArray<Float> mTaskIndexOverrideMap = new SparseArray<>(); |
| |
| // The freeform workspace layout |
| FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm; |
| TaskGridLayoutAlgorithm mTaskGridLayoutAlgorithm; |
| |
| // 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); |
| mTaskGridLayoutAlgorithm = new TaskGridLayoutAlgorithm(context); |
| 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, |
| 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, |
| R.dimen.recents_layout_initial_bottom_offset_tablet); |
| mFreeformLayoutAlgorithm.reloadOnConfigurationChange(context); |
| mTaskGridLayoutAlgorithm.reloadOnConfigurationChange(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, |
| R.dimen.recents_layout_top_margin_tablet); |
| 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, |
| R.dimen.recents_layout_side_margin_tablet); |
| mBaseBottomMargin = res.getDimensionPixelSize(R.dimen.recents_layout_bottom_margin); |
| mFreeformStackGap = |
| res.getDimensionPixelSize(R.dimen.recents_freeform_layout_bottom_margin); |
| mTitleBarHeight = getDimensionForDevice(mContext, |
| R.dimen.recents_task_view_header_height, |
| R.dimen.recents_task_view_header_height, |
| R.dimen.recents_task_view_header_height, |
| R.dimen.recents_task_view_header_height_tablet_land, |
| R.dimen.recents_task_view_header_height, |
| R.dimen.recents_task_view_header_height_tablet_land, |
| R.dimen.recents_grid_task_view_header_height); |
| } |
| |
| /** |
| * 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); |
| mTaskGridLayoutAlgorithm.setSystemInsets(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(); |
| } |
| |
| // Initialize the grid layout |
| mTaskGridLayoutAlgorithm.initialize(windowRect); |
| } |
| |
| /** |
| * Computes the minimum and maximum scroll progress values and the progress values for each task |
| * in the stack. |
| */ |
| void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet, |
| RecentsActivityLaunchState launchState) { |
| 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 = mInitialScrollP = 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 (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 || launchState.launchedFromPipApp |
| || launchState.launchedWithNextPipApp) { |
| 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.launchedFromPipApp |
| || launchState.launchedWithNextPipApp || launchState.launchedViaDockGesture; |
| if (launchState.launchedFromBlacklistedApp) { |
| mInitialScrollP = mMaxScrollP; |
| } else 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.launchedFromPipApp || |
| launchState.launchedWithNextPipApp || |
| launchState.launchedFromBlacklistedApp || |
| 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<Task> 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; |
| } |
| } |
| |
| public Rect getStackActionButtonRect() { |
| return useGridLayout() |
| ? mTaskGridLayoutAlgorithm.getStackActionButtonRect() : mStackActionButtonRect; |
| } |
| |
| /** |
| * 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<Task> tasks) { |
| if (useGridLayout()) { |
| return mTaskGridLayoutAlgorithm.computeStackVisibilityReport(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(mNumFreeformTasks > 0 ? Math.max(mNumFreeformTasks, 1) : 0, |
| mNumFreeformTasks > 0 ? Math.max(mNumFreeformTasks, 1) : 0); |
| } |
| |
| // 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 = mNumFreeformTasks > 0 ? Math.max(mNumFreeformTasks, 1) : 0; |
| int numVisibleThumbnails = mNumFreeformTasks > 0 ? Math.max(mNumFreeformTasks, 0) : 0; |
| 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--) { |
| taskProgress = getStackScrollForTask(tasks.get(j)); |
| if (!currentRange.isInRange(taskProgress)) { |
| break; |
| } |
| numVisibleTasks++; |
| } |
| break; |
| } |
| } else { |
| // 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 if (useGridLayout()) { |
| int taskIndex = mTaskIndexMap.get(task.key.id); |
| int taskCount = mTaskIndexMap.size(); |
| mTaskGridLayoutAlgorithm.getTransform(taskIndex, taskCount, 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. |
| */ |
| TaskViewTransform transformToScreenCoordinates(TaskViewTransform transformOut, |
| Rect windowOverrideRect) { |
| Rect windowRect = windowOverrideRect != null |
| ? windowOverrideRect |
| : Recents.getSystemServices().getWindowRect(); |
| transformOut.rect.offset(windowRect.left, windowRect.top); |
| if (useGridLayout()) { |
| // Draw the thumbnail a little lower to perfectly coincide with the view we are |
| // transitioning to, where the header bar has already been drawn. |
| transformOut.rect.offset(0, mTitleBarHeight); |
| } |
| 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 leftInset, |
| int rightInset, Rect taskStackBounds) { |
| taskStackBounds.set(windowRect.left + leftInset, 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, int gridLayoutResId) { |
| return getDimensionForDevice(ctx, phoneResId, phoneResId, tabletResId, tabletResId, |
| xlargeTabletResId, xlargeTabletResId, gridLayoutResId); |
| } |
| |
| /** |
| * 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, int gridLayoutResId) { |
| RecentsConfiguration config = Recents.getConfiguration(); |
| Resources res = ctx.getResources(); |
| boolean isLandscape = Utilities.getAppConfiguration(ctx).orientation == |
| Configuration.ORIENTATION_LANDSCAPE; |
| if (config.isGridEnabled) { |
| return res.getDimensionPixelSize(gridLayoutResId); |
| } else 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; |
| } |
| |
| /** |
| * Returns the proper task rectangle according to the current grid state. |
| */ |
| public Rect getTaskRect() { |
| return useGridLayout() ? mTaskGridLayoutAlgorithm.getTaskGridRect() : mTaskRect; |
| } |
| |
| public void dump(String prefix, PrintWriter writer) { |
| String innerPrefix = prefix + " "; |
| |
| writer.print(prefix); writer.print(TAG); |
| writer.write(" numStackTasks="); writer.print(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(); |
| } |
| } |
| } |
| } |