| /* |
| * 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 static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; |
| import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; |
| import static android.app.ActivityManager.StackId.INVALID_STACK_ID; |
| |
| import android.animation.ObjectAnimator; |
| import android.animation.ValueAnimator; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.GradientDrawable; |
| import android.os.Bundle; |
| import android.os.Parcelable; |
| import android.provider.Settings; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.MutableBoolean; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewDebug; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.widget.FrameLayout; |
| |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.logging.MetricsProto.MetricsEvent; |
| import com.android.systemui.Interpolators; |
| import com.android.systemui.R; |
| import com.android.systemui.recents.Recents; |
| import com.android.systemui.recents.RecentsActivity; |
| import com.android.systemui.recents.RecentsActivityLaunchState; |
| import com.android.systemui.recents.RecentsConfiguration; |
| import com.android.systemui.recents.RecentsDebugFlags; |
| import com.android.systemui.recents.events.EventBus; |
| import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent; |
| import com.android.systemui.recents.events.activity.ConfigurationChangedEvent; |
| import com.android.systemui.recents.events.activity.DismissRecentsToHomeAnimationStarted; |
| import com.android.systemui.recents.events.activity.EnterRecentsTaskStackAnimationCompletedEvent; |
| import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationCompletedEvent; |
| import com.android.systemui.recents.events.activity.HideHistoryButtonEvent; |
| import com.android.systemui.recents.events.activity.HideHistoryEvent; |
| import com.android.systemui.recents.events.activity.HideRecentsEvent; |
| import com.android.systemui.recents.events.activity.IterateRecentsEvent; |
| import com.android.systemui.recents.events.activity.LaunchNextTaskRequestEvent; |
| import com.android.systemui.recents.events.activity.LaunchTaskEvent; |
| import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent; |
| import com.android.systemui.recents.events.activity.MultiWindowStateChangedEvent; |
| import com.android.systemui.recents.events.activity.PackagesChangedEvent; |
| import com.android.systemui.recents.events.activity.ShowHistoryButtonEvent; |
| import com.android.systemui.recents.events.activity.ShowHistoryEvent; |
| import com.android.systemui.recents.events.ui.AllTaskViewsDismissedEvent; |
| import com.android.systemui.recents.events.ui.DeleteTaskDataEvent; |
| import com.android.systemui.recents.events.ui.DismissTaskViewEvent; |
| import com.android.systemui.recents.events.ui.TaskViewDismissedEvent; |
| import com.android.systemui.recents.events.ui.UpdateFreeformTaskViewVisibilityEvent; |
| import com.android.systemui.recents.events.ui.UserInteractionEvent; |
| import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent; |
| import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; |
| import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; |
| import com.android.systemui.recents.events.ui.dragndrop.DragStartInitializeDropTargetsEvent; |
| import com.android.systemui.recents.events.ui.focus.DismissFocusedTaskViewEvent; |
| import com.android.systemui.recents.events.ui.focus.FocusNextTaskViewEvent; |
| import com.android.systemui.recents.events.ui.focus.FocusPreviousTaskViewEvent; |
| import com.android.systemui.recents.misc.DozeTrigger; |
| import com.android.systemui.recents.misc.ReferenceCountedTrigger; |
| import com.android.systemui.recents.misc.SystemServicesProxy; |
| import com.android.systemui.recents.misc.Utilities; |
| import com.android.systemui.recents.model.Task; |
| import com.android.systemui.recents.model.TaskStack; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| |
| /* The visual representation of a task stack view */ |
| public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks, |
| TaskView.TaskViewCallbacks, TaskStackViewScroller.TaskStackViewScrollerCallbacks, |
| TaskStackLayoutAlgorithm.TaskStackLayoutAlgorithmCallbacks, |
| ViewPool.ViewPoolConsumer<TaskView, Task> { |
| |
| private final static String KEY_SAVED_STATE_SUPER = "saved_instance_state_super"; |
| private final static String KEY_SAVED_STATE_LAYOUT_FOCUSED_STATE = |
| "saved_instance_state_layout_focused_state"; |
| private final static String KEY_SAVED_STATE_LAYOUT_STACK_SCROLL = |
| "saved_instance_state_layout_stack_scroll"; |
| |
| // The thresholds at which to show/hide the history button. |
| private static final float SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f; |
| private static final float HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f; |
| |
| public static final int DEFAULT_SYNC_STACK_DURATION = 200; |
| private static final int DRAG_SCALE_DURATION = 175; |
| private static final float DRAG_SCALE_FACTOR = 1.05f; |
| |
| private static final int LAUNCH_NEXT_SCROLL_BASE_DURATION = 200; |
| private static final int LAUNCH_NEXT_SCROLL_INCR_DURATION = 32; |
| |
| private static final ArraySet<Task.TaskKey> EMPTY_TASK_SET = new ArraySet<>(); |
| |
| LayoutInflater mInflater; |
| TaskStack mStack = new TaskStack(); |
| @ViewDebug.ExportedProperty(deepExport=true, prefix="layout_") |
| TaskStackLayoutAlgorithm mLayoutAlgorithm; |
| @ViewDebug.ExportedProperty(deepExport=true, prefix="scroller_") |
| TaskStackViewScroller mStackScroller; |
| @ViewDebug.ExportedProperty(deepExport=true, prefix="touch_") |
| TaskStackViewTouchHandler mTouchHandler; |
| TaskStackAnimationHelper mAnimationHelper; |
| GradientDrawable mFreeformWorkspaceBackground; |
| ObjectAnimator mFreeformWorkspaceBackgroundAnimator; |
| ViewPool<TaskView, Task> mViewPool; |
| |
| ArrayList<TaskView> mTaskViews = new ArrayList<>(); |
| ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>(); |
| ArraySet<Task.TaskKey> mIgnoreTasks = new ArraySet<>(); |
| AnimationProps mDeferredTaskViewLayoutAnimation = null; |
| |
| @ViewDebug.ExportedProperty(deepExport=true, prefix="doze_") |
| DozeTrigger mUIDozeTrigger; |
| @ViewDebug.ExportedProperty(deepExport=true, prefix="focused_task_") |
| Task mFocusedTask; |
| |
| int mTaskCornerRadiusPx; |
| private int mDividerSize; |
| private int mStartTimerIndicatorDuration; |
| |
| @ViewDebug.ExportedProperty(category="recents") |
| boolean mTaskViewsClipDirty = true; |
| @ViewDebug.ExportedProperty(category="recents") |
| boolean mAwaitingFirstLayout = true; |
| @ViewDebug.ExportedProperty(category="recents") |
| boolean mInMeasureLayout = false; |
| @ViewDebug.ExportedProperty(category="recents") |
| boolean mEnterAnimationComplete = false; |
| @ViewDebug.ExportedProperty(category="recents") |
| boolean mTouchExplorationEnabled; |
| @ViewDebug.ExportedProperty(category="recents") |
| boolean mScreenPinningEnabled; |
| |
| // The stable stack bounds are the full bounds that we were measured with from RecentsView |
| @ViewDebug.ExportedProperty(category="recents") |
| private Rect mStableStackBounds = new Rect(); |
| // The current stack bounds are dynamic and may change as the user drags and drops |
| @ViewDebug.ExportedProperty(category="recents") |
| private Rect mStackBounds = new Rect(); |
| |
| private Rect mTmpRect = new Rect(); |
| private ArrayMap<Task.TaskKey, TaskView> mTmpTaskViewMap = new ArrayMap<>(); |
| private List<TaskView> mTmpTaskViews = new ArrayList<>(); |
| private TaskViewTransform mTmpTransform = new TaskViewTransform(); |
| private int[] mTmpIntPair = new int[2]; |
| |
| // A convenience update listener to request updating clipping of tasks |
| private ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = |
| new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| if (!mTaskViewsClipDirty) { |
| mTaskViewsClipDirty = true; |
| invalidate(); |
| } |
| } |
| }; |
| |
| // The drop targets for a task drag |
| private DropTarget mFreeformWorkspaceDropTarget = new DropTarget() { |
| @Override |
| public boolean acceptsDrop(int x, int y, int width, int height, boolean isCurrentTarget) { |
| // This drop target has a fixed bounds and should be checked last, so just fall through |
| // if it is the current target |
| if (!isCurrentTarget) { |
| return mLayoutAlgorithm.mFreeformRect.contains(x, y); |
| } |
| return false; |
| } |
| }; |
| |
| private DropTarget mStackDropTarget = new DropTarget() { |
| @Override |
| public boolean acceptsDrop(int x, int y, int width, int height, boolean isCurrentTarget) { |
| // This drop target has a fixed bounds and should be checked last, so just fall through |
| // if it is the current target |
| if (!isCurrentTarget) { |
| return mLayoutAlgorithm.mStackRect.contains(x, y); |
| } |
| return false; |
| } |
| }; |
| |
| public TaskStackView(Context context) { |
| super(context); |
| SystemServicesProxy ssp = Recents.getSystemServices(); |
| Resources res = context.getResources(); |
| |
| // Set the stack first |
| mStack.setCallbacks(this); |
| mViewPool = new ViewPool<>(context, this); |
| mInflater = LayoutInflater.from(context); |
| mLayoutAlgorithm = new TaskStackLayoutAlgorithm(context, this); |
| mStackScroller = new TaskStackViewScroller(context, this, mLayoutAlgorithm); |
| mTouchHandler = new TaskStackViewTouchHandler(context, this, mStackScroller); |
| mAnimationHelper = new TaskStackAnimationHelper(context, this); |
| mTaskCornerRadiusPx = res.getDimensionPixelSize( |
| R.dimen.recents_task_view_rounded_corners_radius); |
| mDividerSize = ssp.getDockedDividerSize(context); |
| |
| int taskBarDismissDozeDelaySeconds = getResources().getInteger( |
| R.integer.recents_task_bar_dismiss_delay_seconds); |
| mUIDozeTrigger = new DozeTrigger(taskBarDismissDozeDelaySeconds, new Runnable() { |
| @Override |
| public void run() { |
| // Show the task bar dismiss buttons |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| tv.startNoUserInteractionAnimation(); |
| } |
| } |
| }); |
| setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); |
| |
| mFreeformWorkspaceBackground = (GradientDrawable) getContext().getDrawable( |
| R.drawable.recents_freeform_workspace_bg); |
| mFreeformWorkspaceBackground.setCallback(this); |
| if (ssp.hasFreeformWorkspaceSupport()) { |
| mFreeformWorkspaceBackground.setColor( |
| getContext().getColor(R.color.recents_freeform_workspace_bg_color)); |
| } |
| } |
| |
| /** |
| * Called only if we are resuming Recents. |
| */ |
| void onResume(boolean isResumingFromVisible) { |
| if (!isResumingFromVisible) { |
| // Reset the focused task |
| resetFocusedTask(getFocusedTask()); |
| } |
| |
| // Reset the state of each of the task views |
| List<TaskView> taskViews = new ArrayList<>(); |
| taskViews.addAll(getTaskViews()); |
| taskViews.addAll(mViewPool.getViews()); |
| for (int i = taskViews.size() - 1; i >= 0; i--) { |
| taskViews.get(i).onResume(isResumingFromVisible); |
| } |
| |
| // Reset the stack state |
| readSystemFlags(); |
| mTaskViewsClipDirty = true; |
| mEnterAnimationComplete = false; |
| mUIDozeTrigger.stopDozing(); |
| if (isResumingFromVisible) { |
| // Animate in the freeform workspace |
| int ffBgAlpha = mLayoutAlgorithm.getStackState().freeformBackgroundAlpha; |
| animateFreeformWorkspaceBackgroundAlpha(ffBgAlpha, new AnimationProps(150, |
| Interpolators.FAST_OUT_SLOW_IN)); |
| } else { |
| mStackScroller.reset(); |
| mLayoutAlgorithm.reset(); |
| mAwaitingFirstLayout = true; |
| requestLayout(); |
| } |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1); |
| super.onAttachedToWindow(); |
| readSystemFlags(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| EventBus.getDefault().unregister(this); |
| } |
| |
| /** |
| * Sets the stack tasks of this TaskStackView from the given TaskStack. |
| */ |
| public void setTasks(TaskStack stack, boolean notifyStackChanges, boolean relayoutTaskStack) { |
| boolean isInitialized = mLayoutAlgorithm.isInitialized(); |
| mStack.setTasks(getContext(), stack.computeAllTasksList(), |
| notifyStackChanges && isInitialized); |
| if (isInitialized) { |
| // Only update the layout if we are notifying, otherwise, we will update it in the next |
| // measure/layout pass |
| updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET); |
| updateToInitialState(); |
| |
| if (relayoutTaskStack) { |
| relayoutTaskViews(AnimationProps.IMMEDIATE); |
| |
| // Rebind all the task views. This will not trigger new resources to be loaded |
| // unless they have actually changed |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| bindTaskView(tv, tv.getTask()); |
| } |
| } |
| } |
| } |
| |
| /** Returns the task stack. */ |
| public TaskStack getStack() { |
| return mStack; |
| } |
| |
| /** |
| * Updates this TaskStackView to the initial state. |
| */ |
| public void updateToInitialState() { |
| mStackScroller.setStackScrollToInitialState(); |
| mLayoutAlgorithm.updateToInitialState(mStack.getStackTasks()); |
| } |
| |
| /** Updates the list of task views */ |
| void updateTaskViewsList() { |
| mTaskViews.clear(); |
| int childCount = getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| View v = getChildAt(i); |
| if (v instanceof TaskView) { |
| mTaskViews.add((TaskView) v); |
| } |
| } |
| } |
| |
| /** Gets the list of task views */ |
| List<TaskView> getTaskViews() { |
| return mTaskViews; |
| } |
| |
| /** |
| * Returns the front most task view. |
| * |
| * @param stackTasksOnly if set, will return the front most task view in the stack (by default |
| * the front most task view will be freeform since they are placed above |
| * stack tasks) |
| */ |
| private TaskView getFrontMostTaskView(boolean stackTasksOnly) { |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = taskViewCount - 1; i >= 0; i--) { |
| TaskView tv = taskViews.get(i); |
| Task task = tv.getTask(); |
| if (stackTasksOnly && task.isFreeformTask()) { |
| continue; |
| } |
| return tv; |
| } |
| return null; |
| } |
| |
| /** |
| * Finds the child view given a specific {@param task}. |
| */ |
| public TaskView getChildViewForTask(Task t) { |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| if (tv.getTask() == t) { |
| return tv; |
| } |
| } |
| return null; |
| } |
| |
| /** Returns the stack algorithm for this task stack. */ |
| public TaskStackLayoutAlgorithm getStackAlgorithm() { |
| return mLayoutAlgorithm; |
| } |
| |
| /** |
| * Adds a task to the ignored set. |
| */ |
| void addIgnoreTask(Task task) { |
| mIgnoreTasks.add(task.key); |
| } |
| |
| /** |
| * Removes a task from the ignored set. |
| */ |
| void removeIgnoreTask(Task task) { |
| mIgnoreTasks.remove(task.key); |
| } |
| |
| /** |
| * Returns whether the specified {@param task} is ignored. |
| */ |
| boolean isIgnoredTask(Task task) { |
| return mIgnoreTasks.contains(task.key); |
| } |
| |
| /** |
| * Computes the task transforms at the current stack scroll for all visible tasks. If a valid |
| * target stack scroll is provided (ie. is different than {@param curStackScroll}), then the |
| * visible range includes all tasks at the target stack scroll. This is useful for ensure that |
| * all views necessary for a transition or animation will be visible at the start. |
| * |
| * This call ignores freeform tasks. |
| * |
| * @param taskTransforms The set of task view transforms to reuse, this list will be sized to |
| * match the size of {@param tasks} |
| * @param tasks The set of tasks for which to generate transforms |
| * @param curStackScroll The current stack scroll |
| * @param targetStackScroll The stack scroll that we anticipate we are going to be scrolling to. |
| * The range of the union of the visible views at the current and |
| * target stack scrolls will be returned. |
| * @param ignoreTasksSet The set of tasks to skip for purposes of calculaing the visible range. |
| * Transforms will still be calculated for the ignore tasks. |
| * @return the front and back most visible task indices (there may be non visible tasks in |
| * between this range) |
| */ |
| int[] computeVisibleTaskTransforms(ArrayList<TaskViewTransform> taskTransforms, |
| ArrayList<Task> tasks, float curStackScroll, float targetStackScroll, |
| ArraySet<Task.TaskKey> ignoreTasksSet, boolean ignoreTaskOverrides) { |
| int taskCount = tasks.size(); |
| int[] visibleTaskRange = mTmpIntPair; |
| visibleTaskRange[0] = -1; |
| visibleTaskRange[1] = -1; |
| boolean useTargetStackScroll = Float.compare(curStackScroll, targetStackScroll) != 0; |
| |
| // We can reuse the task transforms where possible to reduce object allocation |
| Utilities.matchTaskListSize(tasks, taskTransforms); |
| |
| // Update the stack transforms |
| TaskViewTransform frontTransform = null; |
| TaskViewTransform frontTransformAtTarget = null; |
| TaskViewTransform transform = null; |
| TaskViewTransform transformAtTarget = null; |
| for (int i = taskCount - 1; i >= 0; i--) { |
| Task task = tasks.get(i); |
| |
| // Calculate the current and (if necessary) the target transform for the task |
| transform = mLayoutAlgorithm.getStackTransform(task, curStackScroll, |
| taskTransforms.get(i), frontTransform, ignoreTaskOverrides); |
| if (useTargetStackScroll && !transform.visible) { |
| // If we have a target stack scroll and the task is not currently visible, then we |
| // just update the transform at the new scroll |
| // TODO: Optimize this |
| transformAtTarget = mLayoutAlgorithm.getStackTransform(task, |
| targetStackScroll, new TaskViewTransform(), frontTransformAtTarget); |
| if (transformAtTarget.visible) { |
| transform.copyFrom(transformAtTarget); |
| } |
| } |
| |
| // For ignore tasks, only calculate the stack transform and skip the calculation of the |
| // visible stack indices |
| if (ignoreTasksSet.contains(task.key)) { |
| continue; |
| } |
| |
| // For freeform tasks, only calculate the stack transform and skip the calculation of |
| // the visible stack indices |
| if (task.isFreeformTask()) { |
| continue; |
| } |
| |
| frontTransform = transform; |
| frontTransformAtTarget = transformAtTarget; |
| if (transform.visible) { |
| if (visibleTaskRange[0] < 0) { |
| visibleTaskRange[0] = i; |
| } |
| visibleTaskRange[1] = i; |
| } |
| } |
| return visibleTaskRange; |
| } |
| |
| /** |
| * Binds the visible {@link TaskView}s at the given target scroll. |
| */ |
| void bindVisibleTaskViews(float targetStackScroll) { |
| bindVisibleTaskViews(targetStackScroll, mIgnoreTasks, false /* ignoreTaskOverrides */); |
| } |
| |
| void bindVisibleTaskViews(float targetStackScroll, boolean ignoreTaskOverrides) { |
| bindVisibleTaskViews(targetStackScroll, mIgnoreTasks, ignoreTaskOverrides); |
| } |
| |
| /** |
| * Synchronizes the set of children {@link TaskView}s to match the visible set of tasks in the |
| * current {@link TaskStack}. This call does not continue on to update their position to the |
| * computed {@link TaskViewTransform}s of the visible range, but only ensures that they will |
| * be added/removed from the view hierarchy and placed in the correct Z order and initial |
| * position (if not currently on screen). |
| * |
| * @param targetStackScroll If provided, will ensure that the set of visible {@link TaskView}s |
| * includes those visible at the current stack scroll, and all at the |
| * target stack scroll. |
| * @param ignoreTasksSet The set of tasks to ignore in this rebinding of the visible |
| * {@link TaskView}s |
| * @param ignoreTaskOverrides If set, the visible task computation will get the transforms for |
| * tasks at their non-overridden task progress |
| */ |
| void bindVisibleTaskViews(float targetStackScroll, ArraySet<Task.TaskKey> ignoreTasksSet, |
| boolean ignoreTaskOverrides) { |
| // Get all the task transforms |
| ArrayList<Task> tasks = mStack.getStackTasks(); |
| int[] visibleTaskRange = computeVisibleTaskTransforms(mCurrentTaskTransforms, tasks, |
| mStackScroller.getStackScroll(), targetStackScroll, ignoreTasksSet, |
| ignoreTaskOverrides); |
| |
| // Return all the invisible children to the pool |
| mTmpTaskViewMap.clear(); |
| List<TaskView> taskViews = getTaskViews(); |
| int lastFocusedTaskIndex = -1; |
| int taskViewCount = taskViews.size(); |
| for (int i = taskViewCount - 1; i >= 0; i--) { |
| TaskView tv = taskViews.get(i); |
| Task task = tv.getTask(); |
| |
| // Skip ignored tasks |
| if (ignoreTasksSet.contains(task.key)) { |
| continue; |
| } |
| |
| // It is possible for the set of lingering TaskViews to differ from the stack if the |
| // stack was updated before the relayout. If the task view is no longer in the stack, |
| // then just return it back to the view pool. |
| int taskIndex = mStack.indexOfStackTask(task); |
| TaskViewTransform transform = null; |
| if (taskIndex != -1) { |
| transform = mCurrentTaskTransforms.get(taskIndex); |
| } |
| |
| if (task.isFreeformTask() || (transform != null && transform.visible)) { |
| mTmpTaskViewMap.put(task.key, tv); |
| } else { |
| if (mTouchExplorationEnabled) { |
| lastFocusedTaskIndex = taskIndex; |
| resetFocusedTask(task); |
| } |
| mViewPool.returnViewToPool(tv); |
| } |
| } |
| |
| // Pick up all the newly visible children |
| for (int i = tasks.size() - 1; i >= 0; i--) { |
| Task task = tasks.get(i); |
| TaskViewTransform transform = mCurrentTaskTransforms.get(i); |
| |
| // Skip ignored tasks |
| if (ignoreTasksSet.contains(task.key)) { |
| continue; |
| } |
| |
| // Skip the invisible non-freeform stack tasks |
| if (!task.isFreeformTask() && !transform.visible) { |
| continue; |
| } |
| |
| TaskView tv = mTmpTaskViewMap.get(task.key); |
| if (tv == null) { |
| tv = mViewPool.pickUpViewFromPool(task, task); |
| if (task.isFreeformTask()) { |
| tv.updateViewPropertiesToTaskTransform(transform, AnimationProps.IMMEDIATE, |
| mRequestUpdateClippingListener); |
| } else { |
| if (transform.rect.top <= mLayoutAlgorithm.mStackRect.top) { |
| tv.updateViewPropertiesToTaskTransform( |
| mLayoutAlgorithm.getBackOfStackTransform(), |
| AnimationProps.IMMEDIATE, mRequestUpdateClippingListener); |
| } else { |
| tv.updateViewPropertiesToTaskTransform( |
| mLayoutAlgorithm.getFrontOfStackTransform(), |
| AnimationProps.IMMEDIATE, mRequestUpdateClippingListener); |
| } |
| } |
| } else { |
| // Reattach it in the right z order |
| final int taskIndex = mStack.indexOfStackTask(task); |
| final int insertIndex = findTaskViewInsertIndex(task, taskIndex); |
| if (insertIndex != getTaskViews().indexOf(tv)){ |
| detachViewFromParent(tv); |
| attachViewToParent(tv, insertIndex, tv.getLayoutParams()); |
| updateTaskViewsList(); |
| } |
| } |
| } |
| |
| // Update the focus if the previous focused task was returned to the view pool |
| if (lastFocusedTaskIndex != -1) { |
| if (lastFocusedTaskIndex < visibleTaskRange[1]) { |
| setFocusedTask(visibleTaskRange[1], false /* scrollToTask */, |
| true /* requestViewFocus */); |
| } else { |
| setFocusedTask(visibleTaskRange[0], false /* scrollToTask */, |
| true /* requestViewFocus */); |
| } |
| } |
| } |
| |
| /** |
| * Relayout the the visible {@link TaskView}s to their current transforms as specified by the |
| * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any |
| * animations that are current running on those task views, and will ensure that the children |
| * {@link TaskView}s will match the set of visible tasks in the stack. |
| * |
| * @see #relayoutTaskViews(AnimationProps, ArraySet<Task.TaskKey>) |
| */ |
| void relayoutTaskViews(AnimationProps animation) { |
| relayoutTaskViews(animation, mIgnoreTasks); |
| } |
| |
| /** |
| * Relayout the the visible {@link TaskView}s to their current transforms as specified by the |
| * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any |
| * animations that are current running on those task views, and will ensure that the children |
| * {@link TaskView}s will match the set of visible tasks in the stack. |
| * |
| * @param ignoreTasksSet the set of tasks to ignore in the relayout |
| */ |
| void relayoutTaskViews(AnimationProps animation, ArraySet<Task.TaskKey> ignoreTasksSet) { |
| // If we had a deferred animation, cancel that |
| mDeferredTaskViewLayoutAnimation = null; |
| |
| // Synchronize the current set of TaskViews |
| bindVisibleTaskViews(mStackScroller.getStackScroll(), ignoreTasksSet, |
| false /* ignoreTaskOverrides */); |
| |
| // Animate them to their final transforms with the given animation |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| int taskIndex = mStack.indexOfStackTask(tv.getTask()); |
| TaskViewTransform transform = mCurrentTaskTransforms.get(taskIndex); |
| |
| if (ignoreTasksSet.contains(tv.getTask().key)) { |
| continue; |
| } |
| |
| updateTaskViewToTransform(tv, transform, animation); |
| } |
| } |
| |
| /** |
| * Posts an update to synchronize the {@link TaskView}s with the stack on the next frame. |
| */ |
| void relayoutTaskViewsOnNextFrame(AnimationProps animation) { |
| mDeferredTaskViewLayoutAnimation = animation; |
| invalidate(); |
| } |
| |
| /** |
| * Called to update a specific {@link TaskView} to a given {@link TaskViewTransform} with a |
| * given set of {@link AnimationProps} properties. |
| */ |
| public void updateTaskViewToTransform(TaskView taskView, TaskViewTransform transform, |
| AnimationProps animation) { |
| if (taskView.isAnimatingTo(transform)) { |
| return; |
| } |
| taskView.cancelTransformAnimation(); |
| taskView.updateViewPropertiesToTaskTransform(transform, animation, |
| mRequestUpdateClippingListener); |
| } |
| |
| /** |
| * Returns the current task transforms of all tasks, falling back to the stack layout if there |
| * is no {@link TaskView} for the task. |
| */ |
| public void getCurrentTaskTransforms(ArrayList<Task> tasks, |
| ArrayList<TaskViewTransform> transformsOut) { |
| Utilities.matchTaskListSize(tasks, transformsOut); |
| int focusState = mLayoutAlgorithm.getFocusState(); |
| for (int i = tasks.size() - 1; i >= 0; i--) { |
| Task task = tasks.get(i); |
| TaskViewTransform transform = transformsOut.get(i); |
| TaskView tv = getChildViewForTask(task); |
| if (tv != null) { |
| transform.fillIn(tv); |
| } else { |
| mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(), |
| focusState, transform, null, true /* forceUpdate */, |
| false /* ignoreTaskOverrides */); |
| } |
| transform.visible = true; |
| } |
| } |
| |
| /** |
| * Returns the task transforms for all the tasks in the stack if the stack was at the given |
| * {@param stackScroll} and {@param focusState}. |
| */ |
| public void getLayoutTaskTransforms(float stackScroll, int focusState, ArrayList<Task> tasks, |
| ArrayList<TaskViewTransform> transformsOut) { |
| Utilities.matchTaskListSize(tasks, transformsOut); |
| for (int i = tasks.size() - 1; i >= 0; i--) { |
| Task task = tasks.get(i); |
| TaskViewTransform transform = transformsOut.get(i); |
| mLayoutAlgorithm.getStackTransform(task, stackScroll, focusState, transform, null, |
| true /* forceUpdate */, true /* ignoreTaskOverrides */); |
| transform.visible = true; |
| } |
| } |
| |
| /** |
| * Cancels the next deferred task view layout. |
| */ |
| void cancelDeferredTaskViewLayoutAnimation() { |
| mDeferredTaskViewLayoutAnimation = null; |
| } |
| |
| /** |
| * Cancels all {@link TaskView} animations. |
| * |
| * @see #cancelAllTaskViewAnimations(ArraySet<Task.TaskKey>) |
| */ |
| void cancelAllTaskViewAnimations() { |
| cancelAllTaskViewAnimations(mIgnoreTasks); |
| } |
| |
| /** |
| * Cancels all {@link TaskView} animations. |
| * |
| * @param ignoreTasksSet The set of tasks to continue running their animations. |
| */ |
| void cancelAllTaskViewAnimations(ArraySet<Task.TaskKey> ignoreTasksSet) { |
| List<TaskView> taskViews = getTaskViews(); |
| for (int i = taskViews.size() - 1; i >= 0; i--) { |
| final TaskView tv = taskViews.get(i); |
| if (!ignoreTasksSet.contains(tv.getTask().key)) { |
| tv.cancelTransformAnimation(); |
| } |
| } |
| } |
| |
| /** |
| * Updates the clip for each of the task views from back to front. |
| */ |
| private void clipTaskViews() { |
| RecentsConfiguration config = Recents.getConfiguration(); |
| |
| // Update the clip on each task child |
| List<TaskView> taskViews = getTaskViews(); |
| TaskView tmpTv = null; |
| TaskView prevVisibleTv = null; |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| TaskView frontTv = null; |
| int clipBottom = 0; |
| |
| if (isIgnoredTask(tv.getTask())) { |
| // For each of the ignore tasks, update the translationZ of its TaskView to be |
| // between the translationZ of the tasks immediately underneath it |
| if (prevVisibleTv != null) { |
| tv.setTranslationZ(Math.max(tv.getTranslationZ(), |
| prevVisibleTv.getTranslationZ() + 0.1f)); |
| } |
| } |
| |
| if (i < (taskViewCount - 1) && tv.shouldClipViewInStack()) { |
| // Find the next view to clip against |
| for (int j = i + 1; j < taskViewCount; j++) { |
| tmpTv = taskViews.get(j); |
| |
| if (tmpTv.shouldClipViewInStack()) { |
| frontTv = tmpTv; |
| break; |
| } |
| } |
| |
| // Clip against the next view, this is just an approximation since we are |
| // stacked and we can make assumptions about the visibility of the this |
| // task relative to the ones in front of it. |
| if (frontTv != null) { |
| float taskBottom = tv.getBottom(); |
| float frontTaskTop = frontTv.getTop(); |
| if (frontTaskTop < taskBottom) { |
| // Map the stack view space coordinate (the rects) to view space |
| clipBottom = (int) (taskBottom - frontTaskTop) - mTaskCornerRadiusPx; |
| } |
| } |
| } |
| tv.getViewBounds().setClipBottom(clipBottom); |
| tv.mThumbnailView.updateThumbnailVisibility(clipBottom - tv.getPaddingBottom()); |
| prevVisibleTv = tv; |
| } |
| mTaskViewsClipDirty = false; |
| } |
| |
| /** |
| * Updates the layout algorithm min and max virtual scroll bounds. |
| * |
| * @see #updateLayoutAlgorithm(boolean, ArraySet<Task.TaskKey>) |
| */ |
| public void updateLayoutAlgorithm(boolean boundScrollToNewMinMax) { |
| updateLayoutAlgorithm(boundScrollToNewMinMax, mIgnoreTasks); |
| } |
| |
| /** |
| * Updates the min and max virtual scroll bounds. |
| * |
| * @param ignoreTasksSet the set of tasks to ignore in the relayout |
| */ |
| public void updateLayoutAlgorithm(boolean boundScrollToNewMinMax, |
| ArraySet<Task.TaskKey> ignoreTasksSet) { |
| // Compute the min and max scroll values |
| mLayoutAlgorithm.update(mStack, ignoreTasksSet); |
| |
| // Update the freeform workspace background |
| SystemServicesProxy ssp = Recents.getSystemServices(); |
| if (ssp.hasFreeformWorkspaceSupport()) { |
| mTmpRect.set(mLayoutAlgorithm.mFreeformRect); |
| mFreeformWorkspaceBackground.setBounds(mTmpRect); |
| } |
| |
| if (boundScrollToNewMinMax) { |
| mStackScroller.boundScroll(); |
| } |
| } |
| |
| /** Returns the scroller. */ |
| public TaskStackViewScroller getScroller() { |
| return mStackScroller; |
| } |
| |
| /** |
| * Sets the focused task to the provided (bounded taskIndex). |
| * |
| * @return whether or not the stack will scroll as a part of this focus change |
| */ |
| private boolean setFocusedTask(int taskIndex, boolean scrollToTask, |
| final boolean requestViewFocus) { |
| return setFocusedTask(taskIndex, scrollToTask, requestViewFocus, 0); |
| } |
| |
| /** |
| * Sets the focused task to the provided (bounded focusTaskIndex). |
| * |
| * @return whether or not the stack will scroll as a part of this focus change |
| */ |
| private boolean setFocusedTask(int focusTaskIndex, boolean scrollToTask, |
| boolean requestViewFocus, int timerIndicatorDuration) { |
| // Find the next task to focus |
| int newFocusedTaskIndex = mStack.getTaskCount() > 0 ? |
| Utilities.clamp(focusTaskIndex, 0, mStack.getTaskCount() - 1) : -1; |
| final Task newFocusedTask = (newFocusedTaskIndex != -1) ? |
| mStack.getStackTasks().get(newFocusedTaskIndex) : null; |
| |
| // Reset the last focused task state if changed |
| if (mFocusedTask != null) { |
| // Cancel the timer indicator, if applicable |
| if (timerIndicatorDuration > 0) { |
| final TaskView tv = getChildViewForTask(mFocusedTask); |
| if (tv != null) { |
| tv.getHeaderView().cancelFocusTimerIndicator(); |
| } |
| } |
| |
| resetFocusedTask(mFocusedTask); |
| } |
| |
| boolean willScroll = false; |
| mFocusedTask = newFocusedTask; |
| |
| if (newFocusedTask != null) { |
| // Start the timer indicator, if applicable |
| if (timerIndicatorDuration > 0) { |
| final TaskView tv = getChildViewForTask(mFocusedTask); |
| if (tv != null) { |
| tv.getHeaderView().startFocusTimerIndicator(timerIndicatorDuration); |
| } else { |
| // The view is null; set a flag for later |
| mStartTimerIndicatorDuration = timerIndicatorDuration; |
| } |
| } |
| |
| if (scrollToTask) { |
| // Cancel any running enter animations at this point when we scroll or change focus |
| if (!mEnterAnimationComplete) { |
| cancelAllTaskViewAnimations(); |
| } |
| |
| mLayoutAlgorithm.clearUnfocusedTaskOverrides(); |
| willScroll = mAnimationHelper.startScrollToFocusedTaskAnimation(newFocusedTask, |
| requestViewFocus); |
| } else { |
| // Focus the task view |
| TaskView newFocusedTaskView = getChildViewForTask(newFocusedTask); |
| if (newFocusedTaskView != null) { |
| newFocusedTaskView.setFocusedState(true, requestViewFocus); |
| } |
| } |
| } |
| return willScroll; |
| } |
| |
| /** |
| * Sets the focused task relative to the currently focused task. |
| * |
| * @param forward whether to go to the next task in the stack (along the curve) or the previous |
| * @param stackTasksOnly if set, will ensure that the traversal only goes along stack tasks, and |
| * if the currently focused task is not a stack task, will set the focus |
| * to the first visible stack task |
| * @param animated determines whether to actually draw the highlight along with the change in |
| * focus. |
| */ |
| public void setRelativeFocusedTask(boolean forward, boolean stackTasksOnly, boolean animated) { |
| setRelativeFocusedTask(forward, stackTasksOnly, animated, false); |
| } |
| |
| /** |
| * Sets the focused task relative to the currently focused task. |
| * |
| * @param forward whether to go to the next task in the stack (along the curve) or the previous |
| * @param stackTasksOnly if set, will ensure that the traversal only goes along stack tasks, and |
| * if the currently focused task is not a stack task, will set the focus |
| * to the first visible stack task |
| * @param animated determines whether to actually draw the highlight along with the change in |
| * focus. |
| * @param cancelWindowAnimations if set, will attempt to cancel window animations if a scroll |
| * happens. |
| */ |
| public void setRelativeFocusedTask(boolean forward, boolean stackTasksOnly, boolean animated, |
| boolean cancelWindowAnimations) { |
| setRelativeFocusedTask(forward, stackTasksOnly, animated, cancelWindowAnimations, 0); |
| } |
| |
| /** |
| * Sets the focused task relative to the currently focused task. |
| * |
| * @param forward whether to go to the next task in the stack (along the curve) or the previous |
| * @param stackTasksOnly if set, will ensure that the traversal only goes along stack tasks, and |
| * if the currently focused task is not a stack task, will set the focus |
| * to the first visible stack task |
| * @param animated determines whether to actually draw the highlight along with the change in |
| * focus. |
| * @param cancelWindowAnimations if set, will attempt to cancel window animations if a scroll |
| * happens. |
| * @param timerIndicatorDuration the duration to initialize the auto-advance timer indicator |
| */ |
| public void setRelativeFocusedTask(boolean forward, boolean stackTasksOnly, boolean animated, |
| boolean cancelWindowAnimations, |
| int timerIndicatorDuration) { |
| int newIndex = mStack.indexOfStackTask(mFocusedTask); |
| if (mFocusedTask != null) { |
| if (stackTasksOnly) { |
| List<Task> tasks = mStack.getStackTasks(); |
| if (mFocusedTask.isFreeformTask()) { |
| // Try and focus the front most stack task |
| TaskView tv = getFrontMostTaskView(stackTasksOnly); |
| if (tv != null) { |
| newIndex = mStack.indexOfStackTask(tv.getTask()); |
| } |
| } else { |
| // Try the next task if it is a stack task |
| int tmpNewIndex = newIndex + (forward ? -1 : 1); |
| if (0 <= tmpNewIndex && tmpNewIndex < tasks.size()) { |
| Task t = tasks.get(tmpNewIndex); |
| if (!t.isFreeformTask()) { |
| newIndex = tmpNewIndex; |
| } |
| } |
| } |
| } else { |
| // No restrictions, lets just move to the new task (looping forward/backwards if |
| // necessary) |
| int taskCount = mStack.getTaskCount(); |
| newIndex = (newIndex + (forward ? -1 : 1) + taskCount) % taskCount; |
| } |
| } else { |
| // We don't have a focused task |
| float stackScroll = mStackScroller.getStackScroll(); |
| ArrayList<Task> tasks = mStack.getStackTasks(); |
| int taskCount = tasks.size(); |
| if (forward) { |
| // Walk backwards and focus the next task smaller than the current stack scroll |
| for (newIndex = taskCount - 1; newIndex >= 0; newIndex--) { |
| float taskP = mLayoutAlgorithm.getStackScrollForTask(tasks.get(newIndex)); |
| if (Float.compare(taskP, stackScroll) <= 0) { |
| break; |
| } |
| } |
| } else { |
| // Walk forwards and focus the next task larger than the current stack scroll |
| for (newIndex = 0; newIndex < taskCount; newIndex++) { |
| float taskP = mLayoutAlgorithm.getStackScrollForTask(tasks.get(newIndex)); |
| if (Float.compare(taskP, stackScroll) >= 0) { |
| break; |
| } |
| } |
| } |
| } |
| if (newIndex != -1) { |
| boolean willScroll = setFocusedTask(newIndex, true /* scrollToTask */, |
| true /* requestViewFocus */, timerIndicatorDuration); |
| if (willScroll && cancelWindowAnimations) { |
| // As we iterate to the next/previous task, cancel any current/lagging window |
| // transition animations |
| EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(null)); |
| } |
| } |
| } |
| |
| /** |
| * Resets the focused task. |
| */ |
| void resetFocusedTask(Task task) { |
| if (task != null) { |
| TaskView tv = getChildViewForTask(task); |
| if (tv != null) { |
| tv.setFocusedState(false, false /* requestViewFocus */); |
| } |
| } |
| mFocusedTask = null; |
| } |
| |
| /** |
| * Returns the focused task. |
| */ |
| Task getFocusedTask() { |
| return mFocusedTask; |
| } |
| |
| @Override |
| public void onInitializeAccessibilityEvent(AccessibilityEvent event) { |
| super.onInitializeAccessibilityEvent(event); |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| if (taskViewCount > 0) { |
| TaskView backMostTask = taskViews.get(0); |
| TaskView frontMostTask = taskViews.get(taskViewCount - 1); |
| event.setFromIndex(mStack.indexOfStackTask(backMostTask.getTask())); |
| event.setToIndex(mStack.indexOfStackTask(frontMostTask.getTask())); |
| event.setContentDescription(frontMostTask.getTask().title); |
| } |
| event.setItemCount(mStack.getTaskCount()); |
| event.setScrollY(mStackScroller.mScroller.getCurrY()); |
| event.setMaxScrollY(mStackScroller.progressToScrollRange(mLayoutAlgorithm.mMaxScrollP)); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(info); |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| if (taskViewCount > 1 && mFocusedTask != null) { |
| info.setScrollable(true); |
| int focusedTaskIndex = mStack.indexOfStackTask(mFocusedTask); |
| if (focusedTaskIndex > 0) { |
| info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); |
| } |
| if (focusedTaskIndex < mStack.getTaskCount() - 1) { |
| info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); |
| } |
| } |
| } |
| |
| @Override |
| protected Parcelable onSaveInstanceState() { |
| Bundle savedState = new Bundle(); |
| savedState.putParcelable(KEY_SAVED_STATE_SUPER, super.onSaveInstanceState()); |
| savedState.putInt(KEY_SAVED_STATE_LAYOUT_FOCUSED_STATE, mLayoutAlgorithm.getFocusState()); |
| savedState.putFloat(KEY_SAVED_STATE_LAYOUT_STACK_SCROLL, mStackScroller.getStackScroll()); |
| return super.onSaveInstanceState(); |
| } |
| |
| @Override |
| protected void onRestoreInstanceState(Parcelable state) { |
| Bundle savedState = (Bundle) state; |
| super.onRestoreInstanceState(savedState.getParcelable(KEY_SAVED_STATE_SUPER)); |
| |
| mLayoutAlgorithm.setFocusState(savedState.getInt(KEY_SAVED_STATE_LAYOUT_FOCUSED_STATE)); |
| mStackScroller.setStackScroll(savedState.getFloat(KEY_SAVED_STATE_LAYOUT_STACK_SCROLL)); |
| } |
| |
| @Override |
| public CharSequence getAccessibilityClassName() { |
| return TaskStackView.class.getName(); |
| } |
| |
| @Override |
| public boolean performAccessibilityAction(int action, Bundle arguments) { |
| if (super.performAccessibilityAction(action, arguments)) { |
| return true; |
| } |
| switch (action) { |
| case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { |
| setRelativeFocusedTask(true, false /* stackTasksOnly */, false /* animated */); |
| return true; |
| } |
| case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { |
| setRelativeFocusedTask(false, false /* stackTasksOnly */, false /* animated */); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| return mTouchHandler.onInterceptTouchEvent(ev); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| return mTouchHandler.onTouchEvent(ev); |
| } |
| |
| @Override |
| public boolean onGenericMotionEvent(MotionEvent ev) { |
| return mTouchHandler.onGenericMotionEvent(ev); |
| } |
| |
| @Override |
| public void computeScroll() { |
| if (mStackScroller.computeScroll()) { |
| // Notify accessibility |
| sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED); |
| } |
| if (mDeferredTaskViewLayoutAnimation != null) { |
| relayoutTaskViews(mDeferredTaskViewLayoutAnimation); |
| mTaskViewsClipDirty = true; |
| mDeferredTaskViewLayoutAnimation = null; |
| } |
| if (mTaskViewsClipDirty) { |
| clipTaskViews(); |
| } |
| } |
| |
| /** |
| * Computes the maximum number of visible tasks and thumbnails. Requires that |
| * updateLayoutForStack() is called first. |
| */ |
| public TaskStackLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() { |
| return mLayoutAlgorithm.computeStackVisibilityReport(mStack.getStackTasks()); |
| } |
| |
| /** |
| * Updates the expected task stack bounds for this stack view. |
| */ |
| public void setTaskStackBounds(Rect taskStackBounds, Rect systemInsets) { |
| // We can get spurious measure passes with the old bounds when docking, and since we are |
| // using the current stack bounds during drag and drop, don't overwrite them until we |
| // actually get new bounds |
| boolean requiresLayout = false; |
| if (!taskStackBounds.equals(mStableStackBounds)) { |
| mStableStackBounds.set(taskStackBounds); |
| mStackBounds.set(taskStackBounds); |
| requiresLayout = true; |
| } |
| if (!systemInsets.equals(mLayoutAlgorithm.mSystemInsets)) { |
| mLayoutAlgorithm.setSystemInsets(systemInsets); |
| requiresLayout = true; |
| } |
| if (requiresLayout) { |
| requestLayout(); |
| } |
| } |
| |
| /** |
| * This is called with the full window width and height to allow stack view children to |
| * perform the full screen transition down. |
| */ |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| mInMeasureLayout = true; |
| int width = MeasureSpec.getSize(widthMeasureSpec); |
| int height = MeasureSpec.getSize(heightMeasureSpec); |
| |
| // Compute the rects in the stack algorithm |
| mLayoutAlgorithm.initialize(mStackBounds, |
| TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); |
| updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET); |
| |
| // If this is the first layout, then scroll to the front of the stack, then update the |
| // TaskViews with the stack so that we can lay them out |
| // TODO: The second check is a workaround for wacky layouts that we get while docking via |
| // long pressing the recents button |
| if (mAwaitingFirstLayout || |
| (mStackScroller.getStackScroll() == mLayoutAlgorithm.mInitialScrollP)) { |
| updateToInitialState(); |
| } |
| |
| // Rebind all the views, including the ignore ones |
| bindVisibleTaskViews(mStackScroller.getStackScroll(), EMPTY_TASK_SET, |
| false /* ignoreTaskOverrides */); |
| |
| // Measure each of the TaskViews |
| mTmpTaskViews.clear(); |
| mTmpTaskViews.addAll(getTaskViews()); |
| mTmpTaskViews.addAll(mViewPool.getViews()); |
| int taskViewCount = mTmpTaskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| measureTaskView(mTmpTaskViews.get(i)); |
| } |
| |
| setMeasuredDimension(width, height); |
| mInMeasureLayout = false; |
| } |
| |
| /** |
| * Measures a TaskView. |
| */ |
| private void measureTaskView(TaskView tv) { |
| if (tv.getBackground() != null) { |
| tv.getBackground().getPadding(mTmpRect); |
| } else { |
| mTmpRect.setEmpty(); |
| } |
| tv.measure( |
| MeasureSpec.makeMeasureSpec( |
| mLayoutAlgorithm.mTaskRect.width() + mTmpRect.left + mTmpRect.right, |
| MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec( |
| mLayoutAlgorithm.mTaskRect.height() + mTmpRect.top + mTmpRect.bottom, |
| MeasureSpec.EXACTLY)); |
| } |
| |
| /** |
| * This is called with the size of the space not including the top or right insets, or the |
| * search bar height in portrait (but including the search bar width in landscape, since we want |
| * to draw under it. |
| */ |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| // Layout each of the TaskViews |
| mTmpTaskViews.clear(); |
| mTmpTaskViews.addAll(getTaskViews()); |
| mTmpTaskViews.addAll(mViewPool.getViews()); |
| int taskViewCount = mTmpTaskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| layoutTaskView(mTmpTaskViews.get(i)); |
| } |
| |
| if (changed) { |
| if (mStackScroller.isScrollOutOfBounds()) { |
| mStackScroller.boundScroll(); |
| } |
| } |
| // Relayout all of the task views including the ignored ones |
| relayoutTaskViews(AnimationProps.IMMEDIATE, EMPTY_TASK_SET); |
| clipTaskViews(); |
| |
| if (mAwaitingFirstLayout || !mEnterAnimationComplete) { |
| mAwaitingFirstLayout = false; |
| onFirstLayout(); |
| } |
| } |
| |
| /** |
| * Lays out a TaskView. |
| */ |
| private void layoutTaskView(TaskView tv) { |
| if (tv.getBackground() != null) { |
| tv.getBackground().getPadding(mTmpRect); |
| } else { |
| mTmpRect.setEmpty(); |
| } |
| Rect taskRect = mLayoutAlgorithm.mTaskRect; |
| tv.layout(taskRect.left - mTmpRect.left, taskRect.top - mTmpRect.top, |
| taskRect.right + mTmpRect.right, taskRect.bottom + mTmpRect.bottom); |
| } |
| |
| /** Handler for the first layout. */ |
| void onFirstLayout() { |
| // Setup the view for the enter animation |
| mAnimationHelper.prepareForEnterAnimation(); |
| |
| // Animate in the freeform workspace |
| int ffBgAlpha = mLayoutAlgorithm.getStackState().freeformBackgroundAlpha; |
| animateFreeformWorkspaceBackgroundAlpha(ffBgAlpha, new AnimationProps(150, |
| Interpolators.FAST_OUT_SLOW_IN)); |
| |
| // Set the task focused state without requesting view focus, and leave the focus animations |
| // until after the enter-animation |
| RecentsConfiguration config = Recents.getConfiguration(); |
| RecentsActivityLaunchState launchState = config.getLaunchState(); |
| int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount()); |
| if (focusedTaskIndex != -1) { |
| setFocusedTask(focusedTaskIndex, false /* scrollToTask */, |
| false /* requestViewFocus */); |
| } |
| |
| // Update the history button visibility |
| if (shouldShowHistoryButton() && |
| mStackScroller.getStackScroll() < SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD) { |
| EventBus.getDefault().send(new ShowHistoryButtonEvent(false /* translate */)); |
| } else { |
| EventBus.getDefault().send(new HideHistoryButtonEvent()); |
| } |
| } |
| |
| public boolean isTouchPointInView(float x, float y, TaskView tv) { |
| mTmpRect.set(tv.getLeft(), tv.getTop(), tv.getRight(), tv.getBottom()); |
| mTmpRect.offset((int) tv.getTranslationX(), (int) tv.getTranslationY()); |
| return mTmpRect.contains((int) x, (int) y); |
| } |
| |
| /** |
| * Returns a non-ignored task in the {@param tasks} list that can be used as an achor when |
| * calculating the scroll position before and after a layout change. |
| */ |
| public Task findAnchorTask(List<Task> tasks, MutableBoolean isFrontMostTask) { |
| for (int i = tasks.size() - 1; i >= 0; i--) { |
| Task task = tasks.get(i); |
| |
| // Ignore deleting tasks |
| if (isIgnoredTask(task)) { |
| if (i == tasks.size() - 1) { |
| isFrontMostTask.value = true; |
| } |
| continue; |
| } |
| return task; |
| } |
| return null; |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| |
| // Draw the freeform workspace background |
| SystemServicesProxy ssp = Recents.getSystemServices(); |
| if (ssp.hasFreeformWorkspaceSupport()) { |
| if (mFreeformWorkspaceBackground.getAlpha() > 0) { |
| mFreeformWorkspaceBackground.draw(canvas); |
| } |
| } |
| } |
| |
| @Override |
| protected boolean verifyDrawable(Drawable who) { |
| if (who == mFreeformWorkspaceBackground) { |
| return true; |
| } |
| return super.verifyDrawable(who); |
| } |
| |
| /** |
| * Launches the freeform tasks. |
| */ |
| public boolean launchFreeformTasks() { |
| ArrayList<Task> tasks = mStack.getFreeformTasks(); |
| if (!tasks.isEmpty()) { |
| Task frontTask = tasks.get(tasks.size() - 1); |
| if (frontTask != null && frontTask.isFreeformTask()) { |
| EventBus.getDefault().send(new LaunchTaskEvent(getChildViewForTask(frontTask), |
| frontTask, null, INVALID_STACK_ID, false)); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /**** TaskStackCallbacks Implementation ****/ |
| |
| @Override |
| public void onStackTaskAdded(TaskStack stack, Task newTask) { |
| // Update the min/max scroll and animate other task views into their new positions |
| updateLayoutAlgorithm(true /* boundScroll */); |
| |
| // Animate all the tasks into place |
| relayoutTaskViews(new AnimationProps(DEFAULT_SYNC_STACK_DURATION, |
| Interpolators.FAST_OUT_SLOW_IN)); |
| } |
| |
| /** |
| * We expect that the {@link TaskView} associated with the removed task is already hidden. |
| */ |
| @Override |
| public void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask, |
| Task newFrontMostTask, AnimationProps animation) { |
| if (mFocusedTask == removedTask) { |
| resetFocusedTask(removedTask); |
| } |
| |
| // Remove the view associated with this task, we can't rely on updateTransforms |
| // to work here because the task is no longer in the list |
| TaskView tv = getChildViewForTask(removedTask); |
| if (tv != null) { |
| mViewPool.returnViewToPool(tv); |
| } |
| |
| // Remove the task from the ignored set |
| removeIgnoreTask(removedTask); |
| |
| // If requested, relayout with the given animation |
| if (animation != null) { |
| updateLayoutAlgorithm(true /* boundScroll */); |
| relayoutTaskViews(animation); |
| } |
| |
| // Update the new front most task's action button |
| if (mScreenPinningEnabled && newFrontMostTask != null) { |
| TaskView frontTv = getChildViewForTask(newFrontMostTask); |
| if (frontTv != null) { |
| frontTv.showActionButton(true /* fadeIn */, DEFAULT_SYNC_STACK_DURATION); |
| } |
| } |
| |
| // If there are no remaining tasks, then just close recents |
| if (mStack.getTaskCount() == 0) { |
| EventBus.getDefault().send(new AllTaskViewsDismissedEvent()); |
| } |
| } |
| |
| @Override |
| public void onHistoryTaskRemoved(TaskStack stack, Task removedTask, |
| AnimationProps animation) { |
| // To be implemented |
| } |
| |
| /**** ViewPoolConsumer Implementation ****/ |
| |
| @Override |
| public TaskView createView(Context context) { |
| return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false); |
| } |
| |
| @Override |
| public void onReturnViewToPool(TaskView tv) { |
| final Task task = tv.getTask(); |
| |
| // Unbind the task from the task view |
| unbindTaskView(tv, task); |
| |
| // Reset the view properties and view state |
| tv.resetViewProperties(); |
| tv.setFocusedState(false, false /* requestViewFocus */); |
| tv.setClipViewInStack(false); |
| if (mScreenPinningEnabled) { |
| tv.hideActionButton(false /* fadeOut */, 0 /* duration */, false /* scaleDown */, null); |
| } |
| |
| // Detach the view from the hierarchy |
| detachViewFromParent(tv); |
| // Update the task views list after removing the task view |
| updateTaskViewsList(); |
| } |
| |
| @Override |
| public void onPickUpViewFromPool(TaskView tv, Task task, boolean isNewView) { |
| // Find the index where this task should be placed in the stack |
| int taskIndex = mStack.indexOfStackTask(task); |
| int insertIndex = findTaskViewInsertIndex(task, taskIndex); |
| |
| // Add/attach the view to the hierarchy |
| if (isNewView) { |
| if (mInMeasureLayout) { |
| // If we are measuring the layout, then just add the view normally as it will be |
| // laid out during the layout pass |
| addView(tv, insertIndex); |
| } else { |
| // Otherwise, this is from a bindVisibleTaskViews() call outside the measure/layout |
| // pass, and we should layout the new child ourselves |
| ViewGroup.LayoutParams params = tv.getLayoutParams(); |
| if (params == null) { |
| params = generateDefaultLayoutParams(); |
| } |
| addViewInLayout(tv, insertIndex, params, true /* preventRequestLayout */); |
| measureTaskView(tv); |
| layoutTaskView(tv); |
| } |
| } else { |
| attachViewToParent(tv, insertIndex, tv.getLayoutParams()); |
| } |
| // Update the task views list after adding the new task view |
| updateTaskViewsList(); |
| |
| // Bind the task view to the new task |
| bindTaskView(tv, task); |
| |
| // If the doze trigger has already fired, then update the state for this task view |
| if (mUIDozeTrigger.isAsleep()) { |
| tv.setNoUserInteractionState(); |
| } |
| |
| // Set the new state for this view, including the callbacks and view clipping |
| tv.setCallbacks(this); |
| tv.setTouchEnabled(true); |
| tv.setClipViewInStack(true); |
| if (mFocusedTask == task) { |
| tv.setFocusedState(true, false /* requestViewFocus */); |
| if (mStartTimerIndicatorDuration > 0) { |
| // The timer indicator couldn't be started before, so start it now |
| tv.getHeaderView().startFocusTimerIndicator(mStartTimerIndicatorDuration); |
| mStartTimerIndicatorDuration = 0; |
| } |
| } |
| |
| // Restore the action button visibility if it is the front most task view |
| if (mScreenPinningEnabled && tv.getTask() == |
| mStack.getStackFrontMostTask(false /* includeFreeform */)) { |
| tv.showActionButton(false /* fadeIn */, 0 /* fadeInDuration */); |
| } |
| } |
| |
| @Override |
| public boolean hasPreferredData(TaskView tv, Task preferredData) { |
| return (tv.getTask() == preferredData); |
| } |
| |
| private void bindTaskView(TaskView tv, Task task) { |
| // Rebind the task and request that this task's data be filled into the TaskView |
| tv.onTaskBound(task); |
| |
| // Load the task data |
| Recents.getTaskLoader().loadTaskData(task, true /* fetchAndInvalidateThumbnails */); |
| } |
| |
| private void unbindTaskView(TaskView tv, Task task) { |
| // Report that this task's data is no longer being used |
| Recents.getTaskLoader().unloadTaskData(task); |
| } |
| |
| /**** TaskViewCallbacks Implementation ****/ |
| |
| @Override |
| public void onTaskViewClipStateChanged(TaskView tv) { |
| if (!mTaskViewsClipDirty) { |
| mTaskViewsClipDirty = true; |
| invalidate(); |
| } |
| } |
| |
| /**** TaskStackLayoutAlgorithm.TaskStackLayoutAlgorithmCallbacks ****/ |
| |
| @Override |
| public void onFocusStateChanged(int prevFocusState, int curFocusState) { |
| if (mDeferredTaskViewLayoutAnimation == null) { |
| mUIDozeTrigger.poke(); |
| relayoutTaskViewsOnNextFrame(AnimationProps.IMMEDIATE); |
| } |
| } |
| |
| /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/ |
| |
| @Override |
| public void onStackScrollChanged(float prevScroll, float curScroll, AnimationProps animation) { |
| mUIDozeTrigger.poke(); |
| if (animation != null) { |
| relayoutTaskViewsOnNextFrame(animation); |
| } |
| mLayoutAlgorithm.updateFocusStateOnScroll(curScroll, curScroll - prevScroll); |
| |
| if (mEnterAnimationComplete) { |
| if (shouldShowHistoryButton() && |
| prevScroll > SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD && |
| curScroll <= SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD) { |
| EventBus.getDefault().send(new ShowHistoryButtonEvent(true /* translate */)); |
| } else if (prevScroll < HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD && |
| curScroll >= HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD) { |
| EventBus.getDefault().send(new HideHistoryButtonEvent()); |
| } |
| } |
| } |
| |
| /**** EventBus Events ****/ |
| |
| public final void onBusEvent(PackagesChangedEvent event) { |
| // Compute which components need to be removed |
| ArraySet<ComponentName> removedComponents = mStack.computeComponentsRemoved( |
| event.packageName, event.userId); |
| |
| // For other tasks, just remove them directly if they no longer exist |
| ArrayList<Task> tasks = mStack.getStackTasks(); |
| for (int i = tasks.size() - 1; i >= 0; i--) { |
| final Task t = tasks.get(i); |
| if (removedComponents.contains(t.key.getComponent())) { |
| final TaskView tv = getChildViewForTask(t); |
| if (tv != null) { |
| // For visible children, defer removing the task until after the animation |
| tv.dismissTask(); |
| } else { |
| // Otherwise, remove the task from the stack immediately |
| mStack.removeTask(t, AnimationProps.IMMEDIATE); |
| } |
| } |
| } |
| } |
| |
| public final void onBusEvent(LaunchTaskEvent event) { |
| // Cancel any doze triggers once a task is launched |
| mUIDozeTrigger.stopDozing(); |
| } |
| |
| public final void onBusEvent(LaunchNextTaskRequestEvent event) { |
| int launchTaskIndex = mStack.indexOfStackTask(mStack.getLaunchTarget()); |
| if (launchTaskIndex != -1) { |
| launchTaskIndex = Math.max(0, launchTaskIndex - 1); |
| } else { |
| launchTaskIndex = mStack.getTaskCount() - 1; |
| } |
| if (launchTaskIndex != -1) { |
| // Stop all animations |
| mUIDozeTrigger.stopDozing(); |
| cancelAllTaskViewAnimations(); |
| |
| final Task launchTask = mStack.getStackTasks().get(launchTaskIndex); |
| if (getChildViewForTask(launchTask) == null) { |
| List<TaskView> taskViews = getTaskViews(); |
| int lastTaskIndex = !taskViews.isEmpty() |
| ? mStack.indexOfStackTask(taskViews.get(taskViews.size() - 1).getTask()) |
| : mStack.getTaskCount() - 1; |
| int duration = LAUNCH_NEXT_SCROLL_BASE_DURATION + |
| Math.abs(mStack.indexOfStackTask(launchTask) - lastTaskIndex) |
| * LAUNCH_NEXT_SCROLL_INCR_DURATION; |
| mStackScroller.animateScroll(mLayoutAlgorithm.getStackScrollForTask(launchTask), |
| duration, new Runnable() { |
| @Override |
| public void run() { |
| EventBus.getDefault().send(new LaunchTaskEvent( |
| getChildViewForTask(launchTask), launchTask, null, |
| INVALID_STACK_ID, false /* screenPinningRequested */)); |
| } |
| }); |
| } else { |
| EventBus.getDefault().send(new LaunchTaskEvent(getChildViewForTask(launchTask), |
| launchTask, null, INVALID_STACK_ID, false /* screenPinningRequested */)); |
| } |
| |
| MetricsLogger.action(getContext(), MetricsEvent.OVERVIEW_LAUNCH_PREVIOUS_TASK, |
| launchTask.key.getComponent().toString()); |
| } else if (mStack.getTaskCount() == 0) { |
| // If there are no tasks, then just hide recents back to home. |
| EventBus.getDefault().send(new HideRecentsEvent(false, true)); |
| } |
| } |
| |
| public final void onBusEvent(LaunchTaskStartedEvent event) { |
| mAnimationHelper.startLaunchTaskAnimation(event.taskView, event.screenPinningRequested, |
| event.getAnimationTrigger()); |
| } |
| |
| public final void onBusEvent(DismissRecentsToHomeAnimationStarted event) { |
| // Stop any scrolling |
| mStackScroller.stopScroller(); |
| mStackScroller.stopBoundScrollAnimation(); |
| |
| // Start the task animations |
| mAnimationHelper.startExitToHomeAnimation(event.animated, event.getAnimationTrigger()); |
| |
| // Dismiss the freeform workspace background |
| int taskViewExitToHomeDuration = TaskStackAnimationHelper.EXIT_TO_HOME_TRANSLATION_DURATION; |
| animateFreeformWorkspaceBackgroundAlpha(0, new AnimationProps(taskViewExitToHomeDuration, |
| Interpolators.FAST_OUT_SLOW_IN)); |
| } |
| |
| public final void onBusEvent(DismissFocusedTaskViewEvent event) { |
| if (mFocusedTask != null) { |
| TaskView tv = getChildViewForTask(mFocusedTask); |
| if (tv != null) { |
| tv.dismissTask(); |
| } |
| resetFocusedTask(mFocusedTask); |
| } |
| } |
| |
| public final void onBusEvent(final DismissTaskViewEvent event) { |
| // For visible children, defer removing the task until after the animation |
| mAnimationHelper.startDeleteTaskAnimation(event.task, event.taskView, |
| event.getAnimationTrigger()); |
| } |
| |
| public final void onBusEvent(TaskViewDismissedEvent event) { |
| removeTaskViewFromStack(event.taskView, event.task); |
| EventBus.getDefault().send(new DeleteTaskDataEvent(event.task)); |
| |
| MetricsLogger.action(getContext(), MetricsEvent.OVERVIEW_DISMISS, |
| event.task.key.getComponent().toString()); |
| } |
| |
| public final void onBusEvent(FocusNextTaskViewEvent event) { |
| // Stop any scrolling |
| mStackScroller.stopScroller(); |
| mStackScroller.stopBoundScrollAnimation(); |
| |
| setRelativeFocusedTask(true, false /* stackTasksOnly */, true /* animated */, false, |
| event.timerIndicatorDuration); |
| } |
| |
| public final void onBusEvent(FocusPreviousTaskViewEvent event) { |
| // Stop any scrolling |
| mStackScroller.stopScroller(); |
| mStackScroller.stopBoundScrollAnimation(); |
| |
| setRelativeFocusedTask(false, false /* stackTasksOnly */, true /* animated */); |
| } |
| |
| public final void onBusEvent(UserInteractionEvent event) { |
| // Poke the doze trigger on user interaction |
| mUIDozeTrigger.poke(); |
| |
| RecentsDebugFlags debugFlags = Recents.getDebugFlags(); |
| if (debugFlags.isFastToggleRecentsEnabled() && mFocusedTask != null) { |
| TaskView tv = getChildViewForTask(mFocusedTask); |
| if (tv != null) { |
| tv.getHeaderView().cancelFocusTimerIndicator(); |
| } |
| } |
| } |
| |
| public final void onBusEvent(DragStartEvent event) { |
| // Ensure that the drag task is not animated |
| addIgnoreTask(event.task); |
| |
| if (event.task.isFreeformTask()) { |
| // Animate to the front of the stack |
| mStackScroller.animateScroll(mLayoutAlgorithm.mInitialScrollP, null); |
| } |
| |
| // Enlarge the dragged view slightly |
| float finalScale = event.taskView.getScaleX() * DRAG_SCALE_FACTOR; |
| mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(), |
| mTmpTransform, null); |
| mTmpTransform.scale = finalScale; |
| mTmpTransform.translationZ = mLayoutAlgorithm.mMaxTranslationZ + 1; |
| updateTaskViewToTransform(event.taskView, mTmpTransform, |
| new AnimationProps(DRAG_SCALE_DURATION, Interpolators.FAST_OUT_SLOW_IN)); |
| } |
| |
| public final void onBusEvent(DragStartInitializeDropTargetsEvent event) { |
| SystemServicesProxy ssp = Recents.getSystemServices(); |
| if (ssp.hasFreeformWorkspaceSupport()) { |
| event.handler.registerDropTargetForCurrentDrag(mStackDropTarget); |
| event.handler.registerDropTargetForCurrentDrag(mFreeformWorkspaceDropTarget); |
| } |
| } |
| |
| public final void onBusEvent(DragDropTargetChangedEvent event) { |
| AnimationProps animation = new AnimationProps(250, Interpolators.FAST_OUT_SLOW_IN); |
| if (event.dropTarget instanceof TaskStack.DockState) { |
| // Calculate the new task stack bounds that matches the window size that Recents will |
| // have after the drop |
| final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget; |
| mStackBounds.set(dockState.getDockedTaskStackBounds(getMeasuredWidth(), |
| getMeasuredHeight(), mDividerSize, mLayoutAlgorithm.mSystemInsets, |
| getResources())); |
| mLayoutAlgorithm.initialize(mStackBounds, |
| TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); |
| updateLayoutAlgorithm(true /* boundScroll */); |
| } else { |
| // Restore the pre-drag task stack bounds, but ensure that we don't layout the dragging |
| // task view, so add it back to the ignore set after updating the layout |
| mStackBounds.set(mStableStackBounds); |
| removeIgnoreTask(event.task); |
| mLayoutAlgorithm.initialize(mStackBounds, |
| TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); |
| updateLayoutAlgorithm(true /* boundScroll */); |
| addIgnoreTask(event.task); |
| } |
| relayoutTaskViews(animation); |
| } |
| |
| public final void onBusEvent(final DragEndEvent event) { |
| // We don't handle drops on the dock regions |
| if (event.dropTarget instanceof TaskStack.DockState) { |
| return; |
| } |
| |
| boolean isFreeformTask = event.task.isFreeformTask(); |
| boolean hasChangedStacks = |
| (!isFreeformTask && event.dropTarget == mFreeformWorkspaceDropTarget) || |
| (isFreeformTask && event.dropTarget == mStackDropTarget); |
| |
| if (hasChangedStacks) { |
| // Move the task to the right position in the stack (ie. the front of the stack if |
| // freeform or the front of the stack if fullscreen). Note, we MUST move the tasks |
| // before we update their stack ids, otherwise, the keys will have changed. |
| if (event.dropTarget == mFreeformWorkspaceDropTarget) { |
| mStack.moveTaskToStack(event.task, FREEFORM_WORKSPACE_STACK_ID); |
| } else if (event.dropTarget == mStackDropTarget) { |
| mStack.moveTaskToStack(event.task, FULLSCREEN_WORKSPACE_STACK_ID); |
| } |
| updateLayoutAlgorithm(true /* boundScroll */); |
| |
| // Move the task to the new stack in the system after the animation completes |
| event.addPostAnimationCallback(new Runnable() { |
| @Override |
| public void run() { |
| SystemServicesProxy ssp = Recents.getSystemServices(); |
| ssp.moveTaskToStack(event.task.key.id, event.task.key.stackId); |
| } |
| }); |
| } |
| |
| // We translated the view but we need to animate it back from the current layout-space rect |
| // to its final layout-space rect |
| int x = (int) event.taskView.getTranslationX(); |
| int y = (int) event.taskView.getTranslationY(); |
| Rect taskViewRect = new Rect(event.taskView.getLeft(), event.taskView.getTop(), |
| event.taskView.getRight(), event.taskView.getBottom()); |
| taskViewRect.offset(x, y); |
| event.taskView.setTranslationX(0); |
| event.taskView.setTranslationY(0); |
| event.taskView.setLeftTopRightBottom(taskViewRect.left, taskViewRect.top, |
| taskViewRect.right, taskViewRect.bottom); |
| |
| // Animate the non-drag TaskViews back into position |
| mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(), |
| mTmpTransform, null); |
| event.getAnimationTrigger().increment(); |
| relayoutTaskViews(new AnimationProps(DEFAULT_SYNC_STACK_DURATION, |
| Interpolators.FAST_OUT_SLOW_IN)); |
| |
| // Animate the drag TaskView back into position |
| updateTaskViewToTransform(event.taskView, mTmpTransform, |
| new AnimationProps(DEFAULT_SYNC_STACK_DURATION, Interpolators.FAST_OUT_SLOW_IN, |
| event.getAnimationTrigger().decrementOnAnimationEnd())); |
| removeIgnoreTask(event.task); |
| } |
| |
| public final void onBusEvent(IterateRecentsEvent event) { |
| if (!mEnterAnimationComplete) { |
| // Cancel the previous task's window transition before animating the focused state |
| EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(null)); |
| } |
| } |
| |
| public final void onBusEvent(EnterRecentsWindowAnimationCompletedEvent event) { |
| mEnterAnimationComplete = true; |
| |
| if (mStack.getTaskCount() > 0) { |
| // Start the task enter animations |
| mAnimationHelper.startEnterAnimation(event.getAnimationTrigger()); |
| |
| // Add a runnable to the post animation ref counter to clear all the views |
| event.addPostAnimationCallback(new Runnable() { |
| @Override |
| public void run() { |
| // Start the dozer to trigger to trigger any UI that shows after a timeout |
| mUIDozeTrigger.startDozing(); |
| |
| // Update the focused state here -- since we only set the focused task without |
| // requesting view focus in onFirstLayout(), actually request view focus and |
| // animate the focused state if we are alt-tabbing now, after the window enter |
| // animation is completed |
| if (mFocusedTask != null) { |
| RecentsConfiguration config = Recents.getConfiguration(); |
| RecentsActivityLaunchState launchState = config.getLaunchState(); |
| setFocusedTask(mStack.indexOfStackTask(mFocusedTask), |
| false /* scrollToTask */, launchState.launchedWithAltTab); |
| } |
| |
| EventBus.getDefault().send(new EnterRecentsTaskStackAnimationCompletedEvent()); |
| } |
| }); |
| } |
| } |
| |
| public final void onBusEvent(UpdateFreeformTaskViewVisibilityEvent event) { |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| Task task = tv.getTask(); |
| if (task.isFreeformTask()) { |
| tv.setVisibility(event.visible ? View.VISIBLE : View.INVISIBLE); |
| } |
| } |
| } |
| |
| public final void onBusEvent(ShowHistoryEvent event) { |
| ReferenceCountedTrigger postAnimTrigger = new ReferenceCountedTrigger(); |
| postAnimTrigger.addLastDecrementRunnable(new Runnable() { |
| @Override |
| public void run() { |
| setVisibility(View.INVISIBLE); |
| } |
| }); |
| mAnimationHelper.startShowHistoryAnimation(postAnimTrigger); |
| } |
| |
| public final void onBusEvent(HideHistoryEvent event) { |
| setVisibility(View.VISIBLE); |
| mAnimationHelper.startHideHistoryAnimation(); |
| } |
| |
| public final void onBusEvent(MultiWindowStateChangedEvent event) { |
| if (!event.inMultiWindow) { |
| // Scroll the stack to the front to see the undocked task |
| mStackScroller.animateScroll(mLayoutAlgorithm.mMaxScrollP, new Runnable() { |
| @Override |
| public void run() { |
| List<TaskView> taskViews = getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| tv.getHeaderView().rebindToTask(tv.getTask(), tv.mTouchExplorationEnabled, |
| tv.mIsDisabledInSafeMode); |
| } |
| } |
| }); |
| } |
| } |
| |
| public final void onBusEvent(ConfigurationChangedEvent event) { |
| mLayoutAlgorithm.reloadOnConfigurationChange(getContext()); |
| mLayoutAlgorithm.initialize(mStackBounds, |
| TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); |
| } |
| |
| /** |
| * Removes the task from the stack, and updates the focus to the next task in the stack if the |
| * removed TaskView was focused. |
| */ |
| private void removeTaskViewFromStack(TaskView tv, Task task) { |
| // Announce for accessibility |
| tv.announceForAccessibility(getContext().getString( |
| R.string.accessibility_recents_item_dismissed, task.title)); |
| |
| // Remove the task from the stack |
| mStack.removeTask(task, new AnimationProps(DEFAULT_SYNC_STACK_DURATION, |
| Interpolators.FAST_OUT_SLOW_IN)); |
| } |
| |
| /** |
| * Starts an alpha animation on the freeform workspace background. |
| */ |
| private void animateFreeformWorkspaceBackgroundAlpha(int targetAlpha, |
| AnimationProps animation) { |
| if (mFreeformWorkspaceBackground.getAlpha() == targetAlpha) { |
| return; |
| } |
| |
| Utilities.cancelAnimationWithoutCallbacks(mFreeformWorkspaceBackgroundAnimator); |
| mFreeformWorkspaceBackgroundAnimator = ObjectAnimator.ofInt(mFreeformWorkspaceBackground, |
| Utilities.DRAWABLE_ALPHA, mFreeformWorkspaceBackground.getAlpha(), targetAlpha); |
| mFreeformWorkspaceBackgroundAnimator.setStartDelay( |
| animation.getDuration(AnimationProps.ALPHA)); |
| mFreeformWorkspaceBackgroundAnimator.setDuration( |
| animation.getDuration(AnimationProps.ALPHA)); |
| mFreeformWorkspaceBackgroundAnimator.setInterpolator( |
| animation.getInterpolator(AnimationProps.ALPHA)); |
| mFreeformWorkspaceBackgroundAnimator.start(); |
| } |
| |
| /** |
| * Returns the insert index for the task in the current set of task views. If the given task |
| * is already in the task view list, then this method returns the insert index assuming it |
| * is first removed at the previous index. |
| * |
| * @param task the task we are finding the index for |
| * @param taskIndex the index of the task in the stack |
| */ |
| private int findTaskViewInsertIndex(Task task, int taskIndex) { |
| if (taskIndex != -1) { |
| List<TaskView> taskViews = getTaskViews(); |
| boolean foundTaskView = false; |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| Task tvTask = taskViews.get(i).getTask(); |
| if (tvTask == task) { |
| foundTaskView = true; |
| } else if (taskIndex < mStack.indexOfStackTask(tvTask)) { |
| if (foundTaskView) { |
| return i - 1; |
| } else { |
| return i; |
| } |
| } |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * @return whether the history button should be visible |
| */ |
| private boolean shouldShowHistoryButton() { |
| return !mStack.getHistoricalTasks().isEmpty(); |
| } |
| |
| /** |
| * Reads current system flags related to accessibility and screen pinning. |
| */ |
| private void readSystemFlags() { |
| SystemServicesProxy ssp = Recents.getSystemServices(); |
| mTouchExplorationEnabled = ssp.isTouchExplorationEnabled(); |
| mScreenPinningEnabled = ssp.getSystemSetting(getContext(), |
| Settings.System.LOCK_TO_APP_ENABLED) != 0; |
| } |
| } |