7f28fd55ff
Bug: 80139193 Change-Id: I19ea05d3cc083628364651e9d9f581fe1aaaeaaa
1342 lines
51 KiB
Java
1342 lines
51 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 com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
|
|
import static com.android.launcher3.anim.Interpolators.ACCEL;
|
|
import static com.android.launcher3.anim.Interpolators.ACCEL_2;
|
|
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
|
|
import static com.android.launcher3.anim.Interpolators.LINEAR;
|
|
import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
|
|
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ObjectAnimator;
|
|
import android.animation.TimeInterpolator;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.TargetApi;
|
|
import android.app.ActivityManager;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Build;
|
|
import android.os.Handler;
|
|
import android.os.UserHandle;
|
|
import android.support.annotation.Nullable;
|
|
import android.text.Layout;
|
|
import android.text.StaticLayout;
|
|
import android.text.TextPaint;
|
|
import android.util.ArraySet;
|
|
import android.util.AttributeSet;
|
|
import android.util.SparseBooleanArray;
|
|
import android.view.KeyEvent;
|
|
import android.view.LayoutInflater;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewDebug;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.widget.ListView;
|
|
|
|
import com.android.launcher3.BaseActivity;
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.Insettable;
|
|
import com.android.launcher3.PagedView;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.anim.AnimatorPlaybackController;
|
|
import com.android.launcher3.anim.PropertyListBuilder;
|
|
import com.android.launcher3.config.FeatureFlags;
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
|
|
import com.android.launcher3.util.PendingAnimation;
|
|
import com.android.launcher3.util.Themes;
|
|
import com.android.quickstep.OverviewCallbacks;
|
|
import com.android.quickstep.QuickScrubController;
|
|
import com.android.quickstep.RecentsModel;
|
|
import com.android.quickstep.TaskUtils;
|
|
import com.android.quickstep.util.ClipAnimationHelper;
|
|
import com.android.quickstep.util.TaskViewDrawable;
|
|
import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
|
|
import com.android.systemui.shared.recents.model.RecentsTaskLoader;
|
|
import com.android.systemui.shared.recents.model.Task;
|
|
import com.android.systemui.shared.recents.model.TaskStack;
|
|
import com.android.systemui.shared.recents.model.ThumbnailData;
|
|
import com.android.systemui.shared.system.ActivityManagerWrapper;
|
|
import com.android.systemui.shared.system.BackgroundExecutor;
|
|
import com.android.systemui.shared.system.PackageManagerWrapper;
|
|
import com.android.systemui.shared.system.TaskStackChangeListener;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* A list of recent tasks.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.P)
|
|
public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable {
|
|
|
|
private static final String TAG = RecentsView.class.getSimpleName();
|
|
|
|
private final Rect mTempRect = new Rect();
|
|
|
|
private static final int DISMISS_TASK_DURATION = 300;
|
|
// The threshold at which we update the SystemUI flags when animating from the task into the app
|
|
private static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.6f;
|
|
|
|
private static final float[] sTempFloatArray = new float[3];
|
|
|
|
protected final T mActivity;
|
|
private final QuickScrubController mQuickScrubController;
|
|
private final float mFastFlingVelocity;
|
|
private final RecentsModel mModel;
|
|
private final int mTaskTopMargin;
|
|
|
|
private final ScrollState mScrollState = new ScrollState();
|
|
// Keeps track of the previously known visible tasks for purposes of loading/unloading task data
|
|
private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
|
|
|
|
private boolean mIsClearAllButtonFullyRevealed;
|
|
|
|
/**
|
|
* TODO: Call reloadIdNeeded in onTaskStackChanged.
|
|
*/
|
|
private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
|
|
@Override
|
|
public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
|
|
if (!mHandleTaskStackChanges) {
|
|
return;
|
|
}
|
|
updateThumbnail(taskId, snapshot);
|
|
}
|
|
|
|
@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;
|
|
}
|
|
// TODO: Re-enable layout transitions for addition of the unpinned task
|
|
reloadIfNeeded();
|
|
}
|
|
|
|
@Override
|
|
public void onTaskRemoved(int taskId) {
|
|
if (!mHandleTaskStackChanges) {
|
|
return;
|
|
}
|
|
BackgroundExecutor.get().submit(() -> {
|
|
TaskView taskView = getTaskView(taskId);
|
|
if (taskView == null) {
|
|
return;
|
|
}
|
|
Handler handler = taskView.getHandler();
|
|
if (handler == null) {
|
|
return;
|
|
}
|
|
|
|
// TODO: Add callbacks from AM reflecting adding/removing from the recents list, and
|
|
// remove all these checks
|
|
Task.TaskKey taskKey = taskView.getTask().key;
|
|
if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(),
|
|
taskKey.userId) == null) {
|
|
// The package was uninstalled
|
|
handler.post(() ->
|
|
dismissTask(taskView, true /* animate */, false /* removeTask */));
|
|
} else {
|
|
RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext());
|
|
RecentsTaskLoadPlan.PreloadOptions opts =
|
|
new RecentsTaskLoadPlan.PreloadOptions();
|
|
opts.loadTitles = false;
|
|
loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1,
|
|
UserHandle.myUserId());
|
|
if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) {
|
|
// The task was removed from the recents list
|
|
handler.post(() ->
|
|
dismissTask(taskView, true /* animate */, false /* removeTask */));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onPinnedStackAnimationStarted() {
|
|
// Needed for activities that auto-enter PiP, which will not trigger a remote
|
|
// animation to be created
|
|
mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
|
|
}
|
|
};
|
|
|
|
private int mLoadPlanId = -1;
|
|
|
|
// Only valid until the launcher state changes to NORMAL
|
|
private int mRunningTaskId = -1;
|
|
private boolean mRunningTaskTileHidden;
|
|
private Task mTmpRunningTask;
|
|
|
|
private boolean mRunningTaskIconScaledDown = false;
|
|
|
|
private boolean mOverviewStateEnabled;
|
|
private boolean mHandleTaskStackChanges;
|
|
private Runnable mNextPageSwitchRunnable;
|
|
private boolean mSwipeDownShouldLaunchApp;
|
|
|
|
private PendingAnimation mPendingAnimation;
|
|
|
|
@ViewDebug.ExportedProperty(category = "launcher")
|
|
private float mContentAlpha = 1;
|
|
|
|
// Keeps track of task views whose visual state should not be reset
|
|
private ArraySet<TaskView> mIgnoreResetTaskViews = new ArraySet<>();
|
|
|
|
private View mClearAllButton;
|
|
|
|
// 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 Layout mEmptyTextLayout;
|
|
|
|
private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
|
|
(inMultiWindowMode) -> {
|
|
if (!inMultiWindowMode && mOverviewStateEnabled) {
|
|
// TODO: Re-enable layout transitions for addition of the unpinned task
|
|
reloadIfNeeded();
|
|
}
|
|
};
|
|
|
|
public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
super(context, attrs, defStyleAttr);
|
|
setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
|
|
enableFreeScroll(true);
|
|
setClipToOutline(true);
|
|
|
|
mFastFlingVelocity = getResources()
|
|
.getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
|
|
mActivity = (T) BaseActivity.fromContext(context);
|
|
mQuickScrubController = new QuickScrubController(mActivity, this);
|
|
mModel = RecentsModel.getInstance(context);
|
|
|
|
mIsRtl = !Utilities.isRtl(getResources());
|
|
setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
|
|
mTaskTopMargin = getResources()
|
|
.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
|
|
|
|
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));
|
|
mEmptyMessagePadding = getResources()
|
|
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
|
|
setWillNotDraw(false);
|
|
updateEmptyMessage();
|
|
}
|
|
|
|
public boolean isRtl() {
|
|
return mIsRtl;
|
|
}
|
|
|
|
public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) {
|
|
TaskView taskView = getTaskView(taskId);
|
|
if (taskView != null) {
|
|
taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData);
|
|
}
|
|
return taskView;
|
|
}
|
|
|
|
@Override
|
|
protected void onWindowVisibilityChanged(int visibility) {
|
|
super.onWindowVisibilityChanged(visibility);
|
|
updateTaskStackListenerState();
|
|
}
|
|
|
|
@Override
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
updateTaskStackListenerState();
|
|
mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
|
|
ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
updateTaskStackListenerState();
|
|
mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
|
|
ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
|
|
}
|
|
|
|
@Override
|
|
public void onViewRemoved(View child) {
|
|
super.onViewRemoved(child);
|
|
|
|
// Clear the task data for the removed child if it was visible
|
|
Task task = ((TaskView) child).getTask();
|
|
if (mHasVisibleTaskData.get(task.key.id)) {
|
|
mHasVisibleTaskData.delete(task.key.id);
|
|
RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
|
|
loader.unloadTaskData(task);
|
|
loader.getHighResThumbnailLoader().onTaskInvisible(task);
|
|
}
|
|
onChildViewsChanged();
|
|
}
|
|
|
|
public boolean isTaskViewVisible(TaskView tv) {
|
|
// For now, just check if it's the active task or an adjacent task
|
|
return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
|
|
}
|
|
|
|
public TaskView getTaskView(int taskId) {
|
|
for (int i = 0; i < getChildCount(); i++) {
|
|
TaskView tv = (TaskView) getChildAt(i);
|
|
if (tv.getTask().key.id == taskId) {
|
|
return tv;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void setOverviewStateEnabled(boolean enabled) {
|
|
mOverviewStateEnabled = enabled;
|
|
updateTaskStackListenerState();
|
|
}
|
|
|
|
public void setNextPageSwitchRunnable(Runnable r) {
|
|
mNextPageSwitchRunnable = r;
|
|
}
|
|
|
|
@Override
|
|
protected void onPageEndTransition() {
|
|
super.onPageEndTransition();
|
|
if (mNextPageSwitchRunnable != null) {
|
|
mNextPageSwitchRunnable.run();
|
|
mNextPageSwitchRunnable = null;
|
|
}
|
|
if (getNextPage() > 0) {
|
|
setSwipeDownShouldLaunchApp(true);
|
|
}
|
|
}
|
|
|
|
private int getScrollEnd() {
|
|
return mIsRtl ? 0 : mMaxScrollX;
|
|
}
|
|
|
|
private float calculateClearAllButtonAlpha() {
|
|
final int childCount = getChildCount();
|
|
if (mShowEmptyMessage || childCount == 0 || mPageScrolls == null
|
|
|| childCount != mPageScrolls.length) {
|
|
return 0;
|
|
}
|
|
|
|
final int scrollEnd = getScrollEnd();
|
|
final int oldestChildScroll = getScrollForPage(childCount - 1);
|
|
|
|
final int clearAllButtonMotionRange = scrollEnd - oldestChildScroll;
|
|
if (clearAllButtonMotionRange == 0) return 0;
|
|
|
|
final float alphaUnbound = ((float) (getScrollX() - oldestChildScroll)) /
|
|
clearAllButtonMotionRange;
|
|
if (alphaUnbound > 1) return 0;
|
|
|
|
return Math.max(alphaUnbound, 0);
|
|
}
|
|
|
|
private void updateClearAllButtonAlpha() {
|
|
if (mClearAllButton != null) {
|
|
final float alpha = calculateClearAllButtonAlpha();
|
|
mIsClearAllButtonFullyRevealed = alpha == 1;
|
|
mClearAllButton.setAlpha(alpha * mContentAlpha);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
|
|
super.onScrollChanged(l, t, oldl, oldt);
|
|
updateClearAllButtonAlpha();
|
|
}
|
|
|
|
@Override
|
|
protected void restoreScrollOnLayout() {
|
|
if (mIsClearAllButtonFullyRevealed) {
|
|
scrollAndForceFinish(getScrollEnd());
|
|
} else {
|
|
super.restoreScrollOnLayout();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
if (ev.getAction() == MotionEvent.ACTION_DOWN && mTouchState == TOUCH_STATE_REST
|
|
&& mScroller.isFinished() && mIsClearAllButtonFullyRevealed) {
|
|
mClearAllButton.getHitRect(mTempRect);
|
|
mTempRect.offset(-getLeft(), -getTop());
|
|
if (mTempRect.contains((int) ev.getX(), (int) ev.getY())) {
|
|
// If nothing is in motion, let the Clear All button process the event.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (ev.getAction() == MotionEvent.ACTION_UP && mShowEmptyMessage) {
|
|
onAllTasksRemoved();
|
|
}
|
|
return super.onTouchEvent(ev);
|
|
}
|
|
|
|
private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
|
|
if (mPendingAnimation != null) {
|
|
mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan));
|
|
return;
|
|
}
|
|
TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
|
|
if (stack == null) {
|
|
removeAllViews();
|
|
onTaskStackUpdated();
|
|
return;
|
|
}
|
|
|
|
int oldChildCount = getChildCount();
|
|
|
|
// Ensure there are as many views as there are tasks in the stack (adding and trimming as
|
|
// necessary)
|
|
final LayoutInflater inflater = LayoutInflater.from(getContext());
|
|
final ArrayList<Task> tasks = new ArrayList<>(stack.getTasks());
|
|
|
|
final int requiredChildCount = tasks.size();
|
|
for (int i = getChildCount(); i < requiredChildCount; i++) {
|
|
final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false);
|
|
addView(taskView);
|
|
}
|
|
while (getChildCount() > requiredChildCount) {
|
|
final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1);
|
|
removeView(taskView);
|
|
}
|
|
|
|
// Unload existing visible task data
|
|
unloadVisibleTaskData();
|
|
|
|
// Rebind and reset all task views
|
|
for (int i = requiredChildCount - 1; i >= 0; i--) {
|
|
final int pageIndex = requiredChildCount - i - 1;
|
|
final Task task = tasks.get(i);
|
|
final TaskView taskView = (TaskView) getChildAt(pageIndex);
|
|
taskView.bind(task);
|
|
}
|
|
resetTaskVisuals();
|
|
|
|
if (oldChildCount != getChildCount()) {
|
|
mQuickScrubController.snapToNextTaskIfAvailable();
|
|
}
|
|
onTaskStackUpdated();
|
|
}
|
|
|
|
protected void onTaskStackUpdated() { }
|
|
|
|
public void resetTaskVisuals() {
|
|
for (int i = getChildCount() - 1; i >= 0; i--) {
|
|
TaskView taskView = (TaskView) getChildAt(i);
|
|
if (!mIgnoreResetTaskViews.contains(taskView)) {
|
|
taskView.resetVisualProperties();
|
|
}
|
|
}
|
|
if (mRunningTaskTileHidden) {
|
|
setRunningTaskHidden(mRunningTaskTileHidden);
|
|
}
|
|
applyIconScale(false /* animate */);
|
|
|
|
updateCurveProperties();
|
|
// Update the set of visible task's data
|
|
loadVisibleTaskData();
|
|
}
|
|
|
|
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);
|
|
DeviceProfile dp = mActivity.getDeviceProfile();
|
|
getTaskSize(dp, mTempRect);
|
|
|
|
mTempRect.top -= mTaskTopMargin;
|
|
setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
|
|
dp.availableWidthPx + mInsets.left - mTempRect.right,
|
|
dp.availableHeightPx + mInsets.top - mTempRect.bottom);
|
|
}
|
|
|
|
protected abstract void getTaskSize(DeviceProfile dp, Rect outRect);
|
|
|
|
public void getTaskSize(Rect outRect) {
|
|
getTaskSize(mActivity.getDeviceProfile(), outRect);
|
|
}
|
|
|
|
@Override
|
|
protected boolean computeScrollHelper() {
|
|
boolean scrolling = super.computeScrollHelper();
|
|
boolean isFlingingFast = false;
|
|
updateCurveProperties();
|
|
if (scrolling || (mTouchState == TOUCH_STATE_SCROLLING)) {
|
|
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();
|
|
}
|
|
|
|
// Update the high res thumbnail loader
|
|
RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
|
|
loader.getHighResThumbnailLoader().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;
|
|
}
|
|
final int halfPageWidth = getNormalChildWidth() / 2;
|
|
final int screenCenter = mInsets.left + getPaddingLeft() + getScrollX() + halfPageWidth;
|
|
final int halfScreenWidth = getMeasuredWidth() / 2;
|
|
final int pageSpacing = mPageSpacing;
|
|
|
|
final int pageCount = getPageCount();
|
|
for (int i = 0; i < pageCount; i++) {
|
|
View page = getPageAt(i);
|
|
float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth;
|
|
float distanceFromScreenCenter = screenCenter - pageCenter;
|
|
float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
|
|
mScrollState.linearInterpolation = Math.min(1,
|
|
Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
|
|
((PageCallbacks) page).onPageScroll(mScrollState);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterates through all thet asks, 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() {
|
|
if (!mOverviewStateEnabled) {
|
|
// Skip loading visible task data if we've already left the overview state
|
|
return;
|
|
}
|
|
|
|
RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
|
|
int centerPageIndex = getPageNearestToCenterOfScreen();
|
|
int lower = Math.max(0, centerPageIndex - 2);
|
|
int upper = Math.min(centerPageIndex + 2, getChildCount() - 1);
|
|
int numChildren = getChildCount();
|
|
|
|
// Update the task data for the in/visible children
|
|
for (int i = 0; i < numChildren; i++) {
|
|
TaskView taskView = (TaskView) getChildAt(i);
|
|
Task task = taskView.getTask();
|
|
boolean visible = lower <= i && i <= 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)) {
|
|
loader.loadTaskData(task);
|
|
loader.getHighResThumbnailLoader().onTaskVisible(task);
|
|
}
|
|
mHasVisibleTaskData.put(task.key.id, visible);
|
|
} else {
|
|
if (mHasVisibleTaskData.get(task.key.id)) {
|
|
loader.unloadTaskData(task);
|
|
loader.getHighResThumbnailLoader().onTaskInvisible(task);
|
|
}
|
|
mHasVisibleTaskData.delete(task.key.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unloads any associated data from the currently visible tasks
|
|
*/
|
|
private void unloadVisibleTaskData() {
|
|
RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
|
|
for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
|
|
if (mHasVisibleTaskData.valueAt(i)) {
|
|
TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
|
|
Task task = taskView.getTask();
|
|
loader.unloadTaskData(task);
|
|
loader.getHighResThumbnailLoader().onTaskInvisible(task);
|
|
}
|
|
}
|
|
mHasVisibleTaskData.clear();
|
|
}
|
|
|
|
protected abstract void onAllTasksRemoved();
|
|
|
|
public void reset() {
|
|
mRunningTaskId = -1;
|
|
mRunningTaskTileHidden = false;
|
|
|
|
unloadVisibleTaskData();
|
|
setCurrentPage(0);
|
|
|
|
OverviewCallbacks.get(getContext()).onResetOverview();
|
|
}
|
|
|
|
/**
|
|
* Reloads the view if anything in recents changed.
|
|
*/
|
|
public void reloadIfNeeded() {
|
|
if (!mModel.isLoadPlanValid(mLoadPlanId)) {
|
|
mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures that the first task in the view represents {@param task} and reloads the view
|
|
* if needed. This allows the swipe-up gesture to assume that the first tile always
|
|
* corresponds to the correct task.
|
|
* 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 showTask(int runningTaskId) {
|
|
if (getChildCount() == 0) {
|
|
// Add an empty view for now until the task plan is loaded and applied
|
|
final TaskView taskView = (TaskView) LayoutInflater.from(getContext())
|
|
.inflate(R.layout.task, this, false);
|
|
addView(taskView);
|
|
|
|
// 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 = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), 0, 0), null,
|
|
null, "", "", 0, 0, false, true, false, false,
|
|
new ActivityManager.TaskDescription(), 0, new ComponentName("", ""), false);
|
|
taskView.bind(mTmpRunningTask);
|
|
}
|
|
setCurrentTask(runningTaskId);
|
|
}
|
|
|
|
/**
|
|
* Hides the tile associated with {@link #mRunningTaskId}
|
|
*/
|
|
public void setRunningTaskHidden(boolean isHidden) {
|
|
mRunningTaskTileHidden = isHidden;
|
|
TaskView runningTask = getTaskView(mRunningTaskId);
|
|
if (runningTask != null) {
|
|
runningTask.setAlpha(isHidden ? 0 : mContentAlpha);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Similar to {@link #showTask(int)} but does not put any restrictions on the first tile.
|
|
*/
|
|
public void setCurrentTask(int runningTaskId) {
|
|
boolean runningTaskTileHidden = mRunningTaskTileHidden;
|
|
boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown;
|
|
|
|
setRunningTaskIconScaledDown(false, false);
|
|
setRunningTaskHidden(false);
|
|
mRunningTaskId = runningTaskId;
|
|
setRunningTaskIconScaledDown(runningTaskIconScaledDown, false);
|
|
setRunningTaskHidden(runningTaskTileHidden);
|
|
|
|
setCurrentPage(0);
|
|
|
|
// Load the tasks (if the loading is already
|
|
mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan);
|
|
}
|
|
|
|
public void showNextTask() {
|
|
TaskView runningTaskView = getTaskView(mRunningTaskId);
|
|
if (runningTaskView == null) {
|
|
// Launch the first task
|
|
if (getChildCount() > 0) {
|
|
((TaskView) getChildAt(0)).launchTask(true /* animate */);
|
|
}
|
|
} else {
|
|
// Get the next launch task
|
|
int runningTaskIndex = indexOfChild(runningTaskView);
|
|
int nextTaskIndex = Math.max(0, Math.min(getChildCount() - 1, runningTaskIndex + 1));
|
|
if (nextTaskIndex < getChildCount()) {
|
|
((TaskView) getChildAt(nextTaskIndex)).launchTask(true /* animate */);
|
|
}
|
|
}
|
|
}
|
|
|
|
public QuickScrubController getQuickScrubController() {
|
|
return mQuickScrubController;
|
|
}
|
|
|
|
public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) {
|
|
if (mRunningTaskIconScaledDown == isScaledDown) {
|
|
return;
|
|
}
|
|
mRunningTaskIconScaledDown = isScaledDown;
|
|
applyIconScale(animate);
|
|
}
|
|
|
|
private void applyIconScale(boolean animate) {
|
|
float scale = mRunningTaskIconScaledDown ? 0 : 1;
|
|
TaskView firstTask = getTaskView(mRunningTaskId);
|
|
if (firstTask != null) {
|
|
if (animate) {
|
|
firstTask.animateIconToScaleAndDim(scale);
|
|
} else {
|
|
firstTask.setIconScaleAndDim(scale);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) {
|
|
mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp;
|
|
}
|
|
|
|
public boolean shouldSwipeDownLaunchApp() {
|
|
return mSwipeDownShouldLaunchApp;
|
|
}
|
|
|
|
public interface PageCallbacks {
|
|
|
|
/**
|
|
* Updates the page UI based on scroll params.
|
|
*/
|
|
default void onPageScroll(ScrollState scrollState) {};
|
|
}
|
|
|
|
public static class ScrollState {
|
|
|
|
/**
|
|
* The progress from 0 to 1, where 0 is the center
|
|
* of the screen and 1 is the edge of the screen.
|
|
*/
|
|
public float linearInterpolation;
|
|
}
|
|
|
|
public void addIgnoreResetTask(TaskView taskView) {
|
|
mIgnoreResetTaskViews.add(taskView);
|
|
}
|
|
|
|
public void removeIgnoreResetTask(TaskView taskView) {
|
|
mIgnoreResetTaskViews.remove(taskView);
|
|
}
|
|
|
|
private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
|
|
addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
|
|
addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
|
|
duration, LINEAR, anim);
|
|
}
|
|
|
|
private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener,
|
|
boolean shouldLog) {
|
|
if (task != null) {
|
|
ActivityManagerWrapper.getInstance().removeTask(task.key.id);
|
|
if (shouldLog) {
|
|
mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
|
|
onEndListener.logAction, Direction.UP, index,
|
|
TaskUtils.getComponentKeyForTask(task.key));
|
|
}
|
|
}
|
|
}
|
|
|
|
public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
|
|
boolean shouldRemoveTask, long duration) {
|
|
if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
|
|
throw new IllegalStateException("Another pending animation is still running");
|
|
}
|
|
AnimatorSet anim = new AnimatorSet();
|
|
PendingAnimation pendingAnimation = new PendingAnimation(anim);
|
|
|
|
int count = getChildCount();
|
|
if (count == 0) {
|
|
return pendingAnimation;
|
|
}
|
|
|
|
int[] oldScroll = new int[count];
|
|
getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
|
|
|
|
int[] newScroll = new int[count];
|
|
getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
|
|
|
|
int scrollDiffPerPage = 0;
|
|
int leftmostPage = mIsRtl ? count -1 : 0;
|
|
int rightmostPage = mIsRtl ? 0 : count - 1;
|
|
if (count > 1) {
|
|
int secondRightmostPage = mIsRtl ? 1 : count - 2;
|
|
scrollDiffPerPage = oldScroll[rightmostPage] - oldScroll[secondRightmostPage];
|
|
}
|
|
int draggedIndex = indexOfChild(taskView);
|
|
|
|
boolean needsCurveUpdates = false;
|
|
for (int i = 0; i < count; i++) {
|
|
View child = getChildAt(i);
|
|
if (child == taskView) {
|
|
if (animateTaskView) {
|
|
addDismissedTaskAnimations(taskView, anim, duration);
|
|
}
|
|
} else {
|
|
// 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 = mIsRtl ? leftmostPage : rightmostPage;
|
|
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) {
|
|
addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff),
|
|
duration, ACCEL, anim);
|
|
needsCurveUpdates = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (needsCurveUpdates) {
|
|
ValueAnimator va = ValueAnimator.ofFloat(0, 1);
|
|
va.addUpdateListener((a) -> updateCurveProperties());
|
|
anim.play(va);
|
|
}
|
|
|
|
// Add a tiny bit of translation Z, so that it draws on top of other views
|
|
if (animateTaskView) {
|
|
taskView.setTranslationZ(0.1f);
|
|
}
|
|
|
|
mPendingAnimation = pendingAnimation;
|
|
mPendingAnimation.addEndListener((onEndListener) -> {
|
|
if (onEndListener.isSuccess) {
|
|
if (shouldRemoveTask) {
|
|
removeTask(taskView.getTask(), draggedIndex, onEndListener, true);
|
|
}
|
|
int pageToSnapTo = mCurrentPage;
|
|
if (draggedIndex < pageToSnapTo) {
|
|
pageToSnapTo -= 1;
|
|
}
|
|
removeView(taskView);
|
|
if (getChildCount() == 0) {
|
|
onAllTasksRemoved();
|
|
} else if (!mIsClearAllButtonFullyRevealed) {
|
|
snapToPageImmediately(pageToSnapTo);
|
|
}
|
|
}
|
|
resetTaskVisuals();
|
|
mPendingAnimation = null;
|
|
});
|
|
return pendingAnimation;
|
|
}
|
|
|
|
public PendingAnimation createAllTasksDismissAnimation(long duration) {
|
|
if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
|
|
throw new IllegalStateException("Another pending animation is still running");
|
|
}
|
|
AnimatorSet anim = new AnimatorSet();
|
|
PendingAnimation pendingAnimation = new PendingAnimation(anim);
|
|
|
|
int count = getChildCount();
|
|
for (int i = 0; i < count; i++) {
|
|
addDismissedTaskAnimations(getChildAt(i), anim, duration);
|
|
}
|
|
|
|
mPendingAnimation = pendingAnimation;
|
|
mPendingAnimation.addEndListener((onEndListener) -> {
|
|
if (onEndListener.isSuccess) {
|
|
while (getChildCount() != 0) {
|
|
TaskView taskView = getPageAt(getChildCount() - 1);
|
|
removeTask(taskView.getTask(), -1, onEndListener, false);
|
|
removeView(taskView);
|
|
}
|
|
onAllTasksRemoved();
|
|
}
|
|
mPendingAnimation = null;
|
|
});
|
|
return pendingAnimation;
|
|
}
|
|
|
|
private static void addAnim(ObjectAnimator anim, long duration,
|
|
TimeInterpolator interpolator, AnimatorSet set) {
|
|
anim.setDuration(duration).setInterpolator(interpolator);
|
|
set.play(anim);
|
|
}
|
|
|
|
private boolean snapToPageRelative(int delta, boolean cycle) {
|
|
if (getPageCount() == 0) {
|
|
return false;
|
|
}
|
|
final int newPageUnbound = getNextPage() + delta;
|
|
if (!cycle && (newPageUnbound < 0 || newPageUnbound >= getChildCount())) {
|
|
return false;
|
|
}
|
|
snapToPage((newPageUnbound + getPageCount()) % getPageCount());
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onVisibilityAggregated(boolean isVisible) {
|
|
super.onVisibilityAggregated(isVisible);
|
|
if (isVisible && !isFocused()) {
|
|
// Having focus, even in touch mode, keeps us from losing [Alt+]Tab by preventing
|
|
// switching to keyboard mode.
|
|
requestFocus();
|
|
}
|
|
}
|
|
|
|
private void runDismissAnimation(PendingAnimation pendingAnim) {
|
|
AnimatorPlaybackController controller = AnimatorPlaybackController.wrap(
|
|
pendingAnim.anim, DISMISS_TASK_DURATION);
|
|
controller.dispatchOnStart();
|
|
controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
|
|
controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
|
|
controller.start();
|
|
}
|
|
|
|
public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
|
|
runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask,
|
|
DISMISS_TASK_DURATION));
|
|
}
|
|
|
|
public void dismissAllTasks() {
|
|
runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
|
|
}
|
|
|
|
@Override
|
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
switch (event.getKeyCode()) {
|
|
case KeyEvent.KEYCODE_TAB:
|
|
return snapToPageRelative(event.isShiftPressed() ? -1 : 1,
|
|
event.isAltPressed() /* cycle */);
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
return snapToPageRelative(mIsRtl ? -1 : 1, false /* cycle */);
|
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
return snapToPageRelative(mIsRtl ? 1 : -1, false /* cycle */);
|
|
case KeyEvent.KEYCODE_DEL:
|
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
|
dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/,
|
|
true /*removeTask*/);
|
|
return true;
|
|
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
|
if (event.isAltPressed()) {
|
|
// Numpad DEL pressed while holding Alt.
|
|
dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/,
|
|
true /*removeTask*/);
|
|
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) {
|
|
alpha = Utilities.boundToRange(alpha, 0, 1);
|
|
mContentAlpha = alpha;
|
|
for (int i = getChildCount() - 1; i >= 0; i--) {
|
|
TaskView child = getPageAt(i);
|
|
if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) {
|
|
getChildAt(i).setAlpha(alpha);
|
|
}
|
|
}
|
|
|
|
int alphaInt = Math.round(alpha * 255);
|
|
mEmptyMessagePaint.setAlpha(alphaInt);
|
|
mEmptyIcon.setAlpha(alphaInt);
|
|
updateClearAllButtonAlpha();
|
|
}
|
|
|
|
private float[] getAdjacentScaleAndTranslation(TaskView currTask,
|
|
float currTaskToScale, float currTaskToTranslationY) {
|
|
float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale());
|
|
sTempFloatArray[0] = currTaskToScale;
|
|
sTempFloatArray[1] = mIsRtl ? -displacement : displacement;
|
|
sTempFloatArray[2] = currTaskToTranslationY;
|
|
return sTempFloatArray;
|
|
}
|
|
|
|
@Override
|
|
public void onViewAdded(View child) {
|
|
super.onViewAdded(child);
|
|
child.setAlpha(mContentAlpha);
|
|
onChildViewsChanged();
|
|
}
|
|
|
|
@Override
|
|
public TaskView getPageAt(int index) {
|
|
return (TaskView) getChildAt(index);
|
|
}
|
|
|
|
public void updateEmptyMessage() {
|
|
boolean isEmpty = getChildCount() == 0;
|
|
boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
|
|
|| mLastMeasureSize.y != getHeight();
|
|
if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
|
|
return;
|
|
}
|
|
setContentDescription(isEmpty ? mEmptyMessage : "");
|
|
mShowEmptyMessage = isEmpty;
|
|
updateEmptyStateUi(hasSizeChanged);
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
super.onLayout(changed, left, top, right, bottom);
|
|
updateEmptyStateUi(changed);
|
|
|
|
// Set the pivot points to match the task preview center
|
|
setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
|
|
+ (getHeight() - mInsets.bottom - getPaddingBottom())) / 2);
|
|
setPivotX(((mInsets.left + getPaddingLeft())
|
|
+ (getWidth() - mInsets.right - getPaddingRight())) / 2);
|
|
}
|
|
|
|
private void updateEmptyStateUi(boolean sizeChanged) {
|
|
boolean hasValidSize = getWidth() > 0 && getHeight() > 0;
|
|
if (sizeChanged && hasValidSize) {
|
|
mEmptyTextLayout = null;
|
|
mLastMeasureSize.set(getWidth(), getHeight());
|
|
}
|
|
updateClearAllButtonAlpha();
|
|
|
|
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, ClipAnimationHelper clipAnimationHelper) {
|
|
AnimatorSet anim = new AnimatorSet();
|
|
|
|
int taskIndex = indexOfChild(tv);
|
|
int centerTaskIndex = getCurrentPage();
|
|
boolean launchingCenterTask = taskIndex == centerTaskIndex;
|
|
|
|
float toScale = clipAnimationHelper.getSourceRect().width()
|
|
/ clipAnimationHelper.getTargetRect().width();
|
|
float toTranslationY = clipAnimationHelper.getSourceRect().centerY()
|
|
- clipAnimationHelper.getTargetRect().centerY();
|
|
if (launchingCenterTask) {
|
|
TaskView centerTask = getPageAt(centerTaskIndex);
|
|
if (taskIndex - 1 >= 0) {
|
|
TaskView adjacentTask = getPageAt(taskIndex - 1);
|
|
float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask,
|
|
toScale, toTranslationY);
|
|
scaleAndTranslation[1] = -scaleAndTranslation[1];
|
|
anim.play(createAnimForChild(adjacentTask, scaleAndTranslation));
|
|
}
|
|
if (taskIndex + 1 < getPageCount()) {
|
|
TaskView adjacentTask = getPageAt(taskIndex + 1);
|
|
float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask,
|
|
toScale, toTranslationY);
|
|
anim.play(createAnimForChild(adjacentTask, scaleAndTranslation));
|
|
}
|
|
} else {
|
|
// We are launching an adjacent task, so parallax the center and other adjacent task.
|
|
float displacementX = tv.getWidth() * (toScale - tv.getCurveScale());
|
|
anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X,
|
|
mIsRtl ? -displacementX : displacementX));
|
|
|
|
int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
|
|
if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
|
|
anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex),
|
|
new PropertyListBuilder()
|
|
.translationX(mIsRtl ? -displacementX : displacementX)
|
|
.scale(1)
|
|
.build()));
|
|
}
|
|
}
|
|
return anim;
|
|
}
|
|
|
|
private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) {
|
|
AnimatorSet anim = new AnimatorSet();
|
|
anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0]));
|
|
anim.play(ObjectAnimator.ofPropertyValuesHolder(child,
|
|
new PropertyListBuilder()
|
|
.translationX(toScaleAndTranslation[1])
|
|
.translationY(toScaleAndTranslation[2])
|
|
.build()));
|
|
return anim;
|
|
}
|
|
|
|
public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) {
|
|
if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
|
|
throw new IllegalStateException("Another pending animation is still running");
|
|
}
|
|
|
|
int count = getChildCount();
|
|
if (count == 0) {
|
|
return new PendingAnimation(new AnimatorSet());
|
|
}
|
|
|
|
tv.setVisibility(INVISIBLE);
|
|
int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
|
|
TaskViewDrawable drawable = new TaskViewDrawable(tv, this);
|
|
getOverlay().add(drawable);
|
|
|
|
ObjectAnimator drawableAnim =
|
|
ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0);
|
|
drawableAnim.setInterpolator(LINEAR);
|
|
drawableAnim.addUpdateListener((animator) -> {
|
|
// Once we pass a certain threshold, update the sysui flags to match the target tasks'
|
|
// flags
|
|
mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW,
|
|
animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD
|
|
? targetSysUiFlags
|
|
: 0);
|
|
});
|
|
|
|
AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv,
|
|
drawable.getClipAnimationHelper());
|
|
anim.play(drawableAnim);
|
|
anim.setDuration(duration);
|
|
|
|
Consumer<Boolean> onTaskLaunchFinish = (result) -> {
|
|
onTaskLaunched(result);
|
|
tv.setVisibility(VISIBLE);
|
|
getOverlay().remove(drawable);
|
|
};
|
|
|
|
mPendingAnimation = new PendingAnimation(anim);
|
|
mPendingAnimation.addEndListener((onEndListener) -> {
|
|
if (onEndListener.isSuccess) {
|
|
Consumer<Boolean> onLaunchResult = (result) -> {
|
|
onTaskLaunchFinish.accept(result);
|
|
if (!result) {
|
|
tv.notifyTaskLaunchFailed(TAG);
|
|
}
|
|
};
|
|
tv.launchTask(false, onLaunchResult, getHandler());
|
|
Task task = tv.getTask();
|
|
if (task != null) {
|
|
mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
|
|
onEndListener.logAction, Direction.DOWN, indexOfChild(tv),
|
|
TaskUtils.getComponentKeyForTask(task.key));
|
|
}
|
|
} else {
|
|
onTaskLaunchFinish.accept(false);
|
|
}
|
|
mPendingAnimation = null;
|
|
});
|
|
return mPendingAnimation;
|
|
}
|
|
|
|
public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
|
|
|
|
protected void onTaskLaunched(boolean success) {
|
|
resetTaskVisuals();
|
|
}
|
|
|
|
@Override
|
|
protected void notifyPageSwitchListener(int prevPage) {
|
|
super.notifyPageSwitchListener(prevPage);
|
|
loadVisibleTaskData();
|
|
}
|
|
|
|
@Override
|
|
protected String getCurrentPageDescription() {
|
|
return "";
|
|
}
|
|
|
|
private int additionalScrollForClearAllButton() {
|
|
return (int) getResources().getDimension(
|
|
R.dimen.clear_all_container_width) - getPaddingEnd();
|
|
}
|
|
|
|
@Override
|
|
protected int computeMaxScrollX() {
|
|
if (getChildCount() == 0) {
|
|
return super.computeMaxScrollX();
|
|
}
|
|
|
|
// Allow a clear_all_container_width-sized gap after the last task.
|
|
return super.computeMaxScrollX() + (mIsRtl ? 0 : additionalScrollForClearAllButton());
|
|
}
|
|
|
|
@Override
|
|
protected int offsetForPageScrolls() {
|
|
return mIsRtl ? additionalScrollForClearAllButton() : 0;
|
|
}
|
|
|
|
public void setClearAllButton(View clearAllButton) {
|
|
mClearAllButton = clearAllButton;
|
|
updateClearAllButtonAlpha();
|
|
}
|
|
|
|
private void onChildViewsChanged() {
|
|
final int childCount = getChildCount();
|
|
mClearAllButton.setVisibility(childCount == 0 ? INVISIBLE : VISIBLE);
|
|
}
|
|
|
|
public void revealClearAllButton() {
|
|
setCurrentPage(getChildCount() - 1); // Loads tasks info if needed.
|
|
scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0);
|
|
}
|
|
|
|
@Override
|
|
public void addChildrenForAccessibility(ArrayList<View> outChildren) {
|
|
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, getChildCount(), false,
|
|
AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
|
|
info.setCollectionInfo(collectionInfo);
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
|
super.onInitializeAccessibilityEvent(event);
|
|
|
|
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
|
|
event.setFromIndex(getCurrentPage());
|
|
event.setToIndex(getCurrentPage());
|
|
event.setItemCount(getChildCount());
|
|
}
|
|
}
|
|
|
|
@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 addTaskAccessibilityActionsExtra(AccessibilityNodeInfo info) {
|
|
}
|
|
|
|
public boolean performTaskAccessibilityActionExtra(int action) {
|
|
return false;
|
|
}
|
|
}
|