blob: 070714ac25c81e536c1cee740245022977bede29 [file] [log] [blame]
/*
* 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_5;
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.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
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.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.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.RunnableList;
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<ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>,
STATE_TYPE extends BaseState<STATE_TYPE>> 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<RecentsView> CONTENT_ALPHA =
new FloatProperty<RecentsView>("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<RecentsView> FULLSCREEN_PROGRESS =
new FloatProperty<RecentsView>("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<RecentsView> TASK_MODALNESS =
new FloatProperty<RecentsView>("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<RecentsView> ADJACENT_PAGE_HORIZONTAL_OFFSET =
new FloatProperty<RecentsView>("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<RecentsView> COLOR_TINT =
new FloatProperty<RecentsView>("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<RecentsView> TASK_SECONDARY_TRANSLATION =
new FloatProperty<RecentsView>("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<RecentsView> TASK_PRIMARY_SPLIT_TRANSLATION =
new FloatProperty<RecentsView>("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<RecentsView> TASK_SECONDARY_SPLIT_TRANSLATION =
new FloatProperty<RecentsView>("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<RecentsView> RECENTS_SCALE_PROPERTY =
new FloatProperty<RecentsView>("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<RecentsView> RECENTS_GRID_PROGRESS =
new FloatProperty<RecentsView>("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;
protected final RecentsOrientedState mOrientationState;
protected final BaseActivityInterface<STATE_TYPE, ACTIVITY_TYPE> mSizeStrategy;
protected RecentsAnimationController mRecentsAnimationController;
protected SurfaceTransactionApplier mSyncTransactionApplier;
protected int mTaskWidth;
protected int mTaskHeight;
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 float[] mTempFloat = new float[1];
private final List<OnScrollChangedListener> 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<TaskView> 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 float mFocusedTaskRatio;
private boolean mRunningTaskIconScaledDown = false;
private boolean mRunningTaskShowScreenshot = 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();
}
}
};
private RunnableList mSideTaskLaunchCallback;
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(),
true /* isForLiveTile */);
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 (ENABLE_QUICKSTEP_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);
executeSideTaskLaunchCallback();
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.getTask().key.id);
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 addSideTaskLaunchCallback(RunnableList callback) {
if (mSideTaskLaunchCallback == null) {
mSideTaskLaunchCallback = new RunnableList();
}
mSideTaskLaunchCallback.add(callback::executeAllAndDestroy);
}
private void executeSideTaskLaunchCallback() {
if (mSideTaskLaunchCallback != null) {
mSideTaskLaunchCallback.executeAllAndDestroy();
mSideTaskLaunchCallback = null;
}
}
public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
if (mRunningTaskId != -1 && mRunningTaskId == taskId) {
RemoteAnimationTargets targets = getLiveTileParams().getTargetSet();
if (targets != null && targets.findTask(taskId) != null) {
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.hasTaskId(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
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<Task> tasks) {
if (mPendingAnimation != null) {
mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks));
return;
}
if (tasks == null || tasks.isEmpty()) {
removeTasksViewsAndClearAllButton();
onTaskStackUpdated();
return;
}
int currentTaskId = -1;
TaskView currentTaskView = getTaskViewAtByAbsoluteIndex(mCurrentPage);
if (currentTaskView != null) {
currentTaskId = currentTaskView.getTask().key.id;
}
// 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);
}
updateTaskSize();
int targetPage = -1;
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) {
targetPage = indexOfChild(runningTaskView);
} else if (getTaskViewCount() > 0) {
targetPage = indexOfChild(getTaskViewAt(0));
}
} else if (currentTaskId != -1) {
currentTaskView = getTaskView(currentTaskId);
if (currentTaskView != null) {
targetPage = indexOfChild(currentTaskView);
}
}
if (targetPage != -1 && mCurrentPage != targetPage) {
setCurrentPage(targetPage);
}
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.getTask().key.id) {
taskView.resetViewTransforms();
taskView.setStableAlpha(mContentAlpha);
taskView.setFullscreenProgress(mFullscreenProgress);
taskView.setModalness(mTaskModalness);
}
}
if (ENABLE_QUICKSTEP_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;
// Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
// null.
if (!mRunningTaskShowScreenshot) {
setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
}
}
if (mRunningTaskTileHidden) {
setRunningTaskHidden(mRunningTaskTileHidden);
}
// Force apply the scale.
if (mIgnoreResetTaskId != mRunningTaskId) {
applyRunningTaskIconScale();
}
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() {
updateOrientationHandler(true);
}
private void updateOrientationHandler(boolean forceRecreateDragLayerControllers) {
// Handle orientation changes.
PagedOrientationHandler oldOrientationHandler = mOrientationHandler;
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());
if (forceRecreateDragLayerControllers
|| !mOrientationHandler.equals(oldOrientationHandler)) {
// Changed orientations, update controllers so they intercept accordingly.
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);
// 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() {
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.getPrimaryFullscreenTranslationProperty().set(taskView,
accumulatedTranslationX);
taskView.getSecondaryFullscreenTranslationProperty().set(taskView, 0f);
// Compensate space caused by TaskView scaling.
float widthDiff =
taskView.getLayoutParams().width * (1 - taskView.getFullscreenScale());
accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff;
}
mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
updateGridProperties();
}
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);
int taskWidth = mTempRect.width();
int taskHeight = mTempRect.height();
if (mFocusedTaskId != -1) {
int boxLength = Math.max(taskWidth, taskHeight);
if (mFocusedTaskRatio > 1) {
taskWidth = boxLength;
taskHeight = (int) (boxLength / mFocusedTaskRatio);
} else {
taskWidth = (int) (boxLength * mFocusedTaskRatio);
taskHeight = boxLength;
}
}
return new Point(taskWidth, taskHeight);
}
/** 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.
TaskView focusedTaskView = getFocusedTaskView();
if (focusedTaskView != null) {
float scrollDiff = Math.abs(getScrollForPage(indexOfChild(focusedTaskView))
- mOrientationHandler.getPrimaryScroll(this));
float delta = (mGridSideMargin - scrollDiff) / (float) mGridSideMargin;
mActionsView.getScrollAlpha().setValue(Utilities.boundToRange(delta, 0, 1));
}
}
// Update the high res thumbnail loader state
mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
return scrolling;
}
/**
* 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 = -1;
if (mRecentsAnimationController != null) {
if (ENABLE_QUICKSTEP_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(/* forceRecreateDragLayerControllers = */ false);
}
});
}
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);
}
/**
* Returns the width to height ratio of the focused {@link TaskView}.
*/
public float getFocusedTaskRatio() {
return mFocusedTaskRatio;
}
/**
* 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);
setRunningTaskIconScaledDown(true);
}
/**
* Called only when a swipe-up gesture from an app has completed. Only called after
* {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
*/
public void onSwipeUpAnimationSuccess() {
if (getRunningTaskView() != null) {
animateUpRunningTaskIconScale();
}
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();
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++) {
View taskView = getTaskViewAt(i);
if (runningIndex == i && taskView.getAlpha() != 0) {
continue;
}
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();
}
}
/**
* Called when a gesture from an app has finished, and the animation to the target has ended.
*/
public void onGestureAnimationEnd() {
mGestureActive = false;
if (mOrientationState.setGestureActive(false)) {
updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
}
setEnableFreeScroll(true);
setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
setRunningTaskViewShowScreenshot(true);
}
setRunningTaskHidden(false);
animateUpRunningTaskIconScale();
if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS
&& (!showAsGrid() || getFocusedTaskView() != null)) {
animateActionsViewIn();
}
mCurrentGestureEndTarget = null;
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mActivity.getDeviceProfile().isMultiWindowMode) {
switchToScreenshot(
() -> finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
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);
if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
setFocusedTask(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
setRunningTaskIconScaledDown(false);
setRunningTaskViewShowScreenshot(true);
setRunningTaskHidden(false);
}
mRunningTaskId = runningTaskId;
}
/**
* Sets the focused task id and store the width to height ratio of the focused task.
*/
protected void setFocusedTask(int focusedTaskId) {
mFocusedTaskId = focusedTaskId;
mFocusedTaskRatio =
mLastComputedTaskSize.width() / (float) mLastComputedTaskSize.height();
}
/**
* 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 (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
mRunningTaskShowScreenshot = showScreenshot;
TaskView runningTaskView = getRunningTaskView();
if (runningTaskView != null) {
runningTaskView.setShowScreenshot(mRunningTaskShowScreenshot);
}
}
}
public void setRunningTaskIconScaledDown(boolean isScaledDown) {
if (mRunningTaskIconScaledDown != isScaledDown) {
mRunningTaskIconScaledDown = isScaledDown;
applyRunningTaskIconScale();
}
}
public boolean isTaskIconScaledDown(TaskView taskView) {
return mRunningTaskIconScaledDown && getRunningTaskView() == taskView;
}
private void applyRunningTaskIconScale() {
TaskView firstTask = getRunningTaskView();
if (firstTask != null) {
firstTask.setIconScaleAndDim(mRunningTaskIconScaledDown ? 0 : 1);
}
}
private void animateActionsViewIn() {
ObjectAnimator anim = ObjectAnimator.ofFloat(
mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 0, 1);
anim.setDuration(TaskView.SCALE_ICON_DURATION);
anim.start();
}
private void animateActionsViewOut() {
ObjectAnimator anim = ObjectAnimator.ofFloat(
mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 1, 0);
anim.setDuration(TaskView.SCALE_ICON_DURATION);
anim.start();
}
public void animateUpRunningTaskIconScale() {
mRunningTaskIconScaledDown = false;
TaskView firstTask = getRunningTaskView();
if (firstTask != null) {
firstTask.setIconScaleAnimStartProgress(0f);
firstTask.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;
}
final int boxLength = Math.max(mLastComputedGridTaskSize.width(),
mLastComputedGridTaskSize.height());
int taskTopMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
/*
* taskGridVerticalDiff is used to position the top of a task in the top row of the grid
* heightOffset is the vertical space one grid task takes + space between top and
* bottom row
* Summed together they provide the top position for bottom row of grid tasks
*/
final float taskGridVerticalDiff =
mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
final float heightOffset = (boxLength + taskTopMargin) + mRowSpacing;
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);
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.getTask().key.id;
boolean isTopRow = isTaskDismissal ? mTopRowIdSet.contains(taskId)
: topRowWidth <= bottomRowWidth;
if (isTopRow) {
topRowWidth += taskWidthAndSpacing;
topSet.add(i);
mTopRowIdSet.add(taskId);
taskView.setGridTranslationY(taskGridVerticalDiff);
// 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(heightOffset + taskGridVerticalDiff);
// 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 snappedTaskFullscreenScrollAdjustment = 0;
float snappedTaskGridTranslationX = 0;
if (snappedTaskView != null) {
snappedTaskFullscreenScrollAdjustment = 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);
taskView.getPrimaryNonFullscreenTranslationProperty().set(taskView,
snappedTaskFullscreenScrollAdjustment);
taskView.getSecondaryNonFullscreenTranslationProperty().set(taskView, 0f);
}
// 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 + snappedTaskFullscreenScrollAdjustment;
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;
int minimumDistance =
mLastComputedGridSize.width() - snappedTaskView.getLayoutParams().width;
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.getTask().key.id;
int taskId2 = taskView2.getTask().key.id;
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<TaskView> 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 (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
&& taskView.isRunningTask()) {
anim.addOnFrameCallback(() -> {
mLiveTileTaskViewSimulator.taskSecondaryTranslation.value =
mOrientationHandler.getSecondaryValue(
taskView.getTranslationX(),
taskView.getTranslationY());
redrawLiveTile();
});
}
}
public PendingAnimation createTaskDismissAnimation(TaskView taskView, 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;
}
int[] oldScroll = new int[count];
int[] newScroll = new int[count];
getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
int taskCount = getTaskViewCount();
int scrollDiffPerPage = 0;
if (count > 1) {
scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
}
int draggedIndex = indexOfChild(taskView);
boolean isFocusedTaskDismissed = taskView.getTask().key.id == mFocusedTaskId;
if (isFocusedTaskDismissed && showAsGrid()) {
anim.setFloat(mActionsView, VIEW_ALPHA, 0, clampToProgress(ACCEL_0_5, 0, 0.5f));
}
float dismissedTaskWidth = taskView.getLayoutParams().width + mPageSpacing;
boolean needsCurveUpdates = false;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child == taskView) {
if (animateTaskView) {
addDismissedTaskAnimations(taskView, 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 == draggedIndex) {
int lastPage = taskCount - 1;
if (mCurrentPage == lastPage) {
offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
}
} else {
// Dragging an adjacent page.
int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
if (draggedIndex == 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 - draggedIndex);
anim.setFloat(child, translationProperty, scrollDiff, clampToProgress(LINEAR,
Utilities.boundToRange(INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ additionalDismissDuration, 0f, 1f), 1));
if (ENABLE_QUICKSTEP_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) {
// Animate task with index >= dismissed index and in the same row as the
// dismissed index, or if the dismissed task was the focused task. Offset
// successive task dismissal durations for a staggered effect.
if (isFocusedTaskDismissed || (i >= draggedIndex && isSameGridRow((TaskView) child,
taskView))) {
FloatProperty translationProperty =
((TaskView) child).getPrimaryDismissTranslationProperty();
float additionalDismissDuration =
ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
i - draggedIndex);
anim.setFloat(child, translationProperty,
!mIsRtl ? -dismissedTaskWidth : dismissedTaskWidth,
clampToProgress(LINEAR, Utilities.boundToRange(
INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ additionalDismissDuration, 0f, 1f), 1));
}
}
}
if (needsCurveUpdates) {
anim.addOnFrameCallback(this::updateCurveProperties);
}
// Add a tiny bit of translation Z, so that it draws on top of other views
if (animateTaskView) {
taskView.setTranslationZ(0.1f);
}
mPendingAnimation = anim;
mPendingAnimation.addEndListener(new Consumer<Boolean>() {
@Override
public void accept(Boolean success) {
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
&& taskView.isRunningTask() && success) {
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
() -> onEnd(success));
} else {
onEnd(success);
}
}
@SuppressWarnings("WrongCall")
private void onEnd(boolean success) {
if (success) {
if (shouldRemoveTask) {
if (taskView.getTask() != null) {
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask()) {
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
() -> removeTaskInternal(taskView));
} else {
removeTaskInternal(taskView);
}
mActivity.getStatsLogManager().logger()
.withItemInfo(taskView.getItemInfo())
.log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
}
}
// Reset task translations as they may have updated via animations in
// createTaskDismissAnimation
resetTaskVisuals();
int pageToSnapTo = mCurrentPage;
// Snap to start if focused task was dismissed, as after quick switch it could
// be at any page but the focused task always displays at the start.
if (taskView.getTask().key.id == mFocusedTaskId) {
pageToSnapTo = mTaskViewStartIndex;
} else if (draggedIndex < pageToSnapTo || pageToSnapTo == (getTaskViewCount()
- 1)) {
pageToSnapTo -= 1;
}
removeViewInLayout(taskView);
if (getTaskViewCount() == 0) {
removeViewInLayout(mClearAllButton);
startHome();
} else {
snapToPageImmediately(pageToSnapTo);
dispatchScrollChanged();
// Grid got messed up, reapply.
updateGridProperties(true);
if (showAsGrid() && getFocusedTaskView() == null
&& mActionsView.getVisibilityAlpha().getValue() == 1) {
animateActionsViewOut();
}
}
// Update the layout synchronously so that the position of next view is
// immediately available.
onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
}
onDismissAnimationEnds();
mPendingAnimation = null;
}
});
return anim;
}
private void removeTaskInternal(TaskView taskView) {
UI_HELPER_EXECUTOR.getHandler().postDelayed(() ->
ActivityManagerWrapper.getInstance().removeTask(
taskView.getTask().key.id),
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 && 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.getTask().key.id != 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) {
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 (ENABLE_QUICKSTEP_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;
}
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, and offset
// by current page's scroll diff.
int midpointScroll = getScrollForPage(midpointIndex)
+ mOrientationHandler.getPrimaryScroll(this) - getScrollForPage(mCurrentPage);
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<TaskView> 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 (ENABLE_QUICKSTEP_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 (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
}
mPendingAnimation.addEndListener(isSuccess -> {
if (isSuccess) {
if (ENABLE_QUICKSTEP_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<View> 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 && ENABLE_QUICKSTEP_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;
executeSideTaskLaunchCallback();
}
public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
mDisallowScrollToClearAll = disallowScrollToClearAll;
updateMinAndMaxScrollX();
}
}
/**
* Updates page scroll synchronously and layout child views.
*/
public void updateScrollSynchronously() {
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<MotionEvent> 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;
// 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;
}
public boolean shouldShowOverviewActionsForState(STATE_TYPE state) {
return !state.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())
|| getFocusedTaskView() != null;
}
/**
* 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<T extends BaseActivity> 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));
}
}