| /* |
| * 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.Bundle; |
| 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(); |
| setFocusable(false); |
| } |
| |
| 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(); |
| final boolean revealed = alpha == 1; |
| if (mIsClearAllButtonFullyRevealed != revealed) { |
| mIsClearAllButtonFullyRevealed = revealed; |
| mClearAllButton.setImportantForAccessibility(revealed ? |
| IMPORTANT_FOR_ACCESSIBILITY_YES : |
| IMPORTANT_FOR_ACCESSIBILITY_NO); |
| } |
| 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); |
| |
| // Keep this logic in sync with ActivityControlHelper.getTranslationYForQuickScrub. |
| 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; |
| } |
| |
| 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); |
| setFocusable(childCount != 0); |
| } |
| |
| public void revealClearAllButton() { |
| setCurrentPage(getChildCount() - 1); // Loads tasks info if needed. |
| scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0); |
| } |
| |
| @Override |
| public boolean performAccessibilityAction(int action, Bundle arguments) { |
| if (getChildCount() > 0) { |
| switch (action) { |
| case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { |
| if (!mIsClearAllButtonFullyRevealed && getCurrentPage() == getPageCount() - 1) { |
| revealClearAllButton(); |
| return true; |
| } |
| } |
| case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { |
| if (mIsClearAllButtonFullyRevealed) { |
| setCurrentPage(getChildCount() - 1); |
| return true; |
| } |
| } |
| break; |
| } |
| } |
| return super.performAccessibilityAction(action, arguments); |
| } |
| |
| @Override |
| public void addChildrenForAccessibility(ArrayList<View> outChildren) { |
| outChildren.add(mClearAllButton); |
| for (int i = getChildCount() - 1; i >= 0; --i) { |
| outChildren.add(getChildAt(i)); |
| } |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(info); |
| |
| if (getChildCount() > 0) { |
| info.addAction(mIsClearAllButtonFullyRevealed ? |
| AccessibilityNodeInfo.ACTION_SCROLL_FORWARD : |
| AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); |
| info.setScrollable(true); |
| } |
| |
| 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); |
| |
| event.setScrollable(getPageCount() > 0); |
| |
| if (!mIsClearAllButtonFullyRevealed |
| && event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { |
| final int childCount = getChildCount(); |
| final int[] visibleTasks = getVisibleChildrenRange(); |
| event.setFromIndex(childCount - visibleTasks[1] - 1); |
| event.setToIndex(childCount - visibleTasks[0] - 1); |
| event.setItemCount(childCount); |
| } |
| } |
| |
| @Override |
| public CharSequence getAccessibilityClassName() { |
| // To hear position-in-list related feedback from Talkback. |
| return ListView.class.getName(); |
| } |
| |
| @Override |
| protected boolean isPageOrderFlipped() { |
| return true; |
| } |
| |
| public boolean performTaskAccessibilityActionExtra(int action) { |
| return false; |
| } |
| } |