Files
Flexera-Launcher/quickstep/src/com/android/quickstep/views/RecentsView.java
T
Vinit Nayak 4ce773962e Take margin insets into account when setting position
Previously we computed insets for task menu position
whenever a Task was told that orientation had changed.
This missed a path for the TaskMenuView's onScrollChanged()
listener, which also calls set position and wasn't taking
the insets into account.

Moved the inset into setPosition() directly so it's caught
on all code paths.

Also noticed 2 bugs where
* We were calling updateChildTaskOrientation() twice, it
already happens in the call to updateOrientationHandler()
* We were directly modifying insets from the activity's
drag layer instead of copying those insets to another rect
and then modifying (this is no longer an issue since we
are not touching those insets at all anymore)

Bug: 192400086
Test: Rotated w/ task menu open for fake and real
landscape. Nothing seems broken.
Note real landscape hides the menu whereas fake one
shows it (which was behavior before this change)

Change-Id: I613dac9519220f49285655ef11a1f72e4a6d31bd
2021-06-30 06:23:38 +00:00

3885 lines
161 KiB
Java

/*
* 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.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
import static com.android.launcher3.statehandlers.DepthController.DEPTH;
import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.LayoutTransition;
import android.animation.LayoutTransition.TransitionListener;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.LocusId;
import android.content.res.Configuration;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnScrollChangedListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.OverScroller;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.SpringProperty;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.DynamicResource;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.ResourceBasedOverride.Overrides;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.TranslateEdgeEffect;
import com.android.launcher3.util.ViewPool;
import com.android.quickstep.AnimatedFloat;
import com.android.quickstep.BaseActivityInterface;
import com.android.quickstep.GestureState;
import com.android.quickstep.RecentsAnimationController;
import com.android.quickstep.RecentsAnimationTargets;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
import com.android.quickstep.RemoteAnimationTargets;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskOverlayFactory;
import com.android.quickstep.TaskThumbnailCache;
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.ViewUtils;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.util.SplitScreenBounds;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.util.SurfaceTransactionApplier;
import com.android.quickstep.util.TaskViewSimulator;
import com.android.quickstep.util.TransformParams;
import com.android.systemui.plugins.ResourceProvider;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.PackageManagerWrapper;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.wm.shell.pip.IPipAnimationListener;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* A list of recent tasks.
*/
@TargetApi(Build.VERSION_CODES.R)
public abstract class RecentsView<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 mOverviewStateEnabled;
private boolean mHandleTaskStackChanges;
private boolean mSwipeDownShouldLaunchApp;
private boolean mTouchDownToStartHome;
private final float mSquaredTouchSlop;
private int mDownX;
private int mDownY;
private PendingAnimation mPendingAnimation;
private LayoutTransition mLayoutTransition;
@ViewDebug.ExportedProperty(category = "launcher")
protected float mContentAlpha = 1;
@ViewDebug.ExportedProperty(category = "launcher")
protected float mFullscreenProgress = 0;
/**
* How modal is the current task to be displayed, 1 means the task is fully modal and no other
* tasks are show. 0 means the task is displays in context in the list with other tasks.
*/
@ViewDebug.ExportedProperty(category = "launcher")
protected float mTaskModalness = 0;
// Keeps track of task id whose visual state should not be reset
private int mIgnoreResetTaskId = -1;
// Variables for empty state
private final Drawable mEmptyIcon;
private final CharSequence mEmptyMessage;
private final TextPaint mEmptyMessagePaint;
private final Point mLastMeasureSize = new Point();
private final int mEmptyMessagePadding;
private boolean mShowEmptyMessage;
private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
private Layout mEmptyTextLayout;
/**
* Placeholder view indicating where the first split screen selected app will be placed
*/
private SplitPlaceholderView mSplitPlaceholderView;
/**
* The first task that split screen selection was initiated with. When split select state is
* initialized, we create a
* {@link #createTaskDismissAnimation(TaskView, boolean, boolean, long)} for this TaskView but
* don't actually remove the task since the user might back out. As such, we also ensure this
* View doesn't go back into the {@link #mTaskViewPool}, see {@link #onViewRemoved(View)}
*/
private TaskView mSplitHiddenTaskView;
/**
* Keeps track of the index of the TaskView that split screen was initialized with so we know
* where to insert it back into list of taskViews in case user backs out of entering split
* screen.
* NOTE: This index is the index while {@link #mSplitHiddenTaskView} was a child of recentsView,
* this doesn't get adjusted to reflect the new child count after the taskView is dismissed/
* removed from recentsView
*/
private int mSplitHiddenTaskViewIndex;
// Keeps track of the index where the first TaskView should be
private int mTaskViewStartIndex = 0;
private OverviewActionsView mActionsView;
private MultiWindowModeChangedListener mMultiWindowModeChangedListener =
new MultiWindowModeChangedListener() {
@Override
public void onMultiWindowModeChanged(boolean inMultiWindowMode) {
mOrientationState.setMultiWindowMode(inMultiWindowMode);
setLayoutRotation(mOrientationState.getTouchRotation(),
mOrientationState.getDisplayRotation());
updateChildTaskOrientations();
if (!inMultiWindowMode && mOverviewStateEnabled) {
// TODO: Re-enable layout transitions for addition of the unpinned task
reloadIfNeeded();
}
}
};
public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
BaseActivityInterface sizeStrategy) {
super(context, attrs, defStyleAttr);
setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
setEnableFreeScroll(true);
mSizeStrategy = sizeStrategy;
mActivity = BaseActivity.fromContext(context);
mOrientationState = new RecentsOrientedState(
context, mSizeStrategy, this::animateRecentsRotationInPlace);
final int rotation = mActivity.getDisplay().getRotation();
mOrientationState.setRecentsRotation(rotation);
mFastFlingVelocity = getResources()
.getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
mModel = RecentsModel.INSTANCE.get(context);
mIdp = InvariantDeviceProfile.INSTANCE.get(context);
mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
.inflate(R.layout.overview_clear_all_button, this, false);
mClearAllButton.setOnClickListener(this::dismissAllTasks);
mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
10 /* initial size */);
mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
mRowSpacing = getResources().getDimensionPixelSize(R.dimen.overview_grid_row_spacing);
mGridSideMargin = getResources().getDimensionPixelSize(R.dimen.overview_grid_side_margin);
mSquaredTouchSlop = squaredTouchSlop(context);
mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
mEmptyIcon.setCallback(this);
mEmptyMessage = context.getText(R.string.recents_empty_message);
mEmptyMessagePaint = new TextPaint();
mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
mEmptyMessagePaint.setTextSize(getResources()
.getDimension(R.dimen.recents_empty_message_text_size));
mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
Typeface.NORMAL));
mEmptyMessagePaint.setAntiAlias(true);
mEmptyMessagePadding = getResources()
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
setWillNotDraw(false);
updateEmptyMessage();
mOrientationHandler = mOrientationState.getOrientationHandler();
mTaskOverlayFactory = Overrides.getObject(
TaskOverlayFactory.class,
context.getApplicationContext(),
R.string.task_overlay_factory_class);
// Initialize quickstep specific cache params here, as this is constructed only once
mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy());
mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
mLiveTileTaskViewSimulator.setOrientationState(mOrientationState);
mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
mTintingColor = getForegroundScrimDimColor(context);
}
public OverScroller getScroller() {
return mScroller;
}
public boolean isRtl() {
return mIsRtl;
}
@Override
protected void initEdgeEffect() {
mEdgeGlowLeft = new TranslateEdgeEffect(getContext());
mEdgeGlowRight = new TranslateEdgeEffect(getContext());
}
@Override
protected void drawEdgeEffect(Canvas canvas) {
// Do not draw edge effect
}
@Override
protected void dispatchDraw(Canvas canvas) {
// Draw overscroll
if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) {
final int restoreCount = canvas.save();
int primarySize = mOrientationHandler.getPrimaryValue(getWidth(), getHeight());
int scroll = OverScroll.dampedScroll(getUndampedOverScrollShift(), primarySize);
mOrientationHandler.set(canvas, CANVAS_TRANSLATE, scroll);
if (mOverScrollShift != scroll) {
mOverScrollShift = scroll;
dispatchScrollChanged();
}
super.dispatchDraw(canvas);
canvas.restoreToCount(restoreCount);
} else {
if (mOverScrollShift != 0) {
mOverScrollShift = 0;
dispatchScrollChanged();
}
super.dispatchDraw(canvas);
}
if (LIVE_TILE.get() && mEnableDrawingLiveTile && mLiveTileParams.getTargetSet() != null) {
redrawLiveTile();
}
}
private float getUndampedOverScrollShift() {
final int width = getWidth();
final int height = getHeight();
int primarySize = mOrientationHandler.getPrimaryValue(width, height);
int secondarySize = mOrientationHandler.getSecondaryValue(width, height);
float effectiveShift = 0;
if (!mEdgeGlowLeft.isFinished()) {
mEdgeGlowLeft.setSize(secondarySize, primarySize);
if (((TranslateEdgeEffect) mEdgeGlowLeft).getTranslationShift(mTempFloat)) {
effectiveShift = mTempFloat[0];
postInvalidateOnAnimation();
}
}
if (!mEdgeGlowRight.isFinished()) {
mEdgeGlowRight.setSize(secondarySize, primarySize);
if (((TranslateEdgeEffect) mEdgeGlowRight).getTranslationShift(mTempFloat)) {
effectiveShift -= mTempFloat[0];
postInvalidateOnAnimation();
}
}
return effectiveShift * primarySize;
}
/**
* Returns the view shift due to overscroll
*/
public int getOverScrollShift() {
return mOverScrollShift;
}
@Override
public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
if (mHandleTaskStackChanges) {
TaskView taskView = getTaskView(taskId);
if (taskView != null) {
Task task = taskView.getTask();
taskView.getThumbnail().setThumbnail(task, thumbnailData);
return task;
}
}
return null;
}
@Override
public void onTaskIconChanged(String pkg, UserHandle user) {
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView tv = getTaskViewAt(i);
Task task = tv.getTask();
if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
&& task.key.userId == user.getIdentifier()) {
task.icon = null;
if (tv.getIconView().getDrawable() != null) {
tv.onTaskListVisibilityChanged(true /* visible */);
}
}
}
}
/**
* Update the thumbnail of the task.
* @param refreshNow Refresh immediately if it's true.
*/
public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) {
TaskView taskView = getTaskView(taskId);
if (taskView != null) {
taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow);
}
return taskView;
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
updateTaskStackListenerState();
}
public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
mActionsView = actionsView;
mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
mSplitPlaceholderView = splitPlaceholderView;
}
public SplitPlaceholderView getSplitPlaceholder() {
return mSplitPlaceholderView;
}
public boolean isSplitSelectionActive() {
return mSplitPlaceholderView.getSplitController().isSplitSelectActive();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
updateTaskStackListenerState();
mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
mSyncTransactionApplier = new SurfaceTransactionApplier(this);
mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier);
RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
mIPipAnimationListener.setActivityAndRecentsView(mActivity, this);
SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
mIPipAnimationListener);
mOrientationState.initListeners();
SplitScreenBounds.INSTANCE.addOnChangeListener(this);
mTaskOverlayFactory.initListeners();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
updateTaskStackListenerState();
mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
mSyncTransactionApplier = null;
mLiveTileParams.setSyncTransactionApplier(null);
RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
mIPipAnimationListener.setActivityAndRecentsView(null, null);
mOrientationState.destroyListeners();
mTaskOverlayFactory.removeListeners();
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
// Clear the task data for the removed child if it was visible unless it's the initial
// taskview for entering split screen, we only pretend to dismiss the task
if (child instanceof TaskView && child != mSplitHiddenTaskView) {
TaskView taskView = (TaskView) child;
mHasVisibleTaskData.delete(taskView.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 launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
if (mRunningTaskId != -1 && mRunningTaskId == taskId &&
getLiveTileParams().getTargetSet().findTask(taskId) != null) {
RemoteAnimationTargets targets = getLiveTileParams().getTargetSet();
launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers, targets.nonApps);
}
}
public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps,
RemoteAnimationTargetCompat[] wallpaper, RemoteAnimationTargetCompat[] nonApps) {
AnimatorSet anim = new AnimatorSet();
TaskView taskView = getTaskView(taskId);
if (taskView == null || !isTaskViewVisible(taskView)) {
// TODO: Refine this animation.
SurfaceTransactionApplier surfaceApplier =
new SurfaceTransactionApplier(mActivity.getDragLayer());
ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
appAnimator.setInterpolator(ACCEL_DEACCEL);
appAnimator.addUpdateListener(valueAnimator -> {
float percent = valueAnimator.getAnimatedFraction();
SurfaceParams.Builder builder = new SurfaceParams.Builder(
apps[apps.length - 1].leash);
Matrix matrix = new Matrix();
matrix.postScale(percent, percent);
matrix.postTranslate(mActivity.getDeviceProfile().widthPx * (1 - percent) / 2,
mActivity.getDeviceProfile().heightPx * (1 - percent) / 2);
builder.withAlpha(percent).withMatrix(matrix);
surfaceApplier.scheduleApply(builder.build());
});
anim.play(appAnimator);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishRecentsAnimation(false /* toRecents */, null);
}
});
} else {
TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps,
true /* launcherClosing */, mActivity.getStateManager(), this,
getDepthController());
}
anim.start();
}
private void updateTaskStartIndex(View affectingView) {
if (!(affectingView instanceof TaskView) && !(affectingView instanceof ClearAllButton)) {
int childCount = getChildCount();
mTaskViewStartIndex = 0;
while (mTaskViewStartIndex < childCount
&& !(getChildAt(mTaskViewStartIndex) instanceof TaskView)) {
mTaskViewStartIndex++;
}
}
}
public boolean isTaskViewVisible(TaskView tv) {
if (showAsGrid()) {
int screenStart = mOrientationHandler.getPrimaryScroll(this);
int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
return isTaskViewWithinBounds(tv, screenStart, screenEnd);
} else {
// For now, just check if it's the active task or an adjacent task
return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
}
}
private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
showAsFullscreen(), showAsGrid());
int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
showAsFullscreen()));
int taskEnd = taskStart + taskSize;
return (taskStart >= start && taskStart <= end) || (taskEnd >= start
&& taskEnd <= end);
}
public TaskView getTaskView(int taskId) {
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView taskView = getTaskViewAt(i);
if (taskView.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;
}
// 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();
if (mNextPage == INVALID_PAGE) {
// Set the current page to the running task, but not if settling on new task.
TaskView runningTaskView = getRunningTaskView();
if (runningTaskView != null) {
setCurrentPage(indexOfChild(runningTaskView));
} else if (getTaskViewCount() > 0) {
setCurrentPage(indexOfChild(getTaskViewAt(0)));
}
}
if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreResetTaskView) {
// If the taskView mapping is changing, do not preserve the visuals. Since we are
// mostly preserving the first task, and new taskViews are added to the end, it should
// generally map to the same task.
mIgnoreResetTaskId = -1;
}
resetTaskVisuals();
onTaskStackUpdated();
updateEnabledOverlays();
}
private boolean isModal() {
return mTaskModalness > 0;
}
public boolean isLoadingTasks() {
return mModel.isLoadingTasksInBackground();
}
private void removeTasksViewsAndClearAllButton() {
for (int i = getTaskViewCount() - 1; i >= 0; i--) {
removeView(getTaskViewAt(i));
}
if (indexOfChild(mClearAllButton) != -1) {
removeView(mClearAllButton);
}
}
public int getTaskViewCount() {
int taskViewCount = getChildCount() - mTaskViewStartIndex;
if (indexOfChild(mClearAllButton) != -1) {
taskViewCount--;
}
return taskViewCount;
}
protected void onTaskStackUpdated() {
// Lazily update the empty message only when the task stack is reapplied
updateEmptyMessage();
}
public void resetTaskVisuals() {
for (int i = getTaskViewCount() - 1; i >= 0; i--) {
TaskView taskView = getTaskViewAt(i);
if (mIgnoreResetTaskId != taskView.getTask().key.id) {
taskView.resetViewTransforms();
taskView.setStableAlpha(mContentAlpha);
taskView.setFullscreenProgress(mFullscreenProgress);
taskView.setModalness(mTaskModalness);
}
}
if (LIVE_TILE.get()) {
// Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need
// to reset the params after it settles in Overview from swipe up so that we don't
// render with obsolete param values.
mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = 0;
mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = 0;
mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
}
if (mRunningTaskTileHidden) {
setRunningTaskHidden(mRunningTaskTileHidden);
}
// 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() {
// Handle orientation changes.
mOrientationHandler = mOrientationState.getOrientationHandler();
mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
setLayoutDirection(mIsRtl
? View.LAYOUT_DIRECTION_RTL
: View.LAYOUT_DIRECTION_LTR);
mClearAllButton.setLayoutDirection(mIsRtl
? View.LAYOUT_DIRECTION_LTR
: View.LAYOUT_DIRECTION_RTL);
mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
mActivity.getDragLayer().recreateControllers();
boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0
|| mOrientationState.getRecentsActivityRotation() != ROTATION_0;
mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
!mOrientationState.canRecentsActivityRotate() && isInLandscape);
// Update TaskView's DeviceProfile dependent layout.
updateChildTaskOrientations();
// Recalculate DeviceProfile dependent layout.
updateSizeAndPadding();
requestLayout();
// Reapply the current page to update page scrolls.
setCurrentPage(mCurrentPage);
}
// Update task size and padding that are dependent on DeviceProfile and insets.
private void updateSizeAndPadding() {
DeviceProfile dp = mActivity.getDeviceProfile();
getTaskSize(mTempRect);
mTaskWidth = mTempRect.width();
mTaskHeight = mTempRect.height();
mTempRect.top -= dp.overviewTaskThumbnailTopMarginPx;
setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
dp.widthPx - mInsets.right - mTempRect.right,
dp.heightPx - mInsets.bottom - mTempRect.bottom);
mSizeStrategy.calculateGridSize(mActivity, mActivity.getDeviceProfile(),
mLastComputedGridSize);
mSizeStrategy.calculateGridTaskSize(mActivity, mActivity.getDeviceProfile(),
mLastComputedGridTaskSize, mOrientationHandler);
// 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 (LIVE_TILE.get() && mEnableDrawingLiveTile) {
// We are still drawing the live tile, finish it now to clean up.
finishRecentsAnimation(true /* toRecents */, null);
} else {
mRecentsAnimationController = null;
}
}
setEnableDrawingLiveTile(false);
mLiveTileParams.setTargetSet(null);
mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
// These are relatively expensive and don't need to be done this frame (RecentsView isn't
// visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
post(() -> {
unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
setCurrentPage(0);
LayoutUtils.setViewEnabled(mActionsView, true);
if (mOrientationState.setGestureActive(false)) {
updateOrientationHandler();
}
});
}
public int getRunningTaskId() {
return mRunningTaskId;
}
public @Nullable TaskView getRunningTaskView() {
return getTaskView(mRunningTaskId);
}
public int getRunningTaskIndex() {
return getTaskIndexForId(mRunningTaskId);
}
public @Nullable TaskView getFocusedTaskView() {
return getTaskView(mFocusedTaskId);
}
/**
* 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() {
Log.d("b/186444448", "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++) {
if (runningIndex == i) {
continue;
}
View taskView = getTaskViewAt(i);
as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1));
}
return as;
}
private void updateChildTaskOrientations() {
for (int i = 0; i < getTaskViewCount(); i++) {
getTaskViewAt(i).setOrientationState(mOrientationState);
}
TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU);
if (tv != null) {
tv.onRotationChanged();
}
}
/**
* Called when a gesture from an app has finished, and an end target has been determined.
*/
public void onPrepareGestureEndAnimation(
@Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget) {
if (mSizeStrategy.stateFromGestureEndTarget(endTarget)
.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) {
if (animatorSet == null) {
setGridProgress(1);
} else {
animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
}
}
mCurrentGestureEndTarget = endTarget;
if (endTarget == GestureState.GestureEndTarget.NEW_TASK
|| endTarget == GestureState.GestureEndTarget.LAST_TASK) {
// When switching to tasks in quick switch, ensures the snapped page's scroll maintain
// invariant between quick switch and overview, to ensure a smooth animation transition.
updateGridProperties();
} else if (endTarget == GestureState.GestureEndTarget.RECENTS) {
setEnableFreeScroll(true);
}
}
/**
* Called when a gesture from an app has finished, and the animation to the target has ended.
*/
public void onGestureAnimationEnd() {
Log.d("b/186444448", "onGestureEnd");
mGestureActive = false;
if (mOrientationState.setGestureActive(false)) {
updateOrientationHandler();
}
setEnableFreeScroll(true);
setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
if (!LIVE_TILE.get()) {
setRunningTaskViewShowScreenshot(true);
}
setRunningTaskHidden(false);
animateUpRunningTaskIconScale();
if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS
&& (!showAsGrid() || getFocusedTaskView() != null)) {
animateActionsViewIn();
}
mCurrentGestureEndTarget = null;
}
/**
* Returns true if we should add a stub taskView for the running task id
*/
protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
return runningTaskInfo != null && getTaskView(runningTaskInfo.taskId) == null;
}
/**
* Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
*
* All subsequent calls to reload will keep the task as the first item until {@link #reset()}
* is called. Also scrolls the view to this task.
*/
public void showCurrentTask(RunningTaskInfo runningTaskInfo) {
if (shouldAddStubTaskView(runningTaskInfo)) {
boolean wasEmpty = getChildCount() == 0;
// Add an empty view for now until the task plan is loaded and applied
final TaskView taskView = mTaskViewPool.getView();
addView(taskView, mTaskViewStartIndex);
if (wasEmpty) {
addView(mClearAllButton);
}
// The temporary running task is only used for the duration between the start of the
// gesture and the task list is loaded and applied
mTmpRunningTask = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false);
taskView.bind(mTmpRunningTask, mOrientationState);
// Measure and layout immediately so that the scroll values is updated instantly
// as the user might be quick-switching
measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
makeMeasureSpec(getMeasuredHeight(), EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
boolean runningTaskTileHidden = mRunningTaskTileHidden;
int runningTaskId = runningTaskInfo == null ? -1 : runningTaskInfo.taskId;
setCurrentTask(runningTaskId);
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 (LIVE_TILE.get()) {
TaskView runningTaskView = getRunningTaskView();
if (runningTaskView != null) {
runningTaskView.setShowScreenshot(showScreenshot);
}
}
}
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();
Log.d("b/186444448", "animateUpRunningTaskIconScale: firstTask="
+ (firstTask != null ? "t:" + firstTask.getTask() : null));
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 (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 (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 (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 (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 (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.
int midpointScroll = getScrollForPage(midpointIndex);
getPersistentChildPosition(midpointIndex, midpointScroll, taskPosition);
float midpointStart = mOrientationHandler.getStart(taskPosition);
getPersistentChildPosition(childIndex, midpointScroll, taskPosition);
// Assume child does not overlap with midPointChild.
isStartShift = mOrientationHandler.getStart(taskPosition) < midpointStart;
} else {
// Position the task at scroll position.
getPersistentChildPosition(childIndex, getScrollForPage(childIndex), taskPosition);
isStartShift = mIsRtl;
}
// Next, calculate the distance to move the task off screen. We also need to account for
// RecentsView scale, because it moves tasks based on its pivot. To do this, we move the
// task position to where it would be offscreen at scale = 1 (computed above), then we
// apply the scale via getMatrix() to determine how much that moves the task from its
// desired position, and adjust the computed distance accordingly.
float distanceToOffscreen;
if (isStartShift) {
float desiredStart = -mOrientationHandler.getPrimarySize(taskPosition);
distanceToOffscreen = -mOrientationHandler.getEnd(taskPosition);
if (mLastComputedTaskStartPushOutDistance == null) {
taskPosition.offsetTo(
mOrientationHandler.getPrimaryValue(desiredStart, 0f),
mOrientationHandler.getSecondaryValue(desiredStart, 0f));
getMatrix().mapRect(taskPosition);
mLastComputedTaskStartPushOutDistance = mOrientationHandler.getEnd(taskPosition)
/ mOrientationHandler.getPrimaryScale(this);
}
distanceToOffscreen -= mLastComputedTaskStartPushOutDistance;
} else {
float desiredStart = mOrientationHandler.getPrimarySize(this);
distanceToOffscreen = desiredStart - mOrientationHandler.getStart(taskPosition);
if (mLastComputedTaskEndPushOutDistance == null) {
taskPosition.offsetTo(
mOrientationHandler.getPrimaryValue(desiredStart, 0f),
mOrientationHandler.getSecondaryValue(desiredStart, 0f));
getMatrix().mapRect(taskPosition);
mLastComputedTaskEndPushOutDistance = (mOrientationHandler.getStart(taskPosition)
- desiredStart) / mOrientationHandler.getPrimaryScale(this);
}
distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
}
return distanceToOffscreen * offsetProgress;
}
protected void setTaskViewsResistanceTranslation(float translation) {
mTaskViewsSecondaryTranslation = translation;
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView task = getTaskViewAt(i);
task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
}
mLiveTileTaskViewSimulator.recentsViewSecondaryTranslation.value = translation;
}
protected void setTaskViewsPrimarySplitTranslation(float translation) {
mTaskViewsPrimarySplitTranslation = translation;
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView task = getTaskViewAt(i);
task.getPrimarySplitTranslationProperty().set(task, translation);
}
}
protected void setTaskViewsSecondarySplitTranslation(float translation) {
mTaskViewsSecondarySplitTranslation = translation;
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView task = getTaskViewAt(i);
task.getSecondarySplitTranslationProperty().set(task, translation);
}
}
/**
* Resets the visuals when exit modal state.
*/
public void resetModalVisuals() {
TaskView taskView = getCurrentPageTaskView();
if (taskView != null) {
taskView.getThumbnail().getTaskOverlay().resetModalVisuals();
}
}
public void initiateSplitSelect(TaskView taskView, SplitPositionOption splitPositionOption) {
mSplitHiddenTaskView = taskView;
SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController();
Rect initialBounds = new Rect(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
taskView.getBottom());
splitController.setInitialTaskSelect(taskView, splitPositionOption, initialBounds);
mSplitHiddenTaskViewIndex = indexOfChild(taskView);
mSplitPlaceholderView.setLayoutParams(
splitController.getLayoutParamsForActivePosition(getResources(),
mActivity.getDeviceProfile()));
mSplitPlaceholderView.setIcon(taskView.getIconView());
}
public PendingAnimation createSplitSelectInitAnimation() {
int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
return createTaskDismissAnimation(mSplitHiddenTaskView, true, false, duration);
}
public void confirmSplitSelect(TaskView taskView) {
mSplitPlaceholderView.getSplitController().setSecondTaskId(taskView);
resetTaskVisuals();
setTranslationY(0);
}
public PendingAnimation cancelSplitSelect(boolean animate) {
SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController();
SplitPositionOption splitOption = splitController.getActiveSplitPositionOption();
Rect initialBounds = splitController.getInitialBounds();
splitController.resetState();
int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
PendingAnimation pendingAnim = new PendingAnimation(duration);
if (!animate) {
resetFromSplitSelectionState();
return pendingAnim;
}
addViewInLayout(mSplitHiddenTaskView, mSplitHiddenTaskViewIndex,
mSplitHiddenTaskView.getLayoutParams());
mSplitHiddenTaskView.setAlpha(0);
int[] oldScroll = new int[getChildCount()];
getPageScrolls(oldScroll, false,
view -> view.getVisibility() != GONE && view != mSplitHiddenTaskView);
int[] newScroll = new int[getChildCount()];
getPageScrolls(newScroll, false, SIMPLE_SCROLL_LOGIC);
boolean needsCurveUpdates = false;
for (int i = mSplitHiddenTaskViewIndex; i >= 0; i--) {
View child = getChildAt(i);
if (child == mSplitHiddenTaskView) {
TaskView taskView = (TaskView) child;
int dir = mOrientationHandler.getSplitTaskViewDismissDirection(splitOption,
mActivity.getDeviceProfile());
FloatProperty<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 (LIVE_TILE.get() && runningTaskIndex != -1
&& runningTaskIndex != taskIndex) {
anim.play(ObjectAnimator.ofFloat(
recentsView.getLiveTileTaskViewSimulator().taskPrimaryTranslation,
AnimatedFloat.VALUE,
primaryTranslation));
}
int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
PropertyValuesHolder[] properties = new PropertyValuesHolder[3];
properties[0] = PropertyValuesHolder.ofFloat(
mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation);
properties[1] = PropertyValuesHolder.ofFloat(View.SCALE_X, 1);
properties[2] = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1);
anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex),
properties));
}
}
return anim;
}
/**
* Returns the scale up required on the view, so that it coves the screen completely
*/
public float getMaxScaleForFullScreen() {
getTaskSize(mTempRect);
return getPagedViewOrientedState().getFullScreenScaleAndPivot(
mTempRect, mActivity.getDeviceProfile(), mTempPointF);
}
public PendingAnimation createTaskLaunchAnimation(
TaskView tv, long duration, Interpolator interpolator) {
if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
throw new IllegalStateException("Another pending animation is still running");
}
int count = getTaskViewCount();
if (count == 0) {
return new PendingAnimation(duration);
}
// When swiping down from overview to tasks, ensures the snapped page's scroll maintain
// invariant between quick switch and overview, to ensure a smooth animation transition.
updateGridProperties();
updateScrollSynchronously();
int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
final boolean[] passedOverviewThreshold = new boolean[] {false};
ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
progressAnim.addUpdateListener(animator -> {
// Once we pass a certain threshold, update the sysui flags to match the target
// tasks' flags
if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) {
mActivity.getSystemUiController().updateUiState(
UI_STATE_FULLSCREEN_TASK, targetSysUiFlags);
} else {
mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
}
// Passing the threshold from taskview to fullscreen app will vibrate
final boolean passed = animator.getAnimatedFraction() >=
SUCCESS_TRANSITION_PROGRESS;
if (passed != passedOverviewThreshold[0]) {
passedOverviewThreshold[0] = passed;
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
}
});
AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
DepthController depthController = getDepthController();
if (depthController != null) {
ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController, DEPTH,
BACKGROUND_APP.getDepth(mActivity));
anim.play(depthAnimator);
}
anim.play(progressAnim);
anim.setInterpolator(interpolator);
mPendingAnimation = new PendingAnimation(duration);
mPendingAnimation.add(anim);
if (LIVE_TILE.get()) {
mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
}
mPendingAnimation.addEndListener(isSuccess -> {
if (isSuccess) {
if (LIVE_TILE.get() && tv.isRunningTask()) {
finishRecentsAnimation(false /* toRecents */, null);
onTaskLaunchAnimationEnd(true /* success */);
} else {
tv.launchTask(this::onTaskLaunchAnimationEnd);
}
Task task = tv.getTask();
if (task != null) {
mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo())
.log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
}
} else {
onTaskLaunchAnimationEnd(false);
}
mPendingAnimation = null;
});
return mPendingAnimation;
}
protected void onTaskLaunchAnimationEnd(boolean success) {
if (success) {
resetTaskVisuals();
}
}
@Override
protected void notifyPageSwitchListener(int prevPage) {
super.notifyPageSwitchListener(prevPage);
loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
updateEnabledOverlays();
}
@Override
protected String getCurrentPageDescription() {
return "";
}
@Override
public void addChildrenForAccessibility(ArrayList<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 && LIVE_TILE.get()) {
// Reset the minimized state since we force-toggled the minimized state when entering
// overview, but never actually finished the recents animation. This is a catch all for
// cases where we haven't already reset it.
SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate();
if (p != null) {
p.setSplitScreenMinimized(false);
}
}
if (mRecentsAnimationController == null) {
if (onFinishComplete != null) {
onFinishComplete.run();
}
return;
}
final boolean sendUserLeaveHint = toRecents && shouldPip;
if (sendUserLeaveHint) {
// Notify the SysUI to use fade-in animation when entering PiP from live tile.
final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext());
systemUiProxy.notifySwipeToHomeFinished();
systemUiProxy.setShelfHeight(true, mActivity.getDeviceProfile().hotseatBarSizePx);
}
mRecentsAnimationController.finish(toRecents, () -> {
if (onFinishComplete != null) {
onFinishComplete.run();
}
onRecentsAnimationComplete();
}, sendUserLeaveHint);
}
/**
* Called when a running recents animation has finished or canceled.
*/
public void onRecentsAnimationComplete() {
// At this point, the recents animation is not running and if the animation was canceled
// by a display rotation then reset this state to show the screenshot
setRunningTaskViewShowScreenshot(true);
// After we finish the recents animation, the current task id should be correctly
// reset so that when the task is launched from Overview later, it goes through the
// flow of starting a new task instead of finishing recents animation to app. A
// typical example of this is (1) user swipes up from app to Overview (2) user
// taps on QSB (3) user goes back to Overview and launch the most recent task.
setCurrentTask(-1);
mRecentsAnimationController = null;
}
public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
mDisallowScrollToClearAll = disallowScrollToClearAll;
updateMinAndMaxScrollX();
}
}
/**
* Updates page scroll synchronously 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));
}
}