/* * Copyright (C) 2017 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.quickstep.views; import static android.view.Surface.ROTATION_0; import static android.view.View.MeasureSpec.EXACTLY; import static android.view.View.MeasureSpec.makeMeasureSpec; import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU; import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; import static com.android.launcher3.Utilities.mapToRange; import static com.android.launcher3.Utilities.squaredHypot; import static com.android.launcher3.Utilities.squaredTouchSlop; import static com.android.launcher3.anim.Interpolators.ACCEL; import static com.android.launcher3.anim.Interpolators.ACCEL_0_75; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.clampToProgress; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN; import static com.android.launcher3.statehandlers.DepthController.DEPTH; import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.LocusId; import android.content.res.Configuration; import android.graphics.BlendMode; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.UserHandle; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Log; import android.util.SparseBooleanArray; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ListView; import android.widget.OverScroller; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.graphics.ColorUtils; import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.PagedView; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.SpringProperty; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.cache.HandlerRunnable; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.touch.OverScroll; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.util.DynamicResource; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.ResourceBasedOverride.Overrides; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; import com.android.launcher3.util.Themes; import com.android.launcher3.util.TranslateEdgeEffect; import com.android.launcher3.util.ViewPool; import com.android.quickstep.AnimatedFloat; import com.android.quickstep.BaseActivityInterface; import com.android.quickstep.GestureState; import com.android.quickstep.RecentsAnimationController; import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RecentsModel; import com.android.quickstep.RecentsModel.TaskVisualsChangeListener; import com.android.quickstep.RemoteAnimationTargets; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskOverlayFactory; import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.TaskViewUtils; import com.android.quickstep.ViewUtils; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.util.SplitScreenBounds; import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TransformParams; import com.android.systemui.plugins.ResourceProvider; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.Task.TaskKey; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.wm.shell.pip.IPipAnimationListener; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; /** * A list of recent tasks. */ @TargetApi(Build.VERSION_CODES.R) public abstract class RecentsView, STATE_TYPE extends BaseState> extends PagedView implements Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback, TaskVisualsChangeListener, SplitScreenBounds.OnChangeListener { // TODO(b/184899234): We use this timeout to wait a fixed period after switching to the // screenshot when dismissing the current live task to ensure the app can try and get stopped. private static final int REMOVE_TASK_WAIT_FOR_APP_STOP_MS = 100; public static final FloatProperty CONTENT_ALPHA = new FloatProperty("contentAlpha") { @Override public void setValue(RecentsView view, float v) { view.setContentAlpha(v); } @Override public Float get(RecentsView view) { return view.getContentAlpha(); } }; public static final FloatProperty FULLSCREEN_PROGRESS = new FloatProperty("fullscreenProgress") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setFullscreenProgress(v); } @Override public Float get(RecentsView recentsView) { return recentsView.mFullscreenProgress; } }; public static final FloatProperty TASK_MODALNESS = new FloatProperty("taskModalness") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskModalness(v); } @Override public Float get(RecentsView recentsView) { return recentsView.mTaskModalness; } }; public static final FloatProperty ADJACENT_PAGE_HORIZONTAL_OFFSET = new FloatProperty("adjacentPageHorizontalOffset") { @Override public void setValue(RecentsView recentsView, float v) { if (recentsView.mAdjacentPageHorizontalOffset != v) { recentsView.mAdjacentPageHorizontalOffset = v; recentsView.updatePageOffsets(); } } @Override public Float get(RecentsView recentsView) { return recentsView.mAdjacentPageHorizontalOffset; } }; /** * Can be used to tint the color of the RecentsView to simulate a scrim that can views * excluded from. Really should be a proper scrim. * TODO(b/187528071): Remove this and replace with a real scrim. */ private static final FloatProperty COLOR_TINT = new FloatProperty("colorTint") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setColorTint(v); } @Override public Float get(RecentsView recentsView) { return recentsView.getColorTint(); } }; /** * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they * are currently both used to apply secondary translation. Should their use cases change to be * more specific, we'd want to create a similar FloatProperty just for a TaskView's * offsetX/Y property */ public static final FloatProperty TASK_SECONDARY_TRANSLATION = new FloatProperty("taskSecondaryTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsResistanceTranslation(v); } @Override public Float get(RecentsView recentsView) { return recentsView.mTaskViewsSecondaryTranslation; } }; /** * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they * are currently both used to apply secondary translation. Should their use cases change to be * more specific, we'd want to create a similar FloatProperty just for a TaskView's * offsetX/Y property */ public static final FloatProperty TASK_PRIMARY_SPLIT_TRANSLATION = new FloatProperty("taskPrimarySplitTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsPrimarySplitTranslation(v); } @Override public Float get(RecentsView recentsView) { return recentsView.mTaskViewsPrimarySplitTranslation; } }; public static final FloatProperty TASK_SECONDARY_SPLIT_TRANSLATION = new FloatProperty("taskSecondarySplitTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsSecondarySplitTranslation(v); } @Override public Float get(RecentsView recentsView) { return recentsView.mTaskViewsSecondarySplitTranslation; } }; /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */ public static final FloatProperty RECENTS_SCALE_PROPERTY = new FloatProperty("recentsScale") { @Override public void setValue(RecentsView view, float scale) { view.setScaleX(scale); view.setScaleY(scale); view.mLastComputedTaskStartPushOutDistance = null; view.mLastComputedTaskEndPushOutDistance = null; view.mLiveTileTaskViewSimulator.recentsViewScale.value = scale; view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation); view.updatePageOffsets(); } @Override public Float get(RecentsView view) { return view.getScaleX(); } }; public static final FloatProperty RECENTS_GRID_PROGRESS = new FloatProperty("recentsGrid") { @Override public void setValue(RecentsView view, float gridProgress) { view.setGridProgress(gridProgress); } @Override public Float get(RecentsView view) { return view.mGridProgress; } }; // OverScroll constants private static final int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270; private static final int DISMISS_TASK_DURATION = 300; private static final int ADDITION_TASK_DURATION = 200; private static final float INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.55f; private static final float ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.05f; private static final float ANIMATION_DISMISS_PROGRESS_MIDPOINT = 0.5f; private static final float SIGNIFICANT_MOVE_THRESHOLD_TABLET = 0.15f; protected final RecentsOrientedState mOrientationState; protected final BaseActivityInterface mSizeStrategy; protected RecentsAnimationController mRecentsAnimationController; protected SurfaceTransactionApplier mSyncTransactionApplier; protected int mTaskWidth; protected int mTaskHeight; // Used to position the top of a task in the top row of the grid private float mTaskGridVerticalDiff; // The vertical space one grid task takes + space between top and bottom row. private float mTopBottomRowHeightDiff; // mTaskGridVerticalDiff and mTopBottomRowHeightDiff summed together provides the top // position for bottom row of grid tasks. protected final TransformParams mLiveTileParams = new TransformParams(); protected final TaskViewSimulator mLiveTileTaskViewSimulator; protected final Rect mLastComputedTaskSize = new Rect(); protected final Rect mLastComputedGridSize = new Rect(); protected final Rect mLastComputedGridTaskSize = new Rect(); // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot. protected Float mLastComputedTaskStartPushOutDistance = null; protected Float mLastComputedTaskEndPushOutDistance = null; protected boolean mEnableDrawingLiveTile = false; protected final Rect mTempRect = new Rect(); protected final RectF mTempRectF = new RectF(); private final PointF mTempPointF = new PointF(); private final Matrix mTempMatrix = new Matrix(); private final float[] mTempFloat = new float[1]; private final List mScrollListeners = new ArrayList<>(); // The threshold at which we update the SystemUI flags when animating from the task into the app public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f; protected final ACTIVITY_TYPE mActivity; private final float mFastFlingVelocity; private final RecentsModel mModel; private final int mRowSpacing; private final int mGridSideMargin; private final ClearAllButton mClearAllButton; private final Rect mClearAllButtonDeadZoneRect = new Rect(); private final Rect mTaskViewDeadZoneRect = new Rect(); /** * Reflects if Recents is currently in the middle of a gesture */ private boolean mGestureActive; // Keeps track of the previously known visible tasks for purposes of loading/unloading task data private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); private final InvariantDeviceProfile mIdp; private final ViewPool mTaskViewPool; private final TaskOverlayFactory mTaskOverlayFactory; protected boolean mDisallowScrollToClearAll; private boolean mOverlayEnabled; protected boolean mFreezeViewVisibility; private boolean mOverviewGridEnabled; private boolean mOverviewFullscreenEnabled; private float mAdjacentPageHorizontalOffset = 0; protected float mTaskViewsSecondaryTranslation = 0; protected float mTaskViewsPrimarySplitTranslation = 0; protected float mTaskViewsSecondarySplitTranslation = 0; // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid. private float mGridProgress = 0; private final IntSet mTopRowIdSet = new IntSet(); // The GestureEndTarget that is still in progress. protected GestureState.GestureEndTarget mCurrentGestureEndTarget; // TODO(b/187528071): Remove these and replace with a real scrim. private float mColorTint; private final int mTintingColor; private ObjectAnimator mTintingAnimator; private int mOverScrollShift = 0; /** * TODO: Call reloadIdNeeded in onTaskStackChanged. */ private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { if (!mHandleTaskStackChanges) { return; } // Check this is for the right user if (!checkCurrentOrManagedUserId(userId, getContext())) { return; } // Remove the task immediately from the task list TaskView taskView = getTaskView(taskId); if (taskView != null) { removeView(taskView); } } @Override public void onActivityUnpinned() { if (!mHandleTaskStackChanges) { return; } reloadIfNeeded(); enableLayoutTransitions(); } @Override public void onTaskRemoved(int taskId) { if (!mHandleTaskStackChanges) { return; } TaskView taskView = getTaskView(taskId); if (taskView == null) { return; } Task.TaskKey taskKey = taskView.getTask().key; UI_HELPER_EXECUTOR.execute(new HandlerRunnable<>( UI_HELPER_EXECUTOR.getHandler(), () -> PackageManagerWrapper.getInstance() .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null, MAIN_EXECUTOR, apkRemoved -> { if (apkRemoved) { dismissTask(taskId); } else { mModel.isTaskRemoved(taskKey.id, taskRemoved -> { if (taskRemoved) { dismissTask(taskId); } }); } })); } }; private final PinnedStackAnimationListener mIPipAnimationListener = new PinnedStackAnimationListener(); private int mPipCornerRadius; // Used to keep track of the last requested task list id, so that we do not request to load the // tasks again if we have already requested it and the task list has not changed private int mTaskListChangeId = -1; // Only valid until the launcher state changes to NORMAL protected int mRunningTaskId = -1; protected boolean mRunningTaskTileHidden; private Task mTmpRunningTask; protected int mFocusedTaskId = -1; private boolean mTaskIconScaledDown = false; private boolean mOverviewStateEnabled; private boolean mHandleTaskStackChanges; private boolean mSwipeDownShouldLaunchApp; private boolean mTouchDownToStartHome; private final float mSquaredTouchSlop; private int mDownX; private int mDownY; private PendingAnimation mPendingAnimation; private LayoutTransition mLayoutTransition; @ViewDebug.ExportedProperty(category = "launcher") protected float mContentAlpha = 1; @ViewDebug.ExportedProperty(category = "launcher") protected float mFullscreenProgress = 0; /** * How modal is the current task to be displayed, 1 means the task is fully modal and no other * tasks are show. 0 means the task is displays in context in the list with other tasks. */ @ViewDebug.ExportedProperty(category = "launcher") protected float mTaskModalness = 0; // Keeps track of task id whose visual state should not be reset private int mIgnoreResetTaskId = -1; // Variables for empty state private final Drawable mEmptyIcon; private final CharSequence mEmptyMessage; private final TextPaint mEmptyMessagePaint; private final Point mLastMeasureSize = new Point(); private final int mEmptyMessagePadding; private boolean mShowEmptyMessage; private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener; private Layout mEmptyTextLayout; /** * Placeholder view indicating where the first split screen selected app will be placed */ private SplitPlaceholderView mSplitPlaceholderView; /** * The first task that split screen selection was initiated with. When split select state is * initialized, we create a * {@link #createTaskDismissAnimation(TaskView, boolean, boolean, long)} for this TaskView but * don't actually remove the task since the user might back out. As such, we also ensure this * View doesn't go back into the {@link #mTaskViewPool}, see {@link #onViewRemoved(View)} */ private TaskView mSplitHiddenTaskView; /** * Keeps track of the index of the TaskView that split screen was initialized with so we know * where to insert it back into list of taskViews in case user backs out of entering split * screen. * NOTE: This index is the index while {@link #mSplitHiddenTaskView} was a child of recentsView, * this doesn't get adjusted to reflect the new child count after the taskView is dismissed/ * removed from recentsView */ private int mSplitHiddenTaskViewIndex; // Keeps track of the index where the first TaskView should be private int mTaskViewStartIndex = 0; private OverviewActionsView mActionsView; private MultiWindowModeChangedListener mMultiWindowModeChangedListener = new MultiWindowModeChangedListener() { @Override public void onMultiWindowModeChanged(boolean inMultiWindowMode) { mOrientationState.setMultiWindowMode(inMultiWindowMode); setLayoutRotation(mOrientationState.getTouchRotation(), mOrientationState.getDisplayRotation()); updateChildTaskOrientations(); if (!inMultiWindowMode && mOverviewStateEnabled) { // TODO: Re-enable layout transitions for addition of the unpinned task reloadIfNeeded(); } } }; public RecentsView(Context context, AttributeSet attrs, int defStyleAttr, BaseActivityInterface sizeStrategy) { super(context, attrs, defStyleAttr); setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); setEnableFreeScroll(true); mSizeStrategy = sizeStrategy; mActivity = BaseActivity.fromContext(context); mOrientationState = new RecentsOrientedState( context, mSizeStrategy, this::animateRecentsRotationInPlace); final int rotation = mActivity.getDisplay().getRotation(); mOrientationState.setRecentsRotation(rotation); mFastFlingVelocity = getResources() .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); mModel = RecentsModel.INSTANCE.get(context); mIdp = InvariantDeviceProfile.INSTANCE.get(context); mClearAllButton = (ClearAllButton) LayoutInflater.from(context) .inflate(R.layout.overview_clear_all_button, this, false); mClearAllButton.setOnClickListener(this::dismissAllTasks); mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */, 10 /* initial size */); mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources()); setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); mRowSpacing = getResources().getDimensionPixelSize(R.dimen.overview_grid_row_spacing); mGridSideMargin = getResources().getDimensionPixelSize(R.dimen.overview_grid_side_margin); mSquaredTouchSlop = squaredTouchSlop(context); mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); mEmptyIcon.setCallback(this); mEmptyMessage = context.getText(R.string.recents_empty_message); mEmptyMessagePaint = new TextPaint(); mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); mEmptyMessagePaint.setTextSize(getResources() .getDimension(R.dimen.recents_empty_message_text_size)); mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context), Typeface.NORMAL)); mEmptyMessagePaint.setAntiAlias(true); mEmptyMessagePadding = getResources() .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); setWillNotDraw(false); updateEmptyMessage(); mOrientationHandler = mOrientationState.getOrientationHandler(); mTaskOverlayFactory = Overrides.getObject( TaskOverlayFactory.class, context.getApplicationContext(), R.string.task_overlay_factory_class); // Initialize quickstep specific cache params here, as this is constructed only once mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5); mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy()); mLiveTileTaskViewSimulator.recentsViewScale.value = 1; mLiveTileTaskViewSimulator.setOrientationState(mOrientationState); mLiveTileTaskViewSimulator.setDrawsBelowRecents(true); mTintingColor = getForegroundScrimDimColor(context); } public OverScroller getScroller() { return mScroller; } public boolean isRtl() { return mIsRtl; } @Override protected void initEdgeEffect() { mEdgeGlowLeft = new TranslateEdgeEffect(getContext()); mEdgeGlowRight = new TranslateEdgeEffect(getContext()); } @Override protected void drawEdgeEffect(Canvas canvas) { // Do not draw edge effect } @Override protected void dispatchDraw(Canvas canvas) { // Draw overscroll if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) { final int restoreCount = canvas.save(); int primarySize = mOrientationHandler.getPrimaryValue(getWidth(), getHeight()); int scroll = OverScroll.dampedScroll(getUndampedOverScrollShift(), primarySize); mOrientationHandler.set(canvas, CANVAS_TRANSLATE, scroll); if (mOverScrollShift != scroll) { mOverScrollShift = scroll; dispatchScrollChanged(); } super.dispatchDraw(canvas); canvas.restoreToCount(restoreCount); } else { if (mOverScrollShift != 0) { mOverScrollShift = 0; dispatchScrollChanged(); } super.dispatchDraw(canvas); } if (LIVE_TILE.get() && mEnableDrawingLiveTile && mLiveTileParams.getTargetSet() != null) { redrawLiveTile(); } } private float getUndampedOverScrollShift() { final int width = getWidth(); final int height = getHeight(); int primarySize = mOrientationHandler.getPrimaryValue(width, height); int secondarySize = mOrientationHandler.getSecondaryValue(width, height); float effectiveShift = 0; if (!mEdgeGlowLeft.isFinished()) { mEdgeGlowLeft.setSize(secondarySize, primarySize); if (((TranslateEdgeEffect) mEdgeGlowLeft).getTranslationShift(mTempFloat)) { effectiveShift = mTempFloat[0]; postInvalidateOnAnimation(); } } if (!mEdgeGlowRight.isFinished()) { mEdgeGlowRight.setSize(secondarySize, primarySize); if (((TranslateEdgeEffect) mEdgeGlowRight).getTranslationShift(mTempFloat)) { effectiveShift -= mTempFloat[0]; postInvalidateOnAnimation(); } } return effectiveShift * primarySize; } /** * Returns the view shift due to overscroll */ public int getOverScrollShift() { return mOverScrollShift; } @Override public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) { if (mHandleTaskStackChanges) { TaskView taskView = getTaskView(taskId); if (taskView != null) { Task task = taskView.getTask(); taskView.getThumbnail().setThumbnail(task, thumbnailData); return task; } } return null; } @Override public void onTaskIconChanged(String pkg, UserHandle user) { for (int i = 0; i < getTaskViewCount(); i++) { TaskView tv = getTaskViewAt(i); Task task = tv.getTask(); if (task != null && task.key != null && pkg.equals(task.key.getPackageName()) && task.key.userId == user.getIdentifier()) { task.icon = null; if (tv.getIconView().getDrawable() != null) { tv.onTaskListVisibilityChanged(true /* visible */); } } } } /** * Update the thumbnail of the task. * @param refreshNow Refresh immediately if it's true. */ public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) { TaskView taskView = getTaskView(taskId); if (taskView != null) { taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow); } return taskView; } @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); updateTaskStackListenerState(); } public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) { mActionsView = actionsView; mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0); mSplitPlaceholderView = splitPlaceholderView; } public SplitPlaceholderView getSplitPlaceholder() { return mSplitPlaceholderView; } public boolean isSplitSelectionActive() { return mSplitPlaceholderView.getSplitController().isSplitSelectActive(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); updateTaskStackListenerState(); mModel.getThumbnailCache().getHighResLoadingState().addCallback(this); mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); mSyncTransactionApplier = new SurfaceTransactionApplier(this); mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier); RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this); mIPipAnimationListener.setActivityAndRecentsView(mActivity, this); SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener( mIPipAnimationListener); mOrientationState.initListeners(); SplitScreenBounds.INSTANCE.addOnChangeListener(this); mTaskOverlayFactory.initListeners(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); updateTaskStackListenerState(); mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this); mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); mSyncTransactionApplier = null; mLiveTileParams.setSyncTransactionApplier(null); RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this); SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null); SplitScreenBounds.INSTANCE.removeOnChangeListener(this); mIPipAnimationListener.setActivityAndRecentsView(null, null); mOrientationState.destroyListeners(); mTaskOverlayFactory.removeListeners(); } @Override public void onViewRemoved(View child) { super.onViewRemoved(child); // Clear the task data for the removed child if it was visible unless it's the initial // taskview for entering split screen, we only pretend to dismiss the task if (child instanceof TaskView && child != mSplitHiddenTaskView) { TaskView taskView = (TaskView) child; mHasVisibleTaskData.delete(taskView.getTaskId()); mTaskViewPool.recycle(taskView); mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0); } updateTaskStartIndex(child); } @Override public void onViewAdded(View child) { super.onViewAdded(child); child.setAlpha(mContentAlpha); // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the // child direction back to match system settings. child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL); updateTaskStartIndex(child); mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false); updateEmptyMessage(); } @Override public void draw(Canvas canvas) { maybeDrawEmptyMessage(canvas); super.draw(canvas); } public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) { if (mRunningTaskId != -1 && mRunningTaskId == taskId && getLiveTileParams().getTargetSet().findTask(taskId) != null) { RemoteAnimationTargets targets = getLiveTileParams().getTargetSet(); launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers, targets.nonApps); } } public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpaper, RemoteAnimationTargetCompat[] nonApps) { AnimatorSet anim = new AnimatorSet(); TaskView taskView = getTaskView(taskId); if (taskView == null || !isTaskViewVisible(taskView)) { // TODO: Refine this animation. SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mActivity.getDragLayer()); ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); appAnimator.setDuration(RECENTS_LAUNCH_DURATION); appAnimator.setInterpolator(ACCEL_DEACCEL); appAnimator.addUpdateListener(valueAnimator -> { float percent = valueAnimator.getAnimatedFraction(); SurfaceParams.Builder builder = new SurfaceParams.Builder( apps[apps.length - 1].leash); Matrix matrix = new Matrix(); matrix.postScale(percent, percent); matrix.postTranslate(mActivity.getDeviceProfile().widthPx * (1 - percent) / 2, mActivity.getDeviceProfile().heightPx * (1 - percent) / 2); builder.withAlpha(percent).withMatrix(matrix); surfaceApplier.scheduleApply(builder.build()); }); anim.play(appAnimator); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finishRecentsAnimation(false /* toRecents */, null); } }); } else { TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps, true /* launcherClosing */, mActivity.getStateManager(), this, getDepthController()); } anim.start(); } private void updateTaskStartIndex(View affectingView) { if (!(affectingView instanceof TaskView) && !(affectingView instanceof ClearAllButton)) { int childCount = getChildCount(); mTaskViewStartIndex = 0; while (mTaskViewStartIndex < childCount && !(getChildAt(mTaskViewStartIndex) instanceof TaskView)) { mTaskViewStartIndex++; } } } public boolean isTaskViewVisible(TaskView tv) { if (showAsGrid()) { int screenStart = mOrientationHandler.getPrimaryScroll(this); int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this); return isTaskViewWithinBounds(tv, screenStart, screenEnd); } else { // For now, just check if it's the active task or an adjacent task return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; } } private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) { int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment( showAsFullscreen(), showAsGrid()); int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment( showAsFullscreen())); int taskEnd = taskStart + taskSize; return (taskStart >= start && taskStart <= end) || (taskEnd >= start && taskEnd <= end); } public TaskView getTaskView(int taskId) { for (int i = 0; i < getTaskViewCount(); i++) { TaskView taskView = getTaskViewAt(i); if (taskView.getTaskId() == taskId) { return taskView; } } return null; } public void setOverviewStateEnabled(boolean enabled) { mOverviewStateEnabled = enabled; updateTaskStackListenerState(); mOrientationState.setRotationWatcherEnabled(enabled); if (!enabled) { // Reset the running task when leaving overview since it can still have a reference to // its thumbnail mTmpRunningTask = null; if (mSplitPlaceholderView.getSplitController().isSplitSelectActive()) { cancelSplitSelect(false); } } updateLocusId(); } /** * Whether the Clear All button is hidden or fully visible. Used to determine if center * displayed page is a task or the Clear All button. * * @return True = Clear All button not fully visible, center page is a task. False = Clear All * button fully visible, center page is Clear All button. */ public boolean isClearAllHidden() { return mClearAllButton.getAlpha() != 1f; } @Override protected void onPageBeginTransition() { super.onPageBeginTransition(); mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true); } @Override protected void onPageEndTransition() { super.onPageEndTransition(); if (isClearAllHidden()) { mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false); } if (getNextPage() > 0) { setSwipeDownShouldLaunchApp(true); } } @Override protected float getSignificantMoveThreshold() { return mActivity.getDeviceProfile().isTablet ? SIGNIFICANT_MOVE_THRESHOLD_TABLET : super.getSignificantMoveThreshold(); } @Override public boolean onTouchEvent(MotionEvent ev) { super.onTouchEvent(ev); if (showAsGrid()) { int taskCount = getTaskViewCount(); for (int i = 0; i < taskCount; i++) { TaskView taskView = getTaskViewAt(i); if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) { // Keep consuming events to pass to delegate return true; } } } else { TaskView taskView = getCurrentPageTaskView(); if (taskView != null && taskView.offerTouchToChildren(ev)) { // Keep consuming events to pass to delegate return true; } } final int x = (int) ev.getX(); final int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_UP: if (mTouchDownToStartHome) { startHome(); } mTouchDownToStartHome = false; break; case MotionEvent.ACTION_CANCEL: mTouchDownToStartHome = false; break; case MotionEvent.ACTION_MOVE: // Passing the touch slop will not allow dismiss to home if (mTouchDownToStartHome && (isHandlingTouch() || squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) { mTouchDownToStartHome = false; } break; case MotionEvent.ACTION_DOWN: // Touch down anywhere but the deadzone around the visible clear all button and // between the task views will start home on touch up if (!isHandlingTouch() && !isModal()) { if (mShowEmptyMessage) { mTouchDownToStartHome = true; } else { updateDeadZoneRects(); final boolean clearAllButtonDeadZoneConsumed = mClearAllButton.getAlpha() == 1 && mClearAllButtonDeadZoneRect.contains(x, y); final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { mTouchDownToStartHome = true; } } } mDownX = x; mDownY = y; break; } return isHandlingTouch(); } @Override protected void onNotSnappingToPageInFreeScroll() { int finalPos = mScroller.getFinalX(); if (!showAsGrid() && finalPos > mMinScroll && finalPos < mMaxScroll) { int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1); int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0); // If scrolling ends in the half of the added space that is closer to // the end, settle to the end. Otherwise snap to the nearest page. // If flinging past one of the ends, don't change the velocity as it // will get stopped at the end anyway. int pageSnapped = finalPos < (firstPageScroll + mMinScroll) / 2 ? mMinScroll : finalPos > (lastPageScroll + mMaxScroll) / 2 ? mMaxScroll : getScrollForPage(mNextPage); mScroller.setFinalX(pageSnapped); // Ensure the scroll/snap doesn't happen too fast; int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION - mScroller.getDuration(); if (extraScrollDuration > 0) { mScroller.extendDuration(extraScrollDuration); } } } @Override protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { // Enables swiping to the left or right only if the task overlay is not modal. if (!isModal()) { super.determineScrollingStart(ev, touchSlopScale); } } protected void applyLoadPlan(ArrayList tasks) { if (mPendingAnimation != null) { mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks)); return; } if (tasks == null || tasks.isEmpty()) { removeTasksViewsAndClearAllButton(); onTaskStackUpdated(); return; } // Unload existing visible task data unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); TaskView ignoreResetTaskView = mIgnoreResetTaskId == -1 ? null : getTaskView(mIgnoreResetTaskId); final int requiredTaskCount = tasks.size(); if (getTaskViewCount() != requiredTaskCount) { if (indexOfChild(mClearAllButton) != -1) { removeView(mClearAllButton); } for (int i = getTaskViewCount(); i < requiredTaskCount; i++) { addView(mTaskViewPool.getView()); } while (getTaskViewCount() > requiredTaskCount) { removeView(getChildAt(getChildCount() - 1)); } if (requiredTaskCount > 0) { addView(mClearAllButton); } } // Rebind and reset all task views for (int i = requiredTaskCount - 1; i >= 0; i--) { final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex; final Task task = tasks.get(i); final TaskView taskView = (TaskView) getChildAt(pageIndex); taskView.bind(task, mOrientationState); } if (mFocusedTaskId == -1 && getTaskViewCount() > 0) { mFocusedTaskId = getTaskViewAt(0).getTaskId(); } updateTaskSize(); if (mNextPage == INVALID_PAGE) { // Set the current page to the running task, but not if settling on new task. TaskView runningTaskView = getRunningTaskView(); if (runningTaskView != null) { setCurrentPage(indexOfChild(runningTaskView)); } else if (getTaskViewCount() > 0) { setCurrentPage(indexOfChild(getTaskViewAt(0))); } } if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreResetTaskView) { // If the taskView mapping is changing, do not preserve the visuals. Since we are // mostly preserving the first task, and new taskViews are added to the end, it should // generally map to the same task. mIgnoreResetTaskId = -1; } resetTaskVisuals(); onTaskStackUpdated(); updateEnabledOverlays(); } private boolean isModal() { return mTaskModalness > 0; } public boolean isLoadingTasks() { return mModel.isLoadingTasksInBackground(); } private void removeTasksViewsAndClearAllButton() { for (int i = getTaskViewCount() - 1; i >= 0; i--) { removeView(getTaskViewAt(i)); } if (indexOfChild(mClearAllButton) != -1) { removeView(mClearAllButton); } } public int getTaskViewCount() { int taskViewCount = getChildCount() - mTaskViewStartIndex; if (indexOfChild(mClearAllButton) != -1) { taskViewCount--; } return taskViewCount; } protected void onTaskStackUpdated() { // Lazily update the empty message only when the task stack is reapplied updateEmptyMessage(); } public void resetTaskVisuals() { for (int i = getTaskViewCount() - 1; i >= 0; i--) { TaskView taskView = getTaskViewAt(i); if (mIgnoreResetTaskId != taskView.getTaskId()) { taskView.resetViewTransforms(); taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); taskView.setStableAlpha(mContentAlpha); taskView.setFullscreenProgress(mFullscreenProgress); taskView.setModalness(mTaskModalness); } } if (LIVE_TILE.get()) { // Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need // to reset the params after it settles in Overview from swipe up so that we don't // render with obsolete param values. mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = 0; mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = 0; mLiveTileTaskViewSimulator.fullScreenProgress.value = 0; mLiveTileTaskViewSimulator.recentsViewScale.value = 1; } if (mRunningTaskTileHidden) { setRunningTaskHidden(mRunningTaskTileHidden); } updateCurveProperties(); // Update the set of visible task's data loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); setTaskModalness(0); setColorTint(0); } public void setFullscreenProgress(float fullscreenProgress) { mFullscreenProgress = fullscreenProgress; int taskCount = getTaskViewCount(); for (int i = 0; i < taskCount; i++) { getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress); } mClearAllButton.setFullscreenProgress(fullscreenProgress); // Fade out the actions view quickly (0.1 range) mActionsView.getFullscreenAlpha().setValue( mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR)); } private void updateTaskStackListenerState() { boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() && getWindowVisibility() == VISIBLE; if (handleTaskStackChanges != mHandleTaskStackChanges) { mHandleTaskStackChanges = handleTaskStackChanges; if (handleTaskStackChanges) { reloadIfNeeded(); } } } @Override public void setInsets(Rect insets) { mInsets.set(insets); // Update DeviceProfile dependant state. DeviceProfile dp = mActivity.getDeviceProfile(); setOverviewGridEnabled( mActivity.getStateManager().getState().displayOverviewTasksAsGrid(dp)); // Propagate DeviceProfile change event. mLiveTileTaskViewSimulator.setDp(dp); mActionsView.setDp(dp); mOrientationState.setDeviceProfile(dp); // Update RecentsView adn TaskView's DeviceProfile dependent layout. updateOrientationHandler(); } private void updateOrientationHandler() { // Handle orientation changes. mOrientationHandler = mOrientationState.getOrientationHandler(); mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources()); setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); mClearAllButton.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL); mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated()); mActivity.getDragLayer().recreateControllers(); boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0 || mOrientationState.getRecentsActivityRotation() != ROTATION_0; mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, !mOrientationState.canRecentsActivityRotate() && isInLandscape); // Update TaskView's DeviceProfile dependent layout. updateChildTaskOrientations(); // Recalculate DeviceProfile dependent layout. updateSizeAndPadding(); requestLayout(); // Reapply the current page to update page scrolls. setCurrentPage(mCurrentPage); } // Update task size and padding that are dependent on DeviceProfile and insets. private void updateSizeAndPadding() { DeviceProfile dp = mActivity.getDeviceProfile(); getTaskSize(mTempRect); mTaskWidth = mTempRect.width(); mTaskHeight = mTempRect.height(); mTempRect.top -= dp.overviewTaskThumbnailTopMarginPx; setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, dp.widthPx - mInsets.right - mTempRect.right, dp.heightPx - mInsets.bottom - mTempRect.bottom); mSizeStrategy.calculateGridSize(mActivity, mActivity.getDeviceProfile(), mLastComputedGridSize); mSizeStrategy.calculateGridTaskSize(mActivity, mActivity.getDeviceProfile(), mLastComputedGridTaskSize, mOrientationHandler); mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top; mTopBottomRowHeightDiff = mLastComputedGridTaskSize.height() + dp.overviewTaskThumbnailTopMarginPx + mRowSpacing; // Force TaskView to update size from thumbnail updateTaskSize(); // Update ActionsView position if (mActionsView != null) { FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mActionsView.getLayoutParams(); if (dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) { layoutParams.gravity = Gravity.BOTTOM; layoutParams.bottomMargin = dp.heightPx - mInsets.bottom - mLastComputedGridSize.bottom; layoutParams.leftMargin = mLastComputedTaskSize.left; layoutParams.rightMargin = dp.widthPx - mLastComputedTaskSize.right; // When in modal state, remove bottom margin to avoid covering content. mActionsView.setModalTransformY(layoutParams.bottomMargin); } else { layoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; layoutParams.bottomMargin = 0; layoutParams.leftMargin = 0; layoutParams.rightMargin = 0; mActionsView.setModalTransformY(0); } mActionsView.setLayoutParams(layoutParams); } } /** * Updates TaskView scaling and translation required to support variable width. */ private void updateTaskSize() { updateTaskSize(false); } /** * Updates TaskView scaling and translation required to support variable width. * * @param isTaskDismissal indicates if update was called due to task dismissal */ private void updateTaskSize(boolean isTaskDismissal) { final int taskCount = getTaskViewCount(); if (taskCount == 0) { return; } float accumulatedTranslationX = 0; for (int i = 0; i < taskCount; i++) { TaskView taskView = getTaskViewAt(i); taskView.updateTaskSize(); taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX); taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f); // Compensate space caused by TaskView scaling. float widthDiff = taskView.getLayoutParams().width * (1 - taskView.getNonGridScale()); accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff; } mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX); updateGridProperties(isTaskDismissal); } public void getTaskSize(Rect outRect) { mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect, mOrientationHandler); mLastComputedTaskSize.set(outRect); } /** * Returns the size of task selected to enter modal state. */ public Point getSelectedTaskSize() { mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), mTempRect, mOrientationHandler); return new Point(mTempRect.width(), mTempRect.height()); } /** Gets the last computed task size */ public Rect getLastComputedTaskSize() { return mLastComputedTaskSize; } public Rect getLastComputedGridTaskSize() { return mLastComputedGridTaskSize; } /** Gets the task size for modal state. */ public void getModalTaskSize(Rect outRect) { mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect); } @Override protected boolean computeScrollHelper() { boolean scrolling = super.computeScrollHelper(); boolean isFlingingFast = false; updateCurveProperties(); if (scrolling || isHandlingTouch()) { if (scrolling) { // Check if we are flinging quickly to disable high res thumbnail loading isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; } // After scrolling, update the visible task's data loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); // After scrolling, update ActionsView's visibility. updateActionsViewScrollAlpha(); } // Update the high res thumbnail loader state mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast); return scrolling; } private void updateActionsViewScrollAlpha() { float scrollAlpha = 1f; if (showAsGrid()) { TaskView focusedTaskView = getFocusedTaskView(); if (focusedTaskView != null) { float scrollDiff = Math.abs(getScrollForPage(indexOfChild(focusedTaskView)) - mOrientationHandler.getPrimaryScroll(this)); float delta = (mGridSideMargin - scrollDiff) / (float) mGridSideMargin; scrollAlpha = Utilities.boundToRange(delta, 0, 1); } } mActionsView.getScrollAlpha().setValue(scrollAlpha); } /** * Scales and adjusts translation of adjacent pages as if on a curved carousel. */ public void updateCurveProperties() { if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { return; } int scroll = mOrientationHandler.getPrimaryScroll(this); mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled); } @Override protected int getDestinationPage(int scaledScroll) { if (!(mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get())) { return super.getDestinationPage(scaledScroll); } final int childCount = getChildCount(); if (mPageScrolls == null || childCount != mPageScrolls.length) { return -1; } // When in tablet with variable task width, return the page which scroll is closest to // screenStart instead of page nearest to center of screen. int minDistanceFromScreenStart = Integer.MAX_VALUE; int minDistanceFromScreenStartIndex = -1; for (int i = 0; i < childCount; ++i) { int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll); if (distanceFromScreenStart < minDistanceFromScreenStart) { minDistanceFromScreenStart = distanceFromScreenStart; minDistanceFromScreenStartIndex = i; } } return minDistanceFromScreenStartIndex; } /** * Iterates through all the tasks, and loads the associated task data for newly visible tasks, * and unloads the associated task data for tasks that are no longer visible. */ public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) { if (!mOverviewStateEnabled || mTaskListChangeId == -1) { // Skip loading visible task data if we've already left the overview state, or if the // task list hasn't been loaded yet (the task views will not reflect the task list) return; } int lower = 0; int upper = 0; int visibleStart = 0; int visibleEnd = 0; if (showAsGrid()) { int screenStart = mOrientationHandler.getPrimaryScroll(this); int pageOrientedSize = mOrientationHandler.getMeasuredSize(this); int halfScreenSize = pageOrientedSize / 2; // Use +/- 50% screen width as visible area. visibleStart = screenStart - halfScreenSize; visibleEnd = screenStart + pageOrientedSize + halfScreenSize; } else { int centerPageIndex = getPageNearestToCenterOfScreen(); int numChildren = getChildCount(); lower = Math.max(0, centerPageIndex - 2); upper = Math.min(centerPageIndex + 2, numChildren - 1); } // Update the task data for the in/visible children for (int i = 0; i < getTaskViewCount(); i++) { TaskView taskView = getTaskViewAt(i); Task task = taskView.getTask(); int index = indexOfChild(taskView); boolean visible; if (showAsGrid()) { visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd); } else { visible = lower <= index && index <= upper; } if (visible) { if (task == mTmpRunningTask) { // Skip loading if this is the task that we are animating into continue; } if (!mHasVisibleTaskData.get(task.key.id)) { // Ignore thumbnail update if it's current running task during the gesture // We snapshot at end of gesture, it will update then int changes = dataChanges; if (taskView == getRunningTaskView() && mGestureActive) { changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL; } taskView.onTaskListVisibilityChanged(true /* visible */, changes); } mHasVisibleTaskData.put(task.key.id, visible); } else { if (mHasVisibleTaskData.get(task.key.id)) { taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges); } mHasVisibleTaskData.delete(task.key.id); } } } /** * Unloads any associated data from the currently visible tasks */ private void unloadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) { for (int i = 0; i < mHasVisibleTaskData.size(); i++) { if (mHasVisibleTaskData.valueAt(i)) { TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); if (taskView != null) { taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges); } } } mHasVisibleTaskData.clear(); } @Override public void onHighResLoadingStateChanged(boolean enabled) { // Whenever the high res loading state changes, poke each of the visible tasks to see if // they want to updated their thumbnail state for (int i = 0; i < mHasVisibleTaskData.size(); i++) { if (mHasVisibleTaskData.valueAt(i)) { TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); if (taskView != null) { // Poke the view again, which will trigger it to load high res if the state // is enabled taskView.onTaskListVisibilityChanged(true /* visible */); } } } } public abstract void startHome(); /** `true` if there is a +1 space available in overview. */ public boolean hasRecentsExtraCard() { return false; } public void reset() { setCurrentTask(-1); mIgnoreResetTaskId = -1; mTaskListChangeId = -1; mFocusedTaskId = getTaskViewCount() > 0 ? getTaskViewAt(0).getTaskId() : -1; if (mRecentsAnimationController != null) { if (LIVE_TILE.get() && mEnableDrawingLiveTile) { // We are still drawing the live tile, finish it now to clean up. finishRecentsAnimation(true /* toRecents */, null); } else { mRecentsAnimationController = null; } } setEnableDrawingLiveTile(false); mLiveTileParams.setTargetSet(null); mLiveTileTaskViewSimulator.setDrawsBelowRecents(true); // These are relatively expensive and don't need to be done this frame (RecentsView isn't // visible anyway), so defer by a frame to get off the critical path, e.g. app to home. post(() -> { unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); setCurrentPage(0); LayoutUtils.setViewEnabled(mActionsView, true); if (mOrientationState.setGestureActive(false)) { updateOrientationHandler(); } }); } public int getRunningTaskId() { return mRunningTaskId; } public @Nullable TaskView getRunningTaskView() { return getTaskView(mRunningTaskId); } public int getRunningTaskIndex() { return getTaskIndexForId(mRunningTaskId); } public @Nullable TaskView getFocusedTaskView() { return getTaskView(mFocusedTaskId); } protected @Nullable TaskView getHomeTaskView() { return null; } /** * Get the index of the task view whose id matches {@param taskId}. * @return -1 if there is no task view for the task id, else the index of the task view. */ public int getTaskIndexForId(int taskId) { TaskView tv = getTaskView(taskId); return tv == null ? -1 : indexOfChild(tv); } public int getTaskViewStartIndex() { return mTaskViewStartIndex; } /** * Reloads the view if anything in recents changed. */ public void reloadIfNeeded() { if (!mModel.isTaskListValid(mTaskListChangeId)) { mTaskListChangeId = mModel.getTasks(this::applyLoadPlan); } } /** * Called when a gesture from an app is starting. */ public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) { mGestureActive = true; // This needs to be called before the other states are set since it can create the task view if (mOrientationState.setGestureActive(true)) { updateOrientationHandler(); } showCurrentTask(runningTaskInfo); setEnableFreeScroll(false); setEnableDrawingLiveTile(false); setRunningTaskHidden(true); setTaskIconScaledDown(true); } /** * Called only when a swipe-up gesture from an app has completed. Only called after * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}. */ public void onSwipeUpAnimationSuccess() { Log.d("b/186444448", "onSwipeUpAnimationSuccess"); animateUpTaskIconScale(); setSwipeDownShouldLaunchApp(true); } private void animateRecentsRotationInPlace(int newRotation) { if (mOrientationState.canRecentsActivityRotate()) { // Let system take care of the rotation return; } AnimatorSet pa = setRecentsChangedOrientation(true); pa.addListener(AnimatorListeners.forSuccessCallback(() -> { setLayoutRotation(newRotation, mOrientationState.getDisplayRotation()); mActivity.getDragLayer().recreateControllers(); updateChildTaskOrientations(); setRecentsChangedOrientation(false).start(); })); pa.start(); } public AnimatorSet setRecentsChangedOrientation(boolean fadeInChildren) { getRunningTaskIndex(); int runningIndex = getCurrentPage(); AnimatorSet as = new AnimatorSet(); for (int i = 0; i < getTaskViewCount(); i++) { if (runningIndex == i) { continue; } View taskView = getTaskViewAt(i); as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1)); } return as; } private void updateChildTaskOrientations() { for (int i = 0; i < getTaskViewCount(); i++) { getTaskViewAt(i).setOrientationState(mOrientationState); } TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU); if (tv != null) { tv.onRotationChanged(); } } /** * Called when a gesture from an app has finished, and an end target has been determined. */ public void onPrepareGestureEndAnimation( @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget) { if (mSizeStrategy.stateFromGestureEndTarget(endTarget) .displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) { if (animatorSet == null) { setGridProgress(1); } else { animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1)); } } mCurrentGestureEndTarget = endTarget; if (endTarget == GestureState.GestureEndTarget.NEW_TASK || endTarget == GestureState.GestureEndTarget.LAST_TASK) { // When switching to tasks in quick switch, ensures the snapped page's scroll maintain // invariant between quick switch and overview, to ensure a smooth animation transition. updateGridProperties(); } else if (endTarget == GestureState.GestureEndTarget.RECENTS) { setEnableFreeScroll(true); } } /** * Called when a gesture from an app has finished, and the animation to the target has ended. */ public void onGestureAnimationEnd() { Log.d("b/186444448", "onGestureEnd"); mGestureActive = false; if (mOrientationState.setGestureActive(false)) { updateOrientationHandler(); } setEnableFreeScroll(true); setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS); if (!LIVE_TILE.get()) { setRunningTaskViewShowScreenshot(true); } setRunningTaskHidden(false); animateUpTaskIconScale(); animateActionsViewIn(); mCurrentGestureEndTarget = null; } /** * Returns true if we should add a stub taskView for the running task id */ protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) { return runningTaskInfo != null && getTaskView(runningTaskInfo.taskId) == null; } /** * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}. * * All subsequent calls to reload will keep the task as the first item until {@link #reset()} * is called. Also scrolls the view to this task. */ public void showCurrentTask(RunningTaskInfo runningTaskInfo) { if (shouldAddStubTaskView(runningTaskInfo)) { boolean wasEmpty = getChildCount() == 0; // Add an empty view for now until the task plan is loaded and applied final TaskView taskView = mTaskViewPool.getView(); addView(taskView, mTaskViewStartIndex); if (wasEmpty) { addView(mClearAllButton); } // The temporary running task is only used for the duration between the start of the // gesture and the task list is loaded and applied mTmpRunningTask = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false); taskView.bind(mTmpRunningTask, mOrientationState); // Measure and layout immediately so that the scroll values is updated instantly // as the user might be quick-switching measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY), makeMeasureSpec(getMeasuredHeight(), EXACTLY)); layout(getLeft(), getTop(), getRight(), getBottom()); } boolean runningTaskTileHidden = mRunningTaskTileHidden; int runningTaskId = runningTaskInfo == null ? -1 : runningTaskInfo.taskId; setCurrentTask(runningTaskId); mFocusedTaskId = runningTaskId; setCurrentPage(getRunningTaskIndex()); setRunningTaskViewShowScreenshot(false); setRunningTaskHidden(runningTaskTileHidden); // Update task size after setting current task. updateTaskSize(); // Reload the task list mTaskListChangeId = mModel.getTasks(this::applyLoadPlan); } /** * Sets the running task id, cleaning up the old running task if necessary. */ public void setCurrentTask(int runningTaskId) { if (mRunningTaskId == runningTaskId) { return; } if (mRunningTaskId != -1) { // Reset the state on the old running task view setTaskIconScaledDown(false); setRunningTaskViewShowScreenshot(true); setRunningTaskHidden(false); } mRunningTaskId = runningTaskId; } /** * Hides the tile associated with {@link #mRunningTaskId} */ public void setRunningTaskHidden(boolean isHidden) { mRunningTaskTileHidden = isHidden; TaskView runningTask = getRunningTaskView(); if (runningTask != null) { runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha); if (!isHidden) { AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask, AccessibilityEvent.TYPE_VIEW_FOCUSED, null); } } } private void setRunningTaskViewShowScreenshot(boolean showScreenshot) { if (LIVE_TILE.get()) { TaskView runningTaskView = getRunningTaskView(); if (runningTaskView != null) { runningTaskView.setShowScreenshot(showScreenshot); } } } public void setTaskIconScaledDown(boolean isScaledDown) { if (mTaskIconScaledDown != isScaledDown) { mTaskIconScaledDown = isScaledDown; int taskCount = getTaskViewCount(); for (int i = 0; i < taskCount; i++) { getTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); } } } private void animateActionsViewIn() { ObjectAnimator anim = ObjectAnimator.ofFloat( mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 0, 1); anim.setDuration(TaskView.SCALE_ICON_DURATION); anim.start(); } public void animateUpTaskIconScale() { mTaskIconScaledDown = false; Log.d("b/186444448", "animateUpRunningTaskIconScale"); int taskCount = getTaskViewCount(); for (int i = 0; i < taskCount; i++) { TaskView taskView = getTaskViewAt(i); taskView.setIconScaleAnimStartProgress(0f); taskView.animateIconScaleAndDimIntoView(); } } /** Updates TaskView and ClearAllButtion scaling and translation required to turn into grid * layout. * This method is used when no task dismissal has occurred. */ private void updateGridProperties() { updateGridProperties(false); } /** * Updates TaskView and ClearAllButton scaling and translation required to turn into grid * layout. * This method only calculates the potential position and depends on {@link #setGridProgress} to * apply the actual scaling and translation. * * @param isTaskDismissal indicates if update was called due to task dismissal */ private void updateGridProperties(boolean isTaskDismissal) { int taskCount = getTaskViewCount(); if (taskCount == 0) { return; } int taskTopMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx; int topRowWidth = 0; int bottomRowWidth = 0; float topAccumulatedTranslationX = 0; float bottomAccumulatedTranslationX = 0; // Contains whether the child index is in top or bottom of grid (for non-focused task) // Different from mTopRowIdSet, which contains the taskId of what task is in top row IntSet topSet = new IntSet(); IntSet bottomSet = new IntSet(); // Horizontal grid translation for each task float[] gridTranslations = new float[taskCount]; int focusedTaskIndex = Integer.MAX_VALUE; int focusedTaskShift = 0; int focusedTaskWidthAndSpacing = 0; int snappedTaskRowWidth = 0; int snappedPage = getNextPage(); TaskView snappedTaskView = getTaskViewAtByAbsoluteIndex(snappedPage); TaskView homeTaskView = getHomeTaskView(); TaskView nextFocusedTaskView = null; if (!isTaskDismissal) { mTopRowIdSet.clear(); } for (int i = 0; i < taskCount; i++) { TaskView taskView = getTaskViewAt(i); int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing; // Evenly distribute tasks between rows unless rearranging due to task dismissal, in // which case keep tasks in their respective rows. For the running task, don't join // the grid. if (taskView.isFocusedTask()) { topRowWidth += taskWidthAndSpacing; bottomRowWidth += taskWidthAndSpacing; focusedTaskIndex = i; focusedTaskWidthAndSpacing = taskWidthAndSpacing; gridTranslations[i] += focusedTaskShift; gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; // Center view vertically in case it's from different orientation. taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin - taskView.getLayoutParams().height) / 2f); if (taskView == snappedTaskView) { // If focused task is snapped, the row width is just task width and spacing. snappedTaskRowWidth = taskWidthAndSpacing; } } else { if (i > focusedTaskIndex) { // For tasks after the focused task, shift by focused task's width and spacing. gridTranslations[i] += mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; } else { // For task before the focused task, accumulate the width and spacing to // calculate the distance focused task need to shift. focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; } int taskId = taskView.getTaskId(); boolean isTopRow = isTaskDismissal ? mTopRowIdSet.contains(taskId) : topRowWidth <= bottomRowWidth; if (isTopRow) { if (homeTaskView != null && nextFocusedTaskView == null) { // TaskView will be focused when swipe up, don't count towards row width. nextFocusedTaskView = taskView; } else { topRowWidth += taskWidthAndSpacing; } topSet.add(i); mTopRowIdSet.add(taskId); taskView.setGridTranslationY(mTaskGridVerticalDiff); // Move horizontally into empty space. float widthOffset = 0; for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) { if (j == focusedTaskIndex) { continue; } widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing; } float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX; topAccumulatedTranslationX += currentTaskTranslationX; } else { bottomRowWidth += taskWidthAndSpacing; bottomSet.add(i); // Move into bottom row. taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff); // Move horizontally into empty space. float widthOffset = 0; for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) { if (j == focusedTaskIndex) { continue; } widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing; } float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX; bottomAccumulatedTranslationX += currentTaskTranslationX; } if (taskView == snappedTaskView) { snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth; } } } // We need to maintain snapped task's page scroll invariant between quick switch and // overview, so we sure snapped task's grid translation is 0, and add a non-fullscreen // translationX that is the same as snapped task's full scroll adjustment. float snappedTaskNonGridScrollAdjustment = 0; float snappedTaskGridTranslationX = 0; if (snappedTaskView != null) { snappedTaskNonGridScrollAdjustment = snappedTaskView.getScrollAdjustment( /*fullscreenEnabled=*/true, /*gridEnabled=*/false); snappedTaskGridTranslationX = gridTranslations[snappedPage - mTaskViewStartIndex]; } for (int i = 0; i < taskCount; i++) { TaskView taskView = getTaskViewAt(i); taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX + snappedTaskNonGridScrollAdjustment); } // Use the accumulated translation of the row containing the last task. float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1) ? topAccumulatedTranslationX : bottomAccumulatedTranslationX; // If the last task is on the shorter row, ClearAllButton will embed into the shorter row // which is not what we want. Compensate the width difference of the 2 rows in that case. float shorterRowCompensation = 0; if (topRowWidth <= bottomRowWidth) { if (topSet.contains(taskCount - 1)) { shorterRowCompensation = bottomRowWidth - topRowWidth; } } else { if (bottomSet.contains(taskCount - 1)) { shorterRowCompensation = topRowWidth - bottomRowWidth; } } float clearAllShorterRowCompensation = mIsRtl ? -shorterRowCompensation : shorterRowCompensation; // If the total width is shorter than one grid's width, move ClearAllButton further away // accordingly. Update longRowWidth if ClearAllButton has been moved. float clearAllShortTotalCompensation = 0; int longRowWidth = Math.max(topRowWidth, bottomRowWidth); if (longRowWidth < mLastComputedGridSize.width()) { float shortTotalCompensation = mLastComputedGridSize.width() - longRowWidth; clearAllShortTotalCompensation = mIsRtl ? -shortTotalCompensation : shortTotalCompensation; longRowWidth = mLastComputedGridSize.width(); } float clearAllTotalTranslationX = clearAllAccumulatedTranslation + clearAllShorterRowCompensation + clearAllShortTotalCompensation + snappedTaskNonGridScrollAdjustment; if (focusedTaskIndex < taskCount) { // Shift by focused task's width and spacing if a task is focused. clearAllTotalTranslationX += mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; } // Make sure there are enough space between snapped page and ClearAllButton, for the case // of swiping up after quick switch. if (snappedTaskView != null) { int distanceFromClearAll = longRowWidth - snappedTaskRowWidth; // ClearAllButton should be off screen when snapped task is in its snapped position. int minimumDistance = mTaskWidth - snappedTaskView.getLayoutParams().width + (mLastComputedGridSize.width() - mTaskWidth) / 2; if (distanceFromClearAll < minimumDistance) { int distanceDifference = minimumDistance - distanceFromClearAll; clearAllTotalTranslationX += mIsRtl ? -distanceDifference : distanceDifference; } } mClearAllButton.setGridTranslationPrimary( clearAllTotalTranslationX - snappedTaskGridTranslationX); mClearAllButton.setGridScrollOffset( mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left : mLastComputedTaskSize.right - mLastComputedGridSize.right); setGridProgress(mGridProgress); } private boolean isSameGridRow(TaskView taskView1, TaskView taskView2) { if (taskView1 == null || taskView2 == null) { return false; } int taskId1 = taskView1.getTaskId(); int taskId2 = taskView2.getTaskId(); if (taskId1 == mFocusedTaskId || taskId2 == mFocusedTaskId) { return false; } return (mTopRowIdSet.contains(taskId1) && mTopRowIdSet.contains(taskId2)) || ( !mTopRowIdSet.contains(taskId1) && !mTopRowIdSet.contains(taskId2)); } /** * Moves TaskView and ClearAllButton between carousel and 2 row grid. * * @param gridProgress 0 = carousel; 1 = 2 row grid. */ private void setGridProgress(float gridProgress) { int taskCount = getTaskViewCount(); if (taskCount == 0) { return; } mGridProgress = gridProgress; for (int i = 0; i < taskCount; i++) { getTaskViewAt(i).setGridProgress(gridProgress); } mClearAllButton.setGridProgress(gridProgress); } private void enableLayoutTransitions() { if (mLayoutTransition == null) { mLayoutTransition = new LayoutTransition(); mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING); mLayoutTransition.setDuration(ADDITION_TASK_DURATION); mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0); mLayoutTransition.addTransitionListener(new TransitionListener() { @Override public void startTransition(LayoutTransition transition, ViewGroup viewGroup, View view, int i) { } @Override public void endTransition(LayoutTransition transition, ViewGroup viewGroup, View view, int i) { // When the unpinned task is added, snap to first page and disable transitions if (view instanceof TaskView) { snapToPage(0); setLayoutTransition(null); } } }); } setLayoutTransition(mLayoutTransition); } public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; } public boolean shouldSwipeDownLaunchApp() { return mSwipeDownShouldLaunchApp; } public void setIgnoreResetTask(int taskId) { mIgnoreResetTaskId = taskId; } public void clearIgnoreResetTask(int taskId) { if (mIgnoreResetTaskId == taskId) { mIgnoreResetTaskId = -1; } } private void addDismissedTaskAnimations(TaskView taskView, long duration, PendingAnimation anim) { // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's // alpha is set to 0 so that it can be recycled in the view pool properly anim.setFloat(taskView, VIEW_ALPHA, 0, clampToProgress(ACCEL, 0, 0.5f)); SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController(); ResourceProvider rp = DynamicResource.provider(mActivity); SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START) .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio)) .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness)); FloatProperty dismissingTaskViewTranslate = taskView.getSecondaryDissmissTranslationProperty(); // TODO(b/186800707) translate entire grid size distance int translateDistance = mOrientationHandler.getSecondaryDimension(taskView); int positiveNegativeFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor(); if (splitController.isSplitSelectActive()) { // Have the task translate towards whatever side was just pinned int dir = mOrientationHandler.getSplitTaskViewDismissDirection(splitController .getActiveSplitPositionOption(), mActivity.getDeviceProfile()); switch (dir) { case PagedOrientationHandler.SPLIT_TRANSLATE_SECONDARY_NEGATIVE: dismissingTaskViewTranslate = taskView .getSecondaryDissmissTranslationProperty(); positiveNegativeFactor = -1; break; case PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_POSITIVE: dismissingTaskViewTranslate = taskView.getPrimaryDismissTranslationProperty(); positiveNegativeFactor = 1; break; case PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_NEGATIVE: dismissingTaskViewTranslate = taskView.getPrimaryDismissTranslationProperty(); positiveNegativeFactor = -1; break; default: throw new IllegalStateException("Invalid split task translation: " + dir); } } // Double translation distance so dismissal drag is the full height, as we only animate // the drag for the first half of the progress. anim.add(ObjectAnimator.ofFloat(taskView, dismissingTaskViewTranslate, positiveNegativeFactor * translateDistance * 2).setDuration(duration), LINEAR, sp); if (LIVE_TILE.get() && mEnableDrawingLiveTile && taskView.isRunningTask()) { anim.addOnFrameCallback(() -> { mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = mOrientationHandler.getSecondaryValue( taskView.getTranslationX(), taskView.getTranslationY()); redrawLiveTile(); }); } } /** * Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}. * @param dismissedTaskView the {@link TaskView} to be dismissed * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated * @param shouldRemoveTask whether the associated {@link Task} should be removed from * ActivityManager after dismissal * @param duration duration of the animation */ public PendingAnimation createTaskDismissAnimation(TaskView dismissedTaskView, boolean animateTaskView, boolean shouldRemoveTask, long duration) { if (mPendingAnimation != null) { mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd(); } PendingAnimation anim = new PendingAnimation(duration); int count = getPageCount(); if (count == 0) { return anim; } boolean showAsGrid = showAsGrid(); int taskCount = getTaskViewCount(); int dismissedIndex = indexOfChild(dismissedTaskView); int dismissedTaskId = dismissedTaskView.getTaskId(); // Grid specific properties. boolean isFocusedTaskDismissed = false; TaskView nextFocusedTaskView = null; boolean nextFocusedTaskFromTop = false; float dismissedTaskWidth = 0; float nextFocusedTaskWidth = 0; // Non-grid specific properties. int[] oldScroll = new int[count]; int[] newScroll = new int[count]; int scrollDiffPerPage = 0; boolean needsCurveUpdates = false; if (showAsGrid) { dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing; isFocusedTaskDismissed = dismissedTaskId == mFocusedTaskId; if (isFocusedTaskDismissed) { nextFocusedTaskFromTop = mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f; // Pick the next focused task from the preferred row. for (int i = 0; i < taskCount; i++) { TaskView taskView = getTaskViewAt(i); if (taskView == dismissedTaskView) { continue; } boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskId()); if ((nextFocusedTaskFromTop && isTopRow || (!nextFocusedTaskFromTop && !isTopRow))) { nextFocusedTaskView = taskView; break; } } if (nextFocusedTaskView != null) { nextFocusedTaskWidth = nextFocusedTaskView.getLayoutParams().width + mPageSpacing; } } } else { getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); getPageScrolls(newScroll, false, v -> v.getVisibility() != GONE && v != dismissedTaskView); if (count > 1) { scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); } } int distanceFromDismissedTask = 0; for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child == dismissedTaskView) { if (animateTaskView) { addDismissedTaskAnimations(dismissedTaskView, duration, anim); } } else if (!showAsGrid) { // Compute scroll offsets from task dismissal for animation. // If we just take newScroll - oldScroll, everything to the right of dragged task // translates to the left. We need to offset this in some cases: // - In RTL, add page offset to all pages, since we want pages to move to the right // Additionally, add a page offset if: // - Current page is rightmost page (leftmost for RTL) // - Dragging an adjacent page on the left side (right side for RTL) int offset = mIsRtl ? scrollDiffPerPage : 0; if (mCurrentPage == dismissedIndex) { int lastPage = taskCount - 1; if (mCurrentPage == lastPage) { offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; } } else { // Dismissing an adjacent page. int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) if (dismissedIndex == negativeAdjacent) { offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; } } int scrollDiff = newScroll[i] - oldScroll[i] + offset; if (scrollDiff != 0) { FloatProperty translationProperty = child instanceof TaskView ? ((TaskView) child).getPrimaryDismissTranslationProperty() : mOrientationHandler.getPrimaryViewTranslate(); float additionalDismissDuration = ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs( i - dismissedIndex); anim.setFloat(child, translationProperty, scrollDiff, clampToProgress(LINEAR, Utilities.boundToRange(INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET + additionalDismissDuration, 0f, 1f), 1)); if (LIVE_TILE.get() && mEnableDrawingLiveTile && child instanceof TaskView && ((TaskView) child).isRunningTask()) { anim.addOnFrameCallback(() -> { mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = mOrientationHandler.getPrimaryValue(child.getTranslationX(), child.getTranslationY()); redrawLiveTile(); }); } needsCurveUpdates = true; } } else if (child instanceof TaskView) { TaskView taskView = (TaskView) child; if (isFocusedTaskDismissed) { if (!isSameGridRow(taskView, nextFocusedTaskView)) { continue; } } else { if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) { continue; } } // Animate task with index >= dismissed index and in the same row as the // dismissed index or next focused index. Offset successive task dismissal // durations for a staggered effect. float animationStartProgress = Utilities.boundToRange( INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * ++distanceFromDismissedTask, 0f, 1f); if (taskView == nextFocusedTaskView) { // Enlarge the task to be focused next, and translate into focus position. float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width(); anim.setFloat(taskView, TaskView.SNAPSHOT_SCALE, scale, clampToProgress(LINEAR, animationStartProgress, 1f)); anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth, clampToProgress(LINEAR, animationStartProgress, 1f)); float secondaryTranslation = -mTaskGridVerticalDiff; if (!nextFocusedTaskFromTop) { secondaryTranslation -= mTopBottomRowHeightDiff; } anim.setFloat(taskView, taskView.getSecondaryDissmissTranslationProperty(), secondaryTranslation, clampToProgress(LINEAR, animationStartProgress, 1f)); anim.setFloat(taskView, TaskView.FOCUS_TRANSITION, 0f, clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT)); } else { float primaryTranslation = isFocusedTaskDismissed ? nextFocusedTaskWidth : dismissedTaskWidth; anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), mIsRtl ? primaryTranslation : -primaryTranslation, clampToProgress(LINEAR, animationStartProgress, 1f)); } } } if (needsCurveUpdates) { anim.addOnFrameCallback(this::updateCurveProperties); } // Add a tiny bit of translation Z, so that it draws on top of other views if (animateTaskView) { dismissedTaskView.setTranslationZ(0.1f); } mPendingAnimation = anim; final TaskView finalNextFocusedTaskView = nextFocusedTaskView; mPendingAnimation.addEndListener(new Consumer() { @Override public void accept(Boolean success) { if (LIVE_TILE.get() && mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) { finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> onEnd(success)); } else { onEnd(success); } } @SuppressWarnings("WrongCall") private void onEnd(boolean success) { if (success) { if (shouldRemoveTask) { if (dismissedTaskView.getTask() != null) { if (LIVE_TILE.get() && dismissedTaskView.isRunningTask()) { finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> removeTaskInternal(dismissedTaskId)); } else { removeTaskInternal(dismissedTaskId); } mActivity.getStatsLogManager().logger() .withItemInfo(dismissedTaskView.getItemInfo()) .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); } } // Reset task translations as they may have updated via animations in // createTaskDismissAnimation resetTaskVisuals(); int pageToSnapTo = mCurrentPage; if (finalNextFocusedTaskView != null) { pageToSnapTo = indexOfChild(finalNextFocusedTaskView); } if (dismissedIndex < pageToSnapTo || pageToSnapTo == (taskCount - 1)) { pageToSnapTo -= 1; } removeViewInLayout(dismissedTaskView); mTopRowIdSet.remove(dismissedTaskId); if (taskCount == 1) { removeViewInLayout(mClearAllButton); startHome(); } else { // Update focus task and its size. if (finalNextFocusedTaskView != null) { mFocusedTaskId = finalNextFocusedTaskView.getTaskId(); mTopRowIdSet.remove(mFocusedTaskId); finalNextFocusedTaskView.animateIconScaleAndDimIntoView(); setCurrentPage(pageToSnapTo); } updateTaskSize(true); // Update scroll and snap to page. updateScrollSynchronously(); snapToPageImmediately(pageToSnapTo); dispatchScrollChanged(); } } onDismissAnimationEnds(); mPendingAnimation = null; } }); return anim; } private void removeTaskInternal(int dismissedTaskId) { UI_HELPER_EXECUTOR.getHandler().postDelayed( () -> ActivityManagerWrapper.getInstance().removeTask(dismissedTaskId), REMOVE_TASK_WAIT_FOR_APP_STOP_MS); } /** * @return {@code true} if one of the task thumbnails would intersect/overlap with the * {@link #mSplitPlaceholderView} */ public boolean shouldShiftThumbnailsForSplitSelect(@SplitConfigurationOptions.StagePosition int stagePosition) { if (!mActivity.getDeviceProfile().isTablet) { // Never enough space on phones return true; } else if (!mActivity.getDeviceProfile().isLandscape) { return false; } Rect splitBounds = new Rect(); float placeholderSize = getResources().getDimension(R.dimen.split_placeholder_size); // This acts as a best approximation on where the splitplaceholder view would be, // doesn't need to be exact necessarily. This also doesn't need to take translations // into account since placeholder view is not translated if (stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT) { splitBounds.set((int) (getWidth() - placeholderSize), 0, getWidth(), getHeight()); } else { splitBounds.set(0, 0, (int) (placeholderSize), getHeight()); } Rect taskBounds = new Rect(); int taskCount = getTaskViewCount(); for (int i = 0; i < taskCount; i++) { TaskView taskView = getTaskViewAt(i); if (taskView == mSplitHiddenTaskView && !(showAsGrid() && taskView == getFocusedTaskView())) { // Case where the hidden task view would have overlapped w/ placeholder, // but because it's going to hide we don't care // TODO (b/187312247) edge case for thumbnails that are off screen but scroll on continue; } taskView.getBoundsOnScreen(taskBounds); if (Rect.intersects(taskBounds, splitBounds)) { return true; } } return false; } protected void onDismissAnimationEnds() { } public PendingAnimation createAllTasksDismissAnimation(long duration) { if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) { throw new IllegalStateException("Another pending animation is still running"); } PendingAnimation anim = new PendingAnimation(duration); int count = getTaskViewCount(); for (int i = 0; i < count; i++) { addDismissedTaskAnimations(getTaskViewAt(i), duration, anim); } mPendingAnimation = anim; mPendingAnimation.addEndListener(isSuccess -> { if (isSuccess) { // Remove all the task views now finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> { UI_HELPER_EXECUTOR.getHandler().postDelayed( ActivityManagerWrapper.getInstance()::removeAllRecentTasks, REMOVE_TASK_WAIT_FOR_APP_STOP_MS); removeTasksViewsAndClearAllButton(); startHome(); }); } mPendingAnimation = null; }); return anim; } private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) { if (pageCount == 0) { return false; } final int newPageUnbound = getNextPage() + delta; if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) { return false; } snapToPage((newPageUnbound + pageCount) % pageCount); getChildAt(getNextPage()).requestFocus(); return true; } private void runDismissAnimation(PendingAnimation pendingAnim) { AnimatorPlaybackController controller = pendingAnim.createPlaybackController(); controller.dispatchOnStart(); controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); controller.start(); } @UiThread private void dismissTask(int taskId) { TaskView taskView = getTaskView(taskId); if (taskView == null) { return; } dismissTask(taskView, true /* animate */, false /* removeTask */); } public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, DISMISS_TASK_DURATION)); } @SuppressWarnings("unused") private void dismissAllTasks(View view) { runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); mActivity.getStatsLogManager().logger().log(LAUNCHER_TASK_CLEAR_ALL); } private void dismissCurrentTask() { TaskView taskView = getNextPageTaskView(); if (taskView != null) { dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_TAB: return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1, event.isAltPressed() /* cycle */); case KeyEvent.KEYCODE_DPAD_RIGHT: return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */); case KeyEvent.KEYCODE_DPAD_LEFT: return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */); case KeyEvent.KEYCODE_DEL: case KeyEvent.KEYCODE_FORWARD_DEL: dismissCurrentTask(); return true; case KeyEvent.KEYCODE_NUMPAD_DOT: if (event.isAltPressed()) { // Numpad DEL pressed while holding Alt. dismissCurrentTask(); return true; } } } return super.dispatchKeyEvent(event); } @Override protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (gainFocus && getChildCount() > 0) { switch (direction) { case FOCUS_FORWARD: setCurrentPage(0); break; case FOCUS_BACKWARD: case FOCUS_RIGHT: case FOCUS_LEFT: setCurrentPage(getChildCount() - 1); break; } } } public float getContentAlpha() { return mContentAlpha; } public void setContentAlpha(float alpha) { if (alpha == mContentAlpha) { return; } alpha = Utilities.boundToRange(alpha, 0, 1); mContentAlpha = alpha; for (int i = getTaskViewCount() - 1; i >= 0; i--) { TaskView child = getTaskViewAt(i); if (!mRunningTaskTileHidden || child.getTaskId() != mRunningTaskId) { child.setStableAlpha(alpha); } } mClearAllButton.setContentAlpha(mContentAlpha); int alphaInt = Math.round(alpha * 255); mEmptyMessagePaint.setAlpha(alphaInt); mEmptyIcon.setAlpha(alphaInt); mActionsView.getContentAlpha().setValue(mContentAlpha); if (alpha > 0) { setVisibility(VISIBLE); } else if (!mFreezeViewVisibility) { setVisibility(INVISIBLE); } } /** * Freezes the view visibility change. When frozen, the view will not change its visibility * to gone due to alpha changes. */ public void setFreezeViewVisibility(boolean freezeViewVisibility) { if (mFreezeViewVisibility != freezeViewVisibility) { mFreezeViewVisibility = freezeViewVisibility; if (!mFreezeViewVisibility) { setVisibility(mContentAlpha > 0 ? VISIBLE : INVISIBLE); } } } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); if (mActionsView != null) { mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE); if (visibility != VISIBLE) { mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false); } } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateRecentsRotation(); } /** * Updates {@link RecentsOrientedState}'s cached RecentsView rotation. */ public void updateRecentsRotation() { final int rotation = mActivity.getDisplay().getRotation(); mOrientationState.setRecentsRotation(rotation); } public void setLayoutRotation(int touchRotation, int displayRotation) { if (mOrientationState.update(touchRotation, displayRotation)) { updateOrientationHandler(); } } public RecentsOrientedState getPagedViewOrientedState() { return mOrientationState; } public PagedOrientationHandler getPagedOrientationHandler() { return mOrientationHandler; } @Nullable public TaskView getNextTaskView() { return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() + 1); } @Nullable public TaskView getCurrentPageTaskView() { return getTaskViewAtByAbsoluteIndex(getCurrentPage()); } @Nullable public TaskView getNextPageTaskView() { return getTaskViewAtByAbsoluteIndex(getNextPage()); } @Nullable public TaskView getTaskViewNearestToCenterOfScreen() { return getTaskViewAtByAbsoluteIndex(getPageNearestToCenterOfScreen()); } /** * Returns null instead of indexOutOfBoundsError when index is not in range */ @Nullable public TaskView getTaskViewAt(int index) { return getTaskViewAtByAbsoluteIndex(index + mTaskViewStartIndex); } @Nullable private TaskView getTaskViewAtByAbsoluteIndex(int index) { if (index < getChildCount() && index >= 0) { View child = getChildAt(index); return child instanceof TaskView ? (TaskView) child : null; } return null; } public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) { mOnEmptyMessageUpdatedListener = listener; } public void updateEmptyMessage() { boolean isEmpty = getTaskViewCount() == 0; boolean hasSizeChanged = mLastMeasureSize.x != getWidth() || mLastMeasureSize.y != getHeight(); if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { return; } setContentDescription(isEmpty ? mEmptyMessage : ""); mShowEmptyMessage = isEmpty; updateEmptyStateUi(hasSizeChanged); invalidate(); if (mOnEmptyMessageUpdatedListener != null) { mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // If we're going to HOME, avoid unnecessary onLayout that cause TaskViews to re-arrange // during animation to HOME. if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.HOME) { return; } super.onLayout(changed, left, top, right, bottom); updateEmptyStateUi(changed); // Update the pivots such that when the task is scaled, it fills the full page getTaskSize(mTempRect); getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect, mActivity.getDeviceProfile(), mTempPointF); setPivotX(mTempPointF.x); setPivotY(mTempPointF.y); setTaskModalness(mTaskModalness); mLastComputedTaskStartPushOutDistance = null; mLastComputedTaskEndPushOutDistance = null; updatePageOffsets(); setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO : IMPORTANT_FOR_ACCESSIBILITY_AUTO); } private void updatePageOffsets() { float offset = mAdjacentPageHorizontalOffset; float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness); int count = getChildCount(); TaskView runningTask = mRunningTaskId == -1 || !mRunningTaskTileHidden ? null : getTaskView(mRunningTaskId); int midpoint = runningTask == null ? -1 : indexOfChild(runningTask); int modalMidpoint = getCurrentPage(); float midpointOffsetSize = 0; float leftOffsetSize = midpoint - 1 >= 0 ? getHorizontalOffsetSize(midpoint - 1, midpoint, offset) : 0; float rightOffsetSize = midpoint + 1 < count ? getHorizontalOffsetSize(midpoint + 1, midpoint, offset) : 0; boolean showAsGrid = showAsGrid(); float modalMidpointOffsetSize = 0; float modalLeftOffsetSize = 0; float modalRightOffsetSize = 0; float gridOffsetSize = 0; if (showAsGrid) { // In grid, we only focus the task on the side. The reference index used for offset // calculation is the task directly next to the focus task in the grid. int referenceIndex = modalMidpoint == 0 ? 1 : 0; gridOffsetSize = referenceIndex < count ? getHorizontalOffsetSize(referenceIndex, modalMidpoint, modalOffset) : 0; } else { modalLeftOffsetSize = modalMidpoint - 1 >= 0 ? getHorizontalOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset) : 0; modalRightOffsetSize = modalMidpoint + 1 < count ? getHorizontalOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset) : 0; } for (int i = 0; i < count; i++) { float translation = i == midpoint ? midpointOffsetSize : i < midpoint ? leftOffsetSize : rightOffsetSize; float modalTranslation = i == modalMidpoint ? modalMidpointOffsetSize : showAsGrid ? gridOffsetSize : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize; float totalTranslation = translation + modalTranslation; View child = getChildAt(i); FloatProperty translationProperty = child instanceof TaskView ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty() : mOrientationHandler.getPrimaryViewTranslate(); translationProperty.set(child, totalTranslation); if (LIVE_TILE.get() && mEnableDrawingLiveTile && i == getRunningTaskIndex()) { mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = totalTranslation; redrawLiveTile(); } } updateCurveProperties(); } /** * Computes the child position with persistent translation considered (see * {@link TaskView#getPersistentTranslationX()}. */ private void getPersistentChildPosition(int childIndex, int midPointScroll, RectF outRect) { View child = getChildAt(childIndex); outRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); if (child instanceof TaskView) { TaskView taskView = (TaskView) child; outRect.offset(taskView.getPersistentTranslationX(), taskView.getPersistentTranslationY()); outRect.top += mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx; mTempMatrix.reset(); float persistentScale = taskView.getPersistentScale(); mTempMatrix.postScale(persistentScale, persistentScale, mIsRtl ? outRect.right : outRect.left, outRect.top); mTempMatrix.mapRect(outRect); } outRect.offset(mOrientationHandler.getPrimaryValue(-midPointScroll, 0), mOrientationHandler.getSecondaryValue(-midPointScroll, 0)); } /** * Computes the distance to offset the given child such that it is completely offscreen when * translating away from the given midpoint. * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen. */ private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) { if (offsetProgress == 0) { // Don't bother calculating everything below if we won't offset anyway. return 0; } // First, get the position of the task relative to the midpoint. If there is no midpoint // then we just use the normal (centered) task position. RectF taskPosition = mTempRectF; // Whether the task should be shifted to start direction (i.e. left edge for portrait, top // edge for landscape/seascape). boolean isStartShift; if (midpointIndex > -1) { // When there is a midpoint reference task, adjacent tasks have less distance to travel // to reach offscreen. Offset the task position to the task's starting point. int midpointScroll = getScrollForPage(midpointIndex); getPersistentChildPosition(midpointIndex, midpointScroll, taskPosition); float midpointStart = mOrientationHandler.getStart(taskPosition); getPersistentChildPosition(childIndex, midpointScroll, taskPosition); // Assume child does not overlap with midPointChild. isStartShift = mOrientationHandler.getStart(taskPosition) < midpointStart; } else { // Position the task at scroll position. getPersistentChildPosition(childIndex, getScrollForPage(childIndex), taskPosition); isStartShift = mIsRtl; } // Next, calculate the distance to move the task off screen. We also need to account for // RecentsView scale, because it moves tasks based on its pivot. To do this, we move the // task position to where it would be offscreen at scale = 1 (computed above), then we // apply the scale via getMatrix() to determine how much that moves the task from its // desired position, and adjust the computed distance accordingly. float distanceToOffscreen; if (isStartShift) { float desiredStart = -mOrientationHandler.getPrimarySize(taskPosition); distanceToOffscreen = -mOrientationHandler.getEnd(taskPosition); if (mLastComputedTaskStartPushOutDistance == null) { taskPosition.offsetTo( mOrientationHandler.getPrimaryValue(desiredStart, 0f), mOrientationHandler.getSecondaryValue(desiredStart, 0f)); getMatrix().mapRect(taskPosition); mLastComputedTaskStartPushOutDistance = mOrientationHandler.getEnd(taskPosition) / mOrientationHandler.getPrimaryScale(this); } distanceToOffscreen -= mLastComputedTaskStartPushOutDistance; } else { float desiredStart = mOrientationHandler.getPrimarySize(this); distanceToOffscreen = desiredStart - mOrientationHandler.getStart(taskPosition); if (mLastComputedTaskEndPushOutDistance == null) { taskPosition.offsetTo( mOrientationHandler.getPrimaryValue(desiredStart, 0f), mOrientationHandler.getSecondaryValue(desiredStart, 0f)); getMatrix().mapRect(taskPosition); mLastComputedTaskEndPushOutDistance = (mOrientationHandler.getStart(taskPosition) - desiredStart) / mOrientationHandler.getPrimaryScale(this); } distanceToOffscreen -= mLastComputedTaskEndPushOutDistance; } return distanceToOffscreen * offsetProgress; } protected void setTaskViewsResistanceTranslation(float translation) { mTaskViewsSecondaryTranslation = translation; for (int i = 0; i < getTaskViewCount(); i++) { TaskView task = getTaskViewAt(i); task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY()); } mLiveTileTaskViewSimulator.recentsViewSecondaryTranslation.value = translation; } protected void setTaskViewsPrimarySplitTranslation(float translation) { mTaskViewsPrimarySplitTranslation = translation; for (int i = 0; i < getTaskViewCount(); i++) { TaskView task = getTaskViewAt(i); task.getPrimarySplitTranslationProperty().set(task, translation); } } protected void setTaskViewsSecondarySplitTranslation(float translation) { mTaskViewsSecondarySplitTranslation = translation; for (int i = 0; i < getTaskViewCount(); i++) { TaskView task = getTaskViewAt(i); task.getSecondarySplitTranslationProperty().set(task, translation); } } /** * Resets the visuals when exit modal state. */ public void resetModalVisuals() { TaskView taskView = getCurrentPageTaskView(); if (taskView != null) { taskView.getThumbnail().getTaskOverlay().resetModalVisuals(); } } public void initiateSplitSelect(TaskView taskView, SplitPositionOption splitPositionOption) { mSplitHiddenTaskView = taskView; SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController(); Rect initialBounds = new Rect(taskView.getLeft(), taskView.getTop(), taskView.getRight(), taskView.getBottom()); splitController.setInitialTaskSelect(taskView, splitPositionOption, initialBounds); mSplitHiddenTaskViewIndex = indexOfChild(taskView); mSplitPlaceholderView.setLayoutParams( splitController.getLayoutParamsForActivePosition(getResources(), mActivity.getDeviceProfile())); mSplitPlaceholderView.setIcon(taskView.getIconView()); } public PendingAnimation createSplitSelectInitAnimation() { int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext()); return createTaskDismissAnimation(mSplitHiddenTaskView, true, false, duration); } public void confirmSplitSelect(TaskView taskView) { mSplitPlaceholderView.getSplitController().setSecondTaskId(taskView); resetTaskVisuals(); setTranslationY(0); } public PendingAnimation cancelSplitSelect(boolean animate) { SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController(); SplitPositionOption splitOption = splitController.getActiveSplitPositionOption(); Rect initialBounds = splitController.getInitialBounds(); splitController.resetState(); int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext()); PendingAnimation pendingAnim = new PendingAnimation(duration); if (!animate) { resetFromSplitSelectionState(); return pendingAnim; } addViewInLayout(mSplitHiddenTaskView, mSplitHiddenTaskViewIndex, mSplitHiddenTaskView.getLayoutParams()); mSplitHiddenTaskView.setAlpha(0); int[] oldScroll = new int[getChildCount()]; getPageScrolls(oldScroll, false, view -> view.getVisibility() != GONE && view != mSplitHiddenTaskView); int[] newScroll = new int[getChildCount()]; getPageScrolls(newScroll, false, SIMPLE_SCROLL_LOGIC); boolean needsCurveUpdates = false; for (int i = mSplitHiddenTaskViewIndex; i >= 0; i--) { View child = getChildAt(i); if (child == mSplitHiddenTaskView) { TaskView taskView = (TaskView) child; int dir = mOrientationHandler.getSplitTaskViewDismissDirection(splitOption, mActivity.getDeviceProfile()); FloatProperty dismissingTaskViewTranslate; Rect hiddenBounds = new Rect(taskView.getLeft(), taskView.getTop(), taskView.getRight(), taskView.getBottom()); int distanceDelta = 0; if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_SECONDARY_NEGATIVE) { dismissingTaskViewTranslate = taskView .getSecondaryDissmissTranslationProperty(); distanceDelta = initialBounds.top - hiddenBounds.top; taskView.layout(initialBounds.left, hiddenBounds.top, initialBounds.right, hiddenBounds.bottom); } else { dismissingTaskViewTranslate = taskView .getPrimaryDismissTranslationProperty(); distanceDelta = initialBounds.left - hiddenBounds.left; taskView.layout(hiddenBounds.left, initialBounds.top, hiddenBounds.right, initialBounds.bottom); if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_POSITIVE) { distanceDelta *= -1; } } pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, dismissingTaskViewTranslate, distanceDelta)); pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, ALPHA, 1)); } else { // If insertion is on last index (furthest from clear all), we directly add the view // else we translate all views to the right of insertion index further right, // ignore views to left if (showAsGrid()) { // TODO(b/186800707) handle more elegantly for grid continue; } int scrollDiff = newScroll[i] - oldScroll[i]; if (scrollDiff != 0) { FloatProperty translationProperty = child instanceof TaskView ? ((TaskView) child).getPrimaryDismissTranslationProperty() : mOrientationHandler.getPrimaryViewTranslate(); ResourceProvider rp = DynamicResource.provider(mActivity); SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END) .setDampingRatio( rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio)) .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness)); pendingAnim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff) .setDuration(duration), ACCEL, sp); needsCurveUpdates = true; } } } if (needsCurveUpdates) { pendingAnim.addOnFrameCallback(this::updateCurveProperties); } pendingAnim.addListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { // TODO(b/186800707) Figure out how to undo for grid view // Need to handle cases where dismissed task is // * Top Row // * Bottom Row // * Focused Task updateGridProperties(); resetFromSplitSelectionState(); } }); return pendingAnim; } private void resetFromSplitSelectionState() { mSplitHiddenTaskView.setTranslationY(0); if (!showAsGrid()) { // TODO(b/186800707) int pageToSnapTo = mCurrentPage; if (mSplitHiddenTaskViewIndex <= pageToSnapTo) { pageToSnapTo += 1; } else { pageToSnapTo = mSplitHiddenTaskViewIndex; } snapToPageImmediately(pageToSnapTo); } onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom()); resetTaskVisuals(); mSplitHiddenTaskView = null; mSplitHiddenTaskViewIndex = -1; } private void updateDeadZoneRects() { // Get the deadzone rect surrounding the clear all button to not dismiss overview to home mClearAllButtonDeadZoneRect.setEmpty(); if (mClearAllButton.getWidth() > 0) { int verticalMargin = getResources() .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin); mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect); mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin); } // Get the deadzone rect between the task views mTaskViewDeadZoneRect.setEmpty(); int count = getTaskViewCount(); if (count > 0) { final View taskView = getTaskViewAt(0); getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), taskView.getBottom()); } } private void updateEmptyStateUi(boolean sizeChanged) { boolean hasValidSize = getWidth() > 0 && getHeight() > 0; if (sizeChanged && hasValidSize) { mEmptyTextLayout = null; mLastMeasureSize.set(getWidth(), getHeight()); } if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), mEmptyMessagePaint, availableWidth) .setAlignment(Layout.Alignment.ALIGN_CENTER) .build(); int totalHeight = mEmptyTextLayout.getHeight() + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); int top = (mLastMeasureSize.y - totalHeight) / 2; int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), top + mEmptyIcon.getIntrinsicHeight()); } } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); } protected void maybeDrawEmptyMessage(Canvas canvas) { if (mShowEmptyMessage && mEmptyTextLayout != null) { // Offset to center in the visible (non-padded) part of RecentsView mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); canvas.save(); canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, (mTempRect.top - mTempRect.bottom) / 2); mEmptyIcon.draw(canvas); canvas.translate(mEmptyMessagePadding, mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); mEmptyTextLayout.draw(canvas); canvas.restore(); } } /** * Animate adjacent tasks off screen while scaling up. * * If launching one of the adjacent tasks, parallax the center task and other adjacent task * to the right. */ public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) { AnimatorSet anim = new AnimatorSet(); int taskIndex = indexOfChild(tv); int centerTaskIndex = getCurrentPage(); boolean launchingCenterTask = taskIndex == centerTaskIndex; float toScale = getMaxScaleForFullScreen(); RecentsView recentsView = tv.getRecentsView(); if (launchingCenterTask) { anim.play(ObjectAnimator.ofFloat(recentsView, RECENTS_SCALE_PROPERTY, toScale)); anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1)); } else { // We are launching an adjacent task, so parallax the center and other adjacent task. float displacementX = tv.getWidth() * (toScale - 1f); float primaryTranslation = mIsRtl ? -displacementX : displacementX; anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation)); int runningTaskIndex = recentsView.getRunningTaskIndex(); if (LIVE_TILE.get() && runningTaskIndex != -1 && runningTaskIndex != taskIndex) { anim.play(ObjectAnimator.ofFloat( recentsView.getLiveTileTaskViewSimulator().taskPrimaryTranslation, AnimatedFloat.VALUE, primaryTranslation)); } int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { PropertyValuesHolder[] properties = new PropertyValuesHolder[3]; properties[0] = PropertyValuesHolder.ofFloat( mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation); properties[1] = PropertyValuesHolder.ofFloat(View.SCALE_X, 1); properties[2] = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1); anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex), properties)); } } return anim; } /** * Returns the scale up required on the view, so that it coves the screen completely */ public float getMaxScaleForFullScreen() { getTaskSize(mTempRect); return getPagedViewOrientedState().getFullScreenScaleAndPivot( mTempRect, mActivity.getDeviceProfile(), mTempPointF); } public PendingAnimation createTaskLaunchAnimation( TaskView tv, long duration, Interpolator interpolator) { if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) { throw new IllegalStateException("Another pending animation is still running"); } int count = getTaskViewCount(); if (count == 0) { return new PendingAnimation(duration); } // When swiping down from overview to tasks, ensures the snapped page's scroll maintain // invariant between quick switch and overview, to ensure a smooth animation transition. updateGridProperties(); updateScrollSynchronously(); int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); final boolean[] passedOverviewThreshold = new boolean[] {false}; ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1); progressAnim.addUpdateListener(animator -> { // Once we pass a certain threshold, update the sysui flags to match the target // tasks' flags if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) { mActivity.getSystemUiController().updateUiState( UI_STATE_FULLSCREEN_TASK, targetSysUiFlags); } else { mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0); } // Passing the threshold from taskview to fullscreen app will vibrate final boolean passed = animator.getAnimatedFraction() >= SUCCESS_TRANSITION_PROGRESS; if (passed != passedOverviewThreshold[0]) { passedOverviewThreshold[0] = passed; performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); } }); AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv); DepthController depthController = getDepthController(); if (depthController != null) { ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(mActivity)); anim.play(depthAnimator); } anim.play(progressAnim); anim.setInterpolator(interpolator); mPendingAnimation = new PendingAnimation(duration); mPendingAnimation.add(anim); if (LIVE_TILE.get()) { mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator); mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); } mPendingAnimation.addEndListener(isSuccess -> { if (isSuccess) { if (LIVE_TILE.get() && tv.isRunningTask()) { finishRecentsAnimation(false /* toRecents */, null); onTaskLaunchAnimationEnd(true /* success */); } else { tv.launchTask(this::onTaskLaunchAnimationEnd); } Task task = tv.getTask(); if (task != null) { mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo()) .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN); } } else { onTaskLaunchAnimationEnd(false); } mPendingAnimation = null; }); return mPendingAnimation; } protected void onTaskLaunchAnimationEnd(boolean success) { if (success) { resetTaskVisuals(); } } @Override protected void notifyPageSwitchListener(int prevPage) { super.notifyPageSwitchListener(prevPage); loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); updateEnabledOverlays(); } @Override protected String getCurrentPageDescription() { return ""; } @Override public void addChildrenForAccessibility(ArrayList outChildren) { // Add children in reverse order for (int i = getChildCount() - 1; i >= 0; --i) { outChildren.add(getChildAt(i)); } } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); final AccessibilityNodeInfo.CollectionInfo collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( 1, getTaskViewCount(), false, AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); info.setCollectionInfo(collectionInfo); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); final int taskViewCount = getTaskViewCount(); event.setScrollable(taskViewCount > 0); if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { final int[] visibleTasks = getVisibleChildrenRange(); event.setFromIndex(taskViewCount - visibleTasks[1]); event.setToIndex(taskViewCount - visibleTasks[0]); event.setItemCount(taskViewCount); } } @Override public CharSequence getAccessibilityClassName() { // To hear position-in-list related feedback from Talkback. return ListView.class.getName(); } @Override protected boolean isPageOrderFlipped() { return true; } public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) { mEnableDrawingLiveTile = enableDrawingLiveTile; } public void redrawLiveTile() { if (mLiveTileParams.getTargetSet() != null) { mLiveTileTaskViewSimulator.apply(mLiveTileParams); } } public TaskViewSimulator getLiveTileTaskViewSimulator() { return mLiveTileTaskViewSimulator; } public TransformParams getLiveTileParams() { return mLiveTileParams; } // TODO: To be removed in a follow up CL public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController, RecentsAnimationTargets recentsAnimationTargets) { mRecentsAnimationController = recentsAnimationController; if (recentsAnimationTargets != null && recentsAnimationTargets.apps.length > 0) { if (mSyncTransactionApplier != null) { recentsAnimationTargets.addReleaseCheck(mSyncTransactionApplier); } mLiveTileTaskViewSimulator.setPreview( recentsAnimationTargets.apps[recentsAnimationTargets.apps.length - 1]); mLiveTileParams.setTargetSet(recentsAnimationTargets); } } public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) { finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete); } public void finishRecentsAnimation(boolean toRecents, boolean shouldPip, Runnable onFinishComplete) { if (!toRecents && LIVE_TILE.get()) { // Reset the minimized state since we force-toggled the minimized state when entering // overview, but never actually finished the recents animation. This is a catch all for // cases where we haven't already reset it. SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate(); if (p != null) { p.setSplitScreenMinimized(false); } } if (mRecentsAnimationController == null) { if (onFinishComplete != null) { onFinishComplete.run(); } return; } final boolean sendUserLeaveHint = toRecents && shouldPip; if (sendUserLeaveHint) { // Notify the SysUI to use fade-in animation when entering PiP from live tile. final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext()); systemUiProxy.notifySwipeToHomeFinished(); systemUiProxy.setShelfHeight(true, mActivity.getDeviceProfile().hotseatBarSizePx); } mRecentsAnimationController.finish(toRecents, () -> { if (onFinishComplete != null) { onFinishComplete.run(); } onRecentsAnimationComplete(); }, sendUserLeaveHint); } /** * Called when a running recents animation has finished or canceled. */ public void onRecentsAnimationComplete() { // At this point, the recents animation is not running and if the animation was canceled // by a display rotation then reset this state to show the screenshot setRunningTaskViewShowScreenshot(true); // After we finish the recents animation, the current task id should be correctly // reset so that when the task is launched from Overview later, it goes through the // flow of starting a new task instead of finishing recents animation to app. A // typical example of this is (1) user swipes up from app to Overview (2) user // taps on QSB (3) user goes back to Overview and launch the most recent task. setCurrentTask(-1); mRecentsAnimationController = null; } public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) { if (mDisallowScrollToClearAll != disallowScrollToClearAll) { mDisallowScrollToClearAll = disallowScrollToClearAll; updateMinAndMaxScrollX(); } } /** * Updates page scroll synchronously after measure and layout child views. */ public void updateScrollSynchronously() { // onMeasure is needed to update child's measured width which is used in scroll calculation, // in case TaskView sizes has changed when being focused/unfocused. onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY), makeMeasureSpec(getMeasuredHeight(), EXACTLY)); onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom()); updateMinAndMaxScrollX(); } @Override protected int computeMinScroll() { if (getTaskViewCount() > 0) { if (mIsRtl) { // If we aren't showing the clear all button, use the rightmost task as the min // scroll. return getScrollForPage(mDisallowScrollToClearAll ? indexOfChild( getTaskViewAt(getTaskViewCount() - 1)) : indexOfChild(mClearAllButton)); } else { TaskView focusedTaskView = showAsGrid() ? getFocusedTaskView() : null; return getScrollForPage(focusedTaskView != null ? indexOfChild(focusedTaskView) : mTaskViewStartIndex); } } return super.computeMinScroll(); } @Override protected int computeMaxScroll() { if (getTaskViewCount() > 0) { if (mIsRtl) { TaskView focusedTaskView = showAsGrid() ? getFocusedTaskView() : null; return getScrollForPage(focusedTaskView != null ? indexOfChild(focusedTaskView) : mTaskViewStartIndex); } else { // If we aren't showing the clear all button, use the leftmost task as the min // scroll. return getScrollForPage(mDisallowScrollToClearAll ? indexOfChild( getTaskViewAt(getTaskViewCount() - 1)) : indexOfChild(mClearAllButton)); } } return super.computeMaxScroll(); } /** * Returns page scroll of ClearAllButton. */ public int getClearAllScroll() { return getScrollForPage(indexOfChild(mClearAllButton)); } @Override protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren, ComputePageScrollsLogic scrollLogic) { int[] newPageScrolls = new int[outPageScrolls.length]; super.getPageScrolls(newPageScrolls, layoutChildren, scrollLogic); boolean showAsFullscreen = showAsFullscreen(); boolean showAsGrid = showAsGrid(); // Align ClearAllButton to the left (RTL) or right (non-RTL), which is different from other // TaskViews. This must be called after laying out ClearAllButton. if (layoutChildren) { int clearAllWidthDiff = mOrientationHandler.getPrimaryValue(mTaskWidth, mTaskHeight) - mOrientationHandler.getPrimarySize(mClearAllButton); mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff); } boolean pageScrollChanged = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); float scrollDiff = 0; if (child instanceof TaskView) { scrollDiff = ((TaskView) child).getScrollAdjustment(showAsFullscreen, showAsGrid); } else if (child instanceof ClearAllButton) { scrollDiff = ((ClearAllButton) child).getScrollAdjustment(showAsFullscreen, showAsGrid); } final int pageScroll = newPageScrolls[i] + (int) scrollDiff; if (outPageScrolls[i] != pageScroll) { pageScrollChanged = true; outPageScrolls[i] = pageScroll; } } return pageScrollChanged; } @Override protected int getChildOffset(int index) { int childOffset = super.getChildOffset(index); View child = getChildAt(index); if (child instanceof TaskView) { childOffset += ((TaskView) child).getOffsetAdjustment(showAsFullscreen(), showAsGrid()); } else if (child instanceof ClearAllButton) { childOffset += ((ClearAllButton) child).getOffsetAdjustment(mOverviewFullscreenEnabled, showAsGrid()); } return childOffset; } @Override protected int getChildVisibleSize(int index) { final TaskView taskView = getTaskViewAtByAbsoluteIndex(index); if (taskView == null) { return super.getChildVisibleSize(index); } return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment( showAsFullscreen())); } public ClearAllButton getClearAllButton() { return mClearAllButton; } /** * @return How many pixels the running task is offset on the currently laid out dominant axis. */ public int getScrollOffset() { return getScrollOffset(getRunningTaskIndex()); } /** * Returns how many pixels the page is offset on the currently laid out dominant axis. */ public int getScrollOffset(int pageIndex) { if (pageIndex == -1) { return 0; } int overScrollShift = getOverScrollShift(); if (mAdjacentPageHorizontalOffset > 0) { // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so // that the page can move freely given there's no visual indication why it shouldn't. overScrollShift = (int) Utilities.mapRange(mAdjacentPageHorizontalOffset, overScrollShift, getUndampedOverScrollShift()); } return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this) + overScrollShift; } /** * Returns how many pixels the task is offset on the currently laid out secondary axis * according to {@link #mGridProgress}. */ public float getGridTranslationSecondary(int pageIndex) { TaskView taskView = getTaskViewAtByAbsoluteIndex(pageIndex); if (taskView == null) { return 0; } return mOrientationHandler.getSecondaryValue(taskView.getGridTranslationX(), taskView.getGridTranslationY()); } public Consumer getEventDispatcher(float navbarRotation) { float degreesRotated; if (navbarRotation == 0) { degreesRotated = mOrientationHandler.getDegreesRotated(); } else { degreesRotated = -navbarRotation; } if (degreesRotated == 0) { return super::onTouchEvent; } // At this point the event coordinates have already been transformed, so we need to // undo that transformation since PagedView also accommodates for the transformation via // PagedOrientationHandler return e -> { if (navbarRotation != 0 && mOrientationState.isMultipleOrientationSupportedByDevice() && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) { mOrientationState.flipVertical(e); super.onTouchEvent(e); mOrientationState.flipVertical(e); return; } mOrientationState.transformEvent(-degreesRotated, e, true); super.onTouchEvent(e); mOrientationState.transformEvent(-degreesRotated, e, false); }; } private void updateEnabledOverlays() { int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1; int taskCount = getTaskViewCount(); for (int i = mTaskViewStartIndex; i < mTaskViewStartIndex + taskCount; i++) { getTaskViewAtByAbsoluteIndex(i).setOverlayEnabled(i == overlayEnabledPage); } } public void setOverlayEnabled(boolean overlayEnabled) { if (mOverlayEnabled != overlayEnabled) { mOverlayEnabled = overlayEnabled; updateEnabledOverlays(); } } public void setOverviewGridEnabled(boolean overviewGridEnabled) { if (mOverviewGridEnabled != overviewGridEnabled) { mOverviewGridEnabled = overviewGridEnabled; updateActionsViewScrollAlpha(); // Request layout to ensure scroll position is recalculated with updated mGridProgress. requestLayout(); } } public void setOverviewFullscreenEnabled(boolean overviewFullscreenEnabled) { if (mOverviewFullscreenEnabled != overviewFullscreenEnabled) { mOverviewFullscreenEnabled = overviewFullscreenEnabled; // Request layout to ensure scroll position is recalculated with updated // mFullscreenProgress. requestLayout(); } } /** * Switch the current running task view to static snapshot mode, * capturing the snapshot at the same time. */ public void switchToScreenshot(Runnable onFinishRunnable) { if (mRecentsAnimationController == null) { if (onFinishRunnable != null) { onFinishRunnable.run(); } return; } switchToScreenshot(mRunningTaskId == -1 ? null : mRecentsAnimationController.screenshotTask(mRunningTaskId), onFinishRunnable); } /** * Switch the current running task view to static snapshot mode, using the * provided thumbnail data as the snapshot. */ public void switchToScreenshot(ThumbnailData thumbnailData, Runnable onFinishRunnable) { TaskView taskView = getRunningTaskView(); if (taskView != null) { taskView.setShowScreenshot(true); if (thumbnailData != null) { taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData); } else { taskView.getThumbnail().refresh(); } ViewUtils.postFrameDrawn(taskView, onFinishRunnable); } else { onFinishRunnable.run(); } } /** * The current task is fully modal (modalness = 1) when it is shown on its own in a modal * way. Modalness 0 means the task is shown in context with all the other tasks. */ private void setTaskModalness(float modalness) { mTaskModalness = modalness; updatePageOffsets(); if (getCurrentPageTaskView() != null) { getCurrentPageTaskView().setModalness(modalness); } // Only show actions view when it's modal for in-place landscape mode. boolean inPlaceLandscape = !mOrientationState.canRecentsActivityRotate() && mOrientationState.getTouchRotation() != ROTATION_0; mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape); mActionsView.setTaskModalness(modalness); } @Nullable protected DepthController getDepthController() { return null; } @Override public void onSecondaryWindowBoundsChanged() { // Invalidate the task view size setInsets(mInsets); } /** * Enables or disables modal state for RecentsView * @param isModalState */ public void setModalStateEnabled(boolean isModalState) { } public TaskOverlayFactory getTaskOverlayFactory() { return mTaskOverlayFactory; } public BaseActivityInterface getSizeStrategy() { return mSizeStrategy; } /** * Set all the task views to color tint scrim mode, dimming or tinting them all. Allows the * tasks to be dimmed while other elements in the recents view are left alone. */ public void showForegroundScrim(boolean show) { if (!show && mColorTint == 0) { if (mTintingAnimator != null) { mTintingAnimator.cancel(); mTintingAnimator = null; } return; } mTintingAnimator = ObjectAnimator.ofFloat(this, COLOR_TINT, show ? 0.5f : 0f); mTintingAnimator.setAutoCancel(true); mTintingAnimator.start(); } /** Tint the RecentsView and TaskViews in to simulate a scrim. */ // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView private void setColorTint(float tintAmount) { mColorTint = tintAmount; for (int i = 0; i < getTaskViewCount(); i++) { getTaskViewAt(i).setColorTint(mColorTint, mTintingColor); } Drawable scrimBg = mActivity.getScrimView().getBackground(); if (scrimBg != null) { if (tintAmount == 0f) { scrimBg.setTintList(null); } else { scrimBg.setTintBlendMode(BlendMode.SRC_OVER); scrimBg.setTint( ColorUtils.setAlphaComponent(mTintingColor, (int) (255 * tintAmount))); } } } private float getColorTint() { return mColorTint; } /** Returns {@code true} if the overview tasks are displayed as a grid. */ public boolean showAsGrid() { return mOverviewGridEnabled || (mCurrentGestureEndTarget != null && mSizeStrategy.stateFromGestureEndTarget( mCurrentGestureEndTarget).displayOverviewTasksAsGrid(mActivity.getDeviceProfile())); } private boolean showAsFullscreen() { return mOverviewFullscreenEnabled && mCurrentGestureEndTarget != GestureState.GestureEndTarget.RECENTS; } /** * Used to register callbacks for when our empty message state changes. * * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener) * @see #updateEmptyMessage() */ public interface OnEmptyMessageUpdatedListener { /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */ void onEmptyMessageUpdated(boolean isEmpty); } /** * Adds a listener for scroll changes */ public void addOnScrollChangedListener(OnScrollChangedListener listener) { mScrollListeners.add(listener); } /** * Removes a previously added scroll change listener */ public void removeOnScrollChangedListener(OnScrollChangedListener listener) { mScrollListeners.remove(listener); } /** * @return Corner radius in pixel value for PiP window, which is updated via * {@link #mIPipAnimationListener} */ public int getPipCornerRadius() { return mPipCornerRadius; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); dispatchScrollChanged(); } private void dispatchScrollChanged() { mLiveTileTaskViewSimulator.setScroll(getScrollOffset()); for (int i = mScrollListeners.size() - 1; i >= 0; i--) { mScrollListeners.get(i).onScrollChanged(); } } private static class PinnedStackAnimationListener extends IPipAnimationListener.Stub { private T mActivity; private RecentsView mRecentsView; public void setActivityAndRecentsView(T activity, RecentsView recentsView) { mActivity = activity; mRecentsView = recentsView; } @Override public void onPipAnimationStarted() { MAIN_EXECUTOR.execute(() -> { // Needed for activities that auto-enter PiP, which will not trigger a remote // animation to be created if (mActivity != null) { mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); } }); } @Override public void onPipCornerRadiusChanged(int cornerRadius) { if (mRecentsView != null) { mRecentsView.mPipCornerRadius = cornerRadius; } } } /** Get the color used for foreground scrimming the RecentsView for sharing. */ public static int getForegroundScrimDimColor(Context context) { int baseColor = Themes.getAttrColor(context, R.attr.overviewScrimColor); // The Black blending is temporary until we have the proper color token. return ColorUtils.blendARGB(Color.BLACK, baseColor, 0.25f); } /** Get the RecentsAnimationController */ @Nullable public RecentsAnimationController getRecentsAnimationController() { return mRecentsAnimationController; } /** Update the current activity locus id to show the enabled state of Overview */ public void updateLocusId() { String locusId = "Overview"; if (mOverviewStateEnabled && mActivity.isStarted()) { locusId += "|ENABLED"; } else { locusId += "|DISABLED"; } final LocusId id = new LocusId(locusId); // Set locus context is a binder call, don't want it to happen during a transition UI_HELPER_EXECUTOR.post(() -> mActivity.setLocusContext(id, Bundle.EMPTY)); } }