diff --git a/packages/SystemUI/res/drawable/recents_info_dark.xml b/packages/SystemUI/res/drawable/recents_info_dark.xml new file mode 100644 index 0000000000000..b1a22425ef2d8 --- /dev/null +++ b/packages/SystemUI/res/drawable/recents_info_dark.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/SystemUI/res/drawable/recents_info_light.xml b/packages/SystemUI/res/drawable/recents_info_light.xml new file mode 100644 index 0000000000000..bc58c3b406d3f --- /dev/null +++ b/packages/SystemUI/res/drawable/recents_info_light.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/SystemUI/res/layout/recents_task_view_header.xml b/packages/SystemUI/res/layout/recents_task_view_header.xml index 07ac39a3eceae..a94e781d4a8d0 100644 --- a/packages/SystemUI/res/layout/recents_task_view_header.xml +++ b/packages/SystemUI/res/layout/recents_task_view_header.xml @@ -64,12 +64,19 @@ android:background="@drawable/recents_button_bg" android:visibility="invisible" android:src="@drawable/recents_dismiss_light" /> - + + + + + + diff --git a/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml new file mode 100644 index 0000000000000..5cdde9e396d0c --- /dev/null +++ b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml @@ -0,0 +1,53 @@ + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml b/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml new file mode 100644 index 0000000000000..e740458728dd7 --- /dev/null +++ b/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml index 6795da43e087a..17ff19549a0d0 100644 --- a/packages/SystemUI/res/values-sw600dp/config.xml +++ b/packages/SystemUI/res/values-sw600dp/config.xml @@ -35,7 +35,7 @@ - -4 + -3 3 200 + + 125 + 125 @@ -194,7 +197,7 @@ - -4 + -3 3 - 64dp + 32dp 56dp diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index d28da41e153f5..fd4161fe3e48d 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -367,6 +367,7 @@ public class SwipeHelper implements Gefingerpoken { } anim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { + updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); mCallback.onChildDismissed(view); if (endAction != null) { endAction.run(); @@ -381,9 +382,17 @@ public class SwipeHelper implements Gefingerpoken { updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); } }); + prepareDismissAnimation(animView, anim); anim.start(); } + /** + * Called to update the dismiss animation. + */ + protected void prepareDismissAnimation(View view, Animator anim) { + // Do nothing + } + public void snapChild(final View view, float velocity) { final View animView = mCallback.getChildContentView(view); final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); @@ -401,14 +410,14 @@ public class SwipeHelper implements Gefingerpoken { mCallback.onChildSnappedBack(animView); } }); - updateSnapBackAnimation(anim); + prepareSnapBackAnimation(animView, anim); anim.start(); } /** * Called to update the snap back animation. */ - protected void updateSnapBackAnimation(Animator anim) { + protected void prepareSnapBackAnimation(View view, Animator anim) { // Do nothing } diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java index db55f286cf20c..3a3b19dc34411 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -183,7 +183,7 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD RecentsConfiguration config = Recents.getConfiguration(); RecentsActivityLaunchState launchState = config.getLaunchState(); if (!plan.hasTasks()) { - loader.preloadTasks(plan, launchState.launchedFromHome); + loader.preloadTasks(plan, -1, launchState.launchedFromHome); } RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options(); loadOpts.runningTaskId = launchState.launchedToTaskId; @@ -192,24 +192,14 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD loader.loadTasks(this, plan, loadOpts); TaskStack stack = plan.getTaskStack(); - ArrayList tasks = stack.getStackTasks(); - int taskCount = stack.getTaskCount(); mRecentsView.setTaskStack(stack); - // Mark the task that is the launch target - int launchTaskIndexInStack = 0; - if (launchState.launchedToTaskId != -1) { - for (int j = 0; j < taskCount; j++) { - Task t = tasks.get(j); - if (t.key.id == launchState.launchedToTaskId) { - t.isLaunchTarget = true; - launchTaskIndexInStack = tasks.size() - j - 1; - break; - } - } - } - // Animate the SystemUI scrims into view + Task launchTarget = stack.getLaunchTarget(); + int taskCount = stack.getTaskCount(); + int launchTaskIndexInStack = launchTarget != null + ? stack.indexOfStackTask(launchTarget) + : 0; boolean hasStatusBarScrim = taskCount > 0; boolean animateStatusBarScrim = launchState.launchedFromHome; boolean hasNavBarScrim = (taskCount > 0) && !config.hasTransposedNavBar; @@ -527,7 +517,7 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD launchOpts.loadThumbnails = false; launchOpts.onlyLoadForCache = true; RecentsTaskLoadPlan loadPlan = loader.createLoadPlan(this); - loader.preloadTasks(loadPlan, false); + loader.preloadTasks(loadPlan, -1, false); loader.loadTasks(this, loadPlan, launchOpts); EventBus.getDefault().send(new TaskStackUpdatedEvent(loadPlan.getTaskStack())); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java index 3151fd7f3e6fe..386696763605f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java @@ -37,6 +37,8 @@ public class RecentsDebugFlags implements TunerService.Tunable { public static final boolean EnableSearchBar = false; // This disables the bitmap and icon caches public static final boolean DisableBackgroundCache = false; + // Enables the task affiliations + public static final boolean EnableAffiliatedTaskGroups = true; // Enables the simulated task affiliations public static final boolean EnableSimulatedTaskGroups = false; // Defines the number of mock task affiliations per group diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java index 7c25d24326a98..3eee08766c735 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java @@ -118,7 +118,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub implements // Load the next task only if we aren't svelte RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); - loader.preloadTasks(plan, true); + loader.preloadTasks(plan, -1, true /* isTopTaskHome */); RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); // This callback is made when a new activity is launched and the old one is paused // so ignore the current activity and try and preload the thumbnail for the @@ -198,7 +198,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub implements // We can use a new plan since the caches will be the same. RecentsTaskLoader loader = Recents.getTaskLoader(); RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); - loader.preloadTasks(plan, true /* isTopTaskHome */); + loader.preloadTasks(plan, -1, true /* isTopTaskHome */); RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); launchOpts.numVisibleTasks = loader.getIconCacheSize(); launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize(); @@ -370,7 +370,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub implements sInstanceLoadPlan = loader.createLoadPlan(mContext); if (topTask != null && !ssp.isRecentsTopMost(topTask, topTaskHome)) { sInstanceLoadPlan.preloadRawTasks(topTaskHome.value); - loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value); + loader.preloadTasks(sInstanceLoadPlan, topTask.id, topTaskHome.value); TaskStack stack = sInstanceLoadPlan.getTaskStack(); if (stack.getTaskCount() > 0) { // We try and draw the thumbnail transition bitmap in parallel before @@ -399,7 +399,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub implements SystemServicesProxy ssp = Recents.getSystemServices(); RecentsTaskLoader loader = Recents.getTaskLoader(); RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); - loader.preloadTasks(plan, true /* isTopTaskHome */); + loader.preloadTasks(plan, -1, true /* isTopTaskHome */); TaskStack focusedStack = plan.getTaskStack(); // Return early if there are no tasks in the focused stack @@ -451,7 +451,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub implements SystemServicesProxy ssp = Recents.getSystemServices(); RecentsTaskLoader loader = Recents.getTaskLoader(); RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); - loader.preloadTasks(plan, true /* isTopTaskHome */); + loader.preloadTasks(plan, -1, true /* isTopTaskHome */); TaskStack focusedStack = plan.getTaskStack(); // Return early if there are no tasks in the focused stack @@ -756,29 +756,18 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub implements private TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, TaskStackView stackView, int runningTaskId, Task runningTaskOut) { // Find the running task in the TaskStack - Task task = null; - ArrayList tasks = stack.getStackTasks(); - if (runningTaskId != -1) { - // Otherwise, try and find the task with the - int taskCount = tasks.size(); - for (int i = taskCount - 1; i >= 0; i--) { - Task t = tasks.get(i); - if (t.key.id == runningTaskId) { - task = t; - runningTaskOut.copyFrom(t); - break; - } - } - } - if (task == null) { + Task launchTask = stack.getLaunchTarget(); + if (launchTask != null) { + runningTaskOut.copyFrom(launchTask); + } else { // If no task is specified or we can not find the task just use the front most one - task = tasks.get(tasks.size() - 1); - runningTaskOut.copyFrom(task); + launchTask = stack.getStackFrontMostTask(); + runningTaskOut.copyFrom(launchTask); } // Get the transform for the running task stackView.getScroller().setStackScrollToInitialState(); - mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task, + mTmpTransform = stackView.getStackAlgorithm().getStackTransform(launchTask, stackView.getScroller().getStackScroll(), mTmpTransform, null); return mTmpTransform; } @@ -826,7 +815,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub implements sInstanceLoadPlan = loader.createLoadPlan(mContext); } if (mReloadTasks || mTriggeredFromAltTab || !sInstanceLoadPlan.hasTasks()) { - loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome); + loader.preloadTasks(sInstanceLoadPlan, topTask.id, isTopTaskHome); } TaskStack stack = sInstanceLoadPlan.getTaskStack(); diff --git a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java index 80597bc0bc146..f6655c7f86a2a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java @@ -36,6 +36,7 @@ import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.TaskViewAnimation; import java.util.ArrayList; import java.util.Calendar; @@ -143,7 +144,6 @@ public class RecentsHistoryAdapter extends RecyclerView.Adapter mTasks = new ArrayList<>(); private final List mRows = new ArrayList<>(); private final SparseIntArray mTaskRowCount = new SparseIntArray(); private TaskStack mStack; @@ -268,8 +268,8 @@ public class RecentsHistoryAdapter extends RecyclerView.AdapterRectF values. + */ +public class RectFEvaluator implements TypeEvaluator { + + private RectF mRect = new RectF(); + + /** + * This function returns the result of linearly interpolating the start and + * end Rect values, with fraction representing the proportion + * between the start and end values. The calculation is a simple parametric + * calculation on each of the separate components in the Rect objects + * (left, top, right, and bottom). + * + *

The object returned will be the reuseRect passed into the constructor.

+ * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start Rect + * @param endValue The end Rect + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + @Override + public RectF evaluate(float fraction, RectF startValue, RectF endValue) { + float left = startValue.left + ((endValue.left - startValue.left) * fraction); + float top = startValue.top + ((endValue.top - startValue.top) * fraction); + float right = startValue.right + ((endValue.right - startValue.right) * fraction); + float bottom = startValue.bottom + ((endValue.bottom - startValue.bottom) * fraction); + mRect.set(left, top, right, bottom); + return mRect; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java index adc1e9200daa4..3f52ae8d7ed15 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java @@ -30,6 +30,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -554,39 +555,41 @@ public class SystemServicesProxy { } } - /** Returns the activity label */ - public String getActivityLabel(ActivityInfo info) { + /** + * Returns the activity label, badging if necessary. + */ + public String getBadgedActivityLabel(ActivityInfo info, int userId) { if (mPm == null) return null; // If we are mocking, then return a mock label if (RecentsDebugFlags.Static.EnableSystemServicesProxy) { - return "Recent Task"; + return "Recent Task: " + userId; } - return info.loadLabel(mPm).toString(); + return getBadgedLabel(info.loadLabel(mPm).toString(), userId); } - /** Returns the application label */ - public String getApplicationLabel(Intent baseIntent, int userId) { + /** + * Returns the application label, badging if necessary. + */ + public String getBadgedApplicationLabel(ApplicationInfo appInfo, int userId) { if (mPm == null) return null; // If we are mocking, then return a mock label if (RecentsDebugFlags.Static.EnableSystemServicesProxy) { - return "Recent Task"; + return "Recent Task App: " + userId; } - ResolveInfo ri = mPm.resolveActivityAsUser(baseIntent, 0, userId); - CharSequence label = (ri != null) ? ri.loadLabel(mPm) : null; - return (label != null) ? label.toString() : null; + return getBadgedLabel(appInfo.loadLabel(mPm).toString(), userId); } - /** Returns the content description for a given task */ - public String getContentDescription(Intent baseIntent, int userId, String activityLabel, - Resources res) { - String applicationLabel = getApplicationLabel(baseIntent, userId); - if (applicationLabel == null) { - return getBadgedLabel(activityLabel, userId); - } + /** + * Returns the content description for a given task, badging it if necessary. The content + * description joins the app and activity labels. + */ + public String getBadgedContentDescription(ActivityInfo info, int userId, Resources res) { + String activityLabel = info.loadLabel(mPm).toString(); + String applicationLabel = info.applicationInfo.loadLabel(mPm).toString(); String badgedApplicationLabel = getBadgedLabel(applicationLabel, userId); return applicationLabel.equals(activityLabel) ? badgedApplicationLabel : res.getString(R.string.accessibility_recents_task_header, @@ -609,6 +612,22 @@ public class SystemServicesProxy { return getBadgedIcon(icon, userId); } + /** + * Returns the application icon for the ApplicationInfo for a user, badging if + * necessary. + */ + public Drawable getBadgedApplicationIcon(ApplicationInfo appInfo, int userId) { + if (mPm == null) return null; + + // If we are mocking, then return a mock label + if (RecentsDebugFlags.Static.EnableSystemServicesProxy) { + return new ColorDrawable(0xFF666666); + } + + Drawable icon = appInfo.loadIcon(mPm); + return getBadgedIcon(icon, userId); + } + /** * Returns the task description icon, loading and badging it if it necessary. */ diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java index 822ad77e71b81..9cdd703852fe3 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java @@ -25,7 +25,8 @@ import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; import android.util.ArraySet; - +import android.util.SparseArray; +import android.util.SparseIntArray; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.recents.Recents; @@ -92,7 +93,7 @@ public class RecentsTaskLoadPlan { } /** - * An optimization to preload the raw list of tasks. The raw tasks are saved in least-recent + * An optimization to preload the raw list of tasks. The raw tasks are saved in least-recent * to most-recent order. */ public synchronized void preloadRawTasks(boolean isTopTaskHome) { @@ -107,7 +108,7 @@ public class RecentsTaskLoadPlan { } /** - * Preloads the list of recent tasks from the system. After this call, the TaskStack will + * Preloads the list of recent tasks from the system. After this call, the TaskStack will * have a list of all the recent tasks with their metadata, not including icons or * thumbnails which were not cached and have to be loaded. * @@ -115,13 +116,16 @@ public class RecentsTaskLoadPlan { * - least-recent to most-recent stack tasks * - least-recent to most-recent freeform tasks */ - public synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) { + public synchronized void preloadPlan(RecentsTaskLoader loader, int topTaskId, + boolean isTopTaskHome) { Resources res = mContext.getResources(); ArrayList allTasks = new ArrayList<>(); if (mRawTasks == null) { preloadRawTasks(isTopTaskHome); } + SparseArray affiliatedTasks = new SparseArray<>(); + SparseIntArray affiliatedTaskCounts = new SparseIntArray(); String dismissDescFormat = mContext.getString( R.string.accessibility_recents_item_will_be_dismissed); long lastStackActiveTime = Prefs.getLong(mContext, @@ -131,6 +135,17 @@ public class RecentsTaskLoadPlan { for (int i = 0; i < taskCount; i++) { ActivityManager.RecentTaskInfo t = mRawTasks.get(i); + // Affiliated tasks are returned in a specific order from ActivityManager but without a + // lastActiveTime since it hasn't yet been started. However, we later sort the task list + // by lastActiveTime, which rearranges the tasks. For now, we need to workaround this + // by updating the lastActiveTime of this task to the lastActiveTime of the task it is + // affiliated with, in the same order that we encounter it in the original list (just + // its index in the task group for the task it is affiliated with). + if (t.persistentId != t.affiliatedTaskId) { + t.lastActiveTime = affiliatedTasks.get(t.affiliatedTaskId).lastActiveTime + + affiliatedTaskCounts.get(t.affiliatedTaskId, 0) + 1; + } + // Compose the task key Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.stackId, t.baseIntent, t.userId, t.firstActiveTime, t.lastActiveTime); @@ -140,13 +155,14 @@ public class RecentsTaskLoadPlan { boolean isFreeformTask = SystemServicesProxy.isFreeformStack(t.stackId); boolean isStackTask = isFreeformTask || (!isHistoricalTask(t) || (t.lastActiveTime >= lastStackActiveTime && i >= (taskCount - MIN_NUM_TASKS))); + boolean isLaunchTarget = taskKey.id == topTaskId; if (isStackTask && newLastStackActiveTime < 0) { newLastStackActiveTime = t.lastActiveTime; } // Load the title, icon, and color String title = loader.getAndUpdateActivityTitle(taskKey, t.taskDescription); - String contentDescription = loader.getAndUpdateContentDescription(taskKey, title, res); + String contentDescription = loader.getAndUpdateContentDescription(taskKey, res); String dismissDescription = String.format(dismissDescFormat, contentDescription); Drawable icon = isStackTask ? loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, res, false) @@ -157,9 +173,11 @@ public class RecentsTaskLoadPlan { // Add the task to the stack Task task = new Task(taskKey, t.affiliatedTaskId, t.affiliatedTaskColor, icon, thumbnail, title, contentDescription, dismissDescription, activityColor, - !isStackTask, t.bounds, t.taskDescription); + !isStackTask, isLaunchTarget, t.bounds, t.taskDescription); allTasks.add(task); + affiliatedTaskCounts.put(taskKey.id, affiliatedTaskCounts.get(taskKey.id, 0) + 1); + affiliatedTasks.put(taskKey.id, taskKey); } if (newLastStackActiveTime != -1) { Prefs.putLong(mContext, Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME, diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java index 28338d8330fa4..44ad2397bd655 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java @@ -314,8 +314,8 @@ public class RecentsTaskLoader { } /** Preloads recents tasks using the specified plan to store the output. */ - public void preloadTasks(RecentsTaskLoadPlan plan, boolean isTopTaskHome) { - plan.preloadPlan(this, isTopTaskHome); + public void preloadTasks(RecentsTaskLoadPlan plan, int topTaskId, boolean isTopTaskHome) { + plan.preloadPlan(this, topTaskId, isTopTaskHome); } /** Begins loading the heavy task data according to the specified options. */ @@ -436,7 +436,7 @@ public class RecentsTaskLoader { // All short paths failed, load the label from the activity info and cache it ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey); if (activityInfo != null) { - label = ssp.getActivityLabel(activityInfo); + label = ssp.getBadgedActivityLabel(activityInfo, taskKey.userId); mActivityLabelCache.put(taskKey, label); return label; } @@ -449,8 +449,7 @@ public class RecentsTaskLoader { * Returns the cached task content description if the task key is not expired, updating the * cache if it is. */ - String getAndUpdateContentDescription(Task.TaskKey taskKey, String activityLabel, - Resources res) { + String getAndUpdateContentDescription(Task.TaskKey taskKey, Resources res) { SystemServicesProxy ssp = Recents.getSystemServices(); // Return the cached content description if it exists @@ -458,13 +457,11 @@ public class RecentsTaskLoader { if (label != null) { return label; } - // If the given activity label is empty, don't compute or cache the content description - if (activityLabel.isEmpty()) { - return ""; - } - label = ssp.getContentDescription(taskKey.baseIntent, taskKey.userId, activityLabel, res); - if (label != null) { + // All short paths failed, load the label from the activity info and cache it + ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey); + if (activityInfo != null) { + label = ssp.getBadgedContentDescription(activityInfo, taskKey.userId, res); mContentDescriptionCache.put(taskKey, label); return label; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java index 29e7077ccf76d..193bd17b81e7d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java @@ -154,7 +154,8 @@ public class Task { public Task(TaskKey key, int affiliationTaskId, int affiliationColor, Drawable icon, Bitmap thumbnail, String title, String contentDescription, String dismissDescription, int colorPrimary, boolean isHistorical, - Rect bounds, ActivityManager.TaskDescription taskDescription) { + boolean isLaunchTarget, Rect bounds, + ActivityManager.TaskDescription taskDescription) { boolean isInAffiliationGroup = (affiliationTaskId != key.id); boolean hasAffiliationGroupColor = isInAffiliationGroup && (affiliationColor != 0); this.key = key; @@ -170,6 +171,7 @@ public class Task { Color.WHITE) > 3f; this.bounds = bounds; this.taskDescription = taskDescription; + this.isLaunchTarget = isLaunchTarget; this.isHistorical = isHistorical; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java index 327cdf87994df..c73273e6f2588 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -42,6 +42,7 @@ import com.android.systemui.recents.misc.NamedCounter; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.views.DropTarget; +import com.android.systemui.recents.views.TaskViewAnimation; import java.util.ArrayList; import java.util.Collections; @@ -226,12 +227,12 @@ public class TaskStack { * Notifies when a task has been removed from the stack. */ void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask, - Task newFrontMostTask); + Task newFrontMostTask, TaskViewAnimation animation); /** * Notifies when a task has been removed from the history. */ - void onHistoryTaskRemoved(TaskStack stack, Task removedTask); + void onHistoryTaskRemoved(TaskStack stack, Task removedTask, TaskViewAnimation animation); } /** @@ -520,21 +521,24 @@ public class TaskStack { } } - /** Removes a task */ - public void removeTask(Task t) { + /** + * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on + * how they should update themselves. + */ + public void removeTask(Task t, TaskViewAnimation animation) { if (mStackTaskList.contains(t)) { boolean wasFrontMostTask = (getStackFrontMostTask() == t); removeTaskImpl(mStackTaskList, t); Task newFrontMostTask = getStackFrontMostTask(); if (mCb != null) { // Notify that a task has been removed - mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask); + mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask, animation); } } else if (mHistoryTaskList.contains(t)) { removeTaskImpl(mHistoryTaskList, t); if (mCb != null) { // Notify that a task has been removed - mCb.onHistoryTaskRemoved(this, t); + mCb.onHistoryTaskRemoved(this, t, animation); } } mRawTaskList.remove(t); @@ -564,7 +568,8 @@ public class TaskStack { Task task = mRawTaskList.get(i); if (!newTasksMap.containsKey(task.key)) { if (notifyStackChanges) { - mCb.onStackTaskRemoved(this, task, i == (taskCount - 1), null); + mCb.onStackTaskRemoved(this, task, i == (taskCount - 1), null, + TaskViewAnimation.IMMEDIATE); } } task.setGroup(null); @@ -843,12 +848,17 @@ public class TaskStack { for (int i = 0; i < taskCount; i++) { Task t = tasks.get(i); TaskGrouping group; - int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId : - IndividualTaskIdOffset + t.key.id; - if (mAffinitiesGroups.containsKey(affiliation)) { - group = getGroupWithAffiliation(affiliation); + if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) { + int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId : + IndividualTaskIdOffset + t.key.id; + if (mAffinitiesGroups.containsKey(affiliation)) { + group = getGroupWithAffiliation(affiliation); + } else { + group = new TaskGrouping(affiliation); + addGroup(group); + } } else { - group = new TaskGrouping(affiliation); + group = new TaskGrouping(t.key.id); addGroup(group); } group.addTask(t); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java index e727652f5582a..e448101577deb 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -513,7 +513,7 @@ public class RecentsView extends FrameLayout { taskViewRect.right, taskViewRect.bottom); // Remove the task view after it is docked - mTaskStackView.updateLayout(false /* boundScroll */); + mTaskStackView.updateLayoutAlgorithm(false /* boundScroll */); stackLayout.getStackTransform(event.task, stackScroller.getStackScroll(), tmpTransform, null); tmpTransform.alpha = 0; @@ -529,11 +529,14 @@ public class RecentsView extends FrameLayout { ssp.startTaskInDockedMode(getContext(), event.taskView, event.task.key.id, dockState.createMode); - mTaskStackView.getStack().removeTask(event.task); + // Animate the stack accordingly + TaskViewAnimation stackAnim = new TaskViewAnimation( + TaskStackView.DEFAULT_SYNC_STACK_DURATION, + mFastOutSlowInInterpolator); + mTaskStackView.getStack().removeTask(event.task, stackAnim); } })); - MetricsLogger.action(mContext, MetricsLogger.ACTION_WINDOW_DOCK_DRAG_DROP); } else { diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java index 2930f4d5c08f6..ccbb329bb151b 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java @@ -130,6 +130,7 @@ public class TaskStackAnimationHelper { // Move the task view slightly lower so we can animate it in RectF bounds = new RectF(mTmpTransform.rect); bounds.offset(0, taskViewAffiliateGroupEnterOffset); + tv.setClipViewInStack(false); tv.setAlpha(0f); tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom); @@ -165,6 +166,8 @@ public class TaskStackAnimationHelper { int taskViewEnterFromAppDuration = res.getInteger( R.integer.recents_task_enter_from_app_duration); + int taskViewEnterFromAffiliatedAppDuration = res.getInteger( + R.integer.recents_task_enter_from_affiliated_app_duration); int taskViewEnterFromHomeDuration = res.getInteger( R.integer.recents_task_enter_from_home_duration); int taskViewEnterFromHomeStaggerDelay = res.getInteger( @@ -174,7 +177,7 @@ public class TaskStackAnimationHelper { List taskViews = mStackView.getTaskViews(); int taskViewCount = taskViews.size(); for (int i = taskViewCount - 1; i >= 0; i--) { - TaskView tv = taskViews.get(i); + final TaskView tv = taskViews.get(i); Task task = tv.getTask(); boolean currentTaskOccludesLaunchTarget = false; if (launchTargetTask != null) { @@ -195,8 +198,14 @@ public class TaskStackAnimationHelper { // Animate the task up if it was occluding the launch target if (currentTaskOccludesLaunchTarget) { TaskViewAnimation taskAnimation = new TaskViewAnimation( - taskViewEnterFromAppDuration, PhoneStatusBar.ALPHA_IN, - postAnimationTrigger.decrementOnAnimationEnd()); + taskViewEnterFromAffiliatedAppDuration, PhoneStatusBar.ALPHA_IN, + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + postAnimationTrigger.decrement(); + tv.setClipViewInStack(false); + } + }); postAnimationTrigger.increment(); mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation); } @@ -286,7 +295,7 @@ public class TaskStackAnimationHelper { } else if (currentTaskOccludesLaunchTarget) { // Animate this task out of view TaskViewAnimation taskAnimation = new TaskViewAnimation( - taskViewExitToAppDuration, mFastOutLinearInInterpolator, + taskViewExitToAppDuration, PhoneStatusBar.ALPHA_OUT, postAnimationTrigger.decrementOnAnimationEnd()); postAnimationTrigger.increment(); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java index 68ff63c1507c9..e99509cb83650 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java @@ -373,7 +373,7 @@ public class TaskStackLayoutAlgorithm { * Computes the minimum and maximum scroll progress values and the progress values for each task * in the stack. */ - void update(TaskStack stack, ArraySet ignoreTasksSet) { + void update(TaskStack stack, ArraySet ignoreTasksSet) { SystemServicesProxy ssp = Recents.getSystemServices(); // Clear the progress map @@ -393,7 +393,7 @@ public class TaskStackLayoutAlgorithm { ArrayList stackTasks = new ArrayList<>(); for (int i = 0; i < tasks.size(); i++) { Task task = tasks.get(i); - if (ignoreTasksSet.contains(task)) { + if (ignoreTasksSet.contains(task.key)) { continue; } if (task.isFreeformTask()) { @@ -434,19 +434,25 @@ public class TaskStackLayoutAlgorithm { mFreeformLayoutAlgorithm.update(freeformTasks, this); mInitialScrollP = mMaxScrollP; } else { + Task launchTask = stack.getLaunchTarget(); + int launchTaskIndex = launchTask != null + ? stack.indexOfStackTask(launchTask) + : mNumStackTasks - 1; if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) { mInitialScrollP = mMinScrollP; } else if (getDefaultFocusState() > 0f) { RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); if (launchState.launchedFromHome) { - mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 1); + mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, launchTaskIndex)); } else { - mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 2); + mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, + launchTaskIndex - 1)); } } else { float offsetPct = (float) (mTaskRect.height() / 2) / mStackRect.height(); float normX = mUnfocusedCurveInterpolator.getX(offsetPct); - mInitialScrollP = (mNumStackTasks - 1) - mUnfocusedRange.getAbsoluteX(normX); + mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, + launchTaskIndex - mUnfocusedRange.getAbsoluteX(normX))); } } } @@ -553,7 +559,8 @@ public class TaskStackLayoutAlgorithm { boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task); if (isFrontMostTaskInGroup) { - getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null); + getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null, + false /* ignoreSingleTaskCase */); float screenY = tmpTransform.rect.top; boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight; if (hasVisibleThumbnail) { @@ -596,13 +603,21 @@ public class TaskStackLayoutAlgorithm { return transformOut; } return getStackTransform(mTaskIndexMap.get(task.key), stackScroll, transformOut, - frontTransform); + frontTransform, false /* ignoreSingleTaskCase */); } } - /** Update/get the transform */ + /** + * Update/get the transform. + * + * @param ignoreSingleTaskCase When set, will ensure that the transform computed does not take + * into account the special single-task case. This is only used + * internally to ensure that we can calculate the transform for any + * position in the stack. + */ public TaskViewTransform getStackTransform(float taskProgress, float stackScroll, - TaskViewTransform transformOut, TaskViewTransform frontTransform) { + TaskViewTransform transformOut, TaskViewTransform frontTransform, + boolean ignoreSingleTaskCase) { SystemServicesProxy ssp = Recents.getSystemServices(); // Compute the focused and unfocused offset @@ -632,7 +647,7 @@ public class TaskStackLayoutAlgorithm { int y; float z; float relP; - if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) { + if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) { // When there is exactly one task, then decouple the task from the stack and just move // in screen space p = (mMinScrollP - stackScroll) / mNumStackTasks; @@ -762,8 +777,8 @@ public class TaskStackLayoutAlgorithm { mFocusState * (mFocusedRange.relativeMin - mUnfocusedRange.relativeMin); float max = mUnfocusedRange.relativeMax + mFocusState * (mFocusedRange.relativeMax - mUnfocusedRange.relativeMax); - getStackTransform(min, 0f, mBackOfStackTransform, null); - getStackTransform(max, 0f, mFrontOfStackTransform, null); + getStackTransform(min, 0f, mBackOfStackTransform, null, true /* ignoreSingleTaskCase */); + getStackTransform(max, 0f, mFrontOfStackTransform, null, true /* ignoreSingleTaskCase */); mBackOfStackTransform.visible = true; mFrontOfStackTransform.visible = true; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java index 7583a19308f29..809d4eed7a116 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -30,6 +30,7 @@ import android.os.Parcelable; import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.MutableBoolean; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -77,7 +78,6 @@ import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; @@ -100,10 +100,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal private static final float SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f; private static final float HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f; - private static final int DEFAULT_SYNC_STACK_DURATION = 200; + public static final int DEFAULT_SYNC_STACK_DURATION = 200; private static final int DRAG_SCALE_DURATION = 175; private static final float DRAG_SCALE_FACTOR = 1.05f; + private static final ArraySet EMPTY_TASK_SET = new ArraySet<>(); + TaskStack mStack; TaskStackLayoutAlgorithm mLayoutAlgorithm; TaskStackViewScroller mStackScroller; @@ -116,7 +118,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal ArrayList mTaskViews = new ArrayList<>(); ArrayList mCurrentTaskTransforms = new ArrayList<>(); - TaskViewAnimation mDeferredTaskViewUpdateAnimation = null; + ArraySet mIgnoreTasks = new ArraySet<>(); + TaskViewAnimation mDeferredTaskViewLayoutAnimation = null; DozeTrigger mUIDozeTrigger; Task mFocusedTask; @@ -137,7 +140,6 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal int[] mTmpVisibleRange = new int[2]; Rect mTmpRect = new Rect(); ArrayMap mTmpTaskViewMap = new ArrayMap<>(); - ArraySet mTmpTaskSet = new ArraySet<>(); List mTmpTaskViews = new ArrayList<>(); TaskViewTransform mTmpTransform = new TaskViewTransform(); LayoutInflater mInflater; @@ -345,30 +347,82 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** - * Gets the stack transforms of a list of tasks, and returns the visible range of tasks. - * This call ignores freeform tasks. + * Adds a task to the ignored set. */ - private boolean updateStackTransforms(ArrayList taskTransforms, - ArrayList tasks, float stackScroll, - int[] visibleRangeOut, ArraySet ignoreTasksSet) { - int taskTransformCount = taskTransforms.size(); + void addIgnoreTask(Task task) { + mIgnoreTasks.add(task.key); + } + + /** + * Removes a task from the ignored set. + */ + void removeIgnoreTask(Task task) { + mIgnoreTasks.remove(task.key); + } + + /** + * Returns whether the specified {@param task} is ignored. + */ + boolean isIgnoredTask(Task task) { + return mIgnoreTasks.contains(task.key); + } + + /** + * Computes the task transforms at the current stack scroll for all visible tasks. If a valid + * target stack scroll is provided (ie. is different than {@param curStackScroll}), then the + * visible range includes all tasks at the target stack scroll. This is useful for ensure that + * all views necessary for a transition or animation will be visible at the start. + * + * This call ignores freeform tasks. + * + * @param taskTransforms The set of task view transforms to reuse, this list will be sized to + * match the size of {@param tasks} + * @param tasks The set of tasks for which to generate transforms + * @param curStackScroll The current stack scroll + * @param targetStackScroll The stack scroll that we anticipate we are going to be scrolling to. + * The range of the union of the visible views at the current and + * target stack scrolls will be returned. + * @param ignoreTasksSet The set of tasks to skip for purposes of calculaing the visible range. + * Transforms will still be calculated for the ignore tasks. + */ + boolean computeVisibleTaskTransforms(ArrayList taskTransforms, + ArrayList tasks, float curStackScroll, float targetStackScroll, + int[] visibleRangeOut, ArraySet ignoreTasksSet) { int taskCount = tasks.size(); int frontMostVisibleIndex = -1; int backMostVisibleIndex = -1; + boolean useTargetStackScroll = Float.compare(curStackScroll, targetStackScroll) != 0; // We can reuse the task transforms where possible to reduce object allocation Utilities.matchTaskListSize(tasks, taskTransforms); // Update the stack transforms TaskViewTransform frontTransform = null; + TaskViewTransform frontTransformAtTarget = null; + TaskViewTransform transform = null; + TaskViewTransform transformAtTarget = null; for (int i = taskCount - 1; i >= 0; i--) { Task task = tasks.get(i); - if (ignoreTasksSet.contains(task)) { - continue; + + // Calculate the current and (if necessary) the target transform for the task + transform = mLayoutAlgorithm.getStackTransform(task, curStackScroll, + taskTransforms.get(i), frontTransform); + if (useTargetStackScroll && !transform.visible) { + // If we have a target stack scroll and the task is not currently visible, then we + // just update the transform at the new scroll + // TODO: Optimize this + transformAtTarget = mLayoutAlgorithm.getStackTransform(task, + targetStackScroll, new TaskViewTransform(), frontTransformAtTarget); + if (transformAtTarget.visible) { + transform.copyFrom(transformAtTarget); + } } - TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll, - taskTransforms.get(i), frontTransform); + // For ignore tasks, only calculate the stack transform and skip the calculation of the + // visible stack indices + if (ignoreTasksSet.contains(task.key)) { + continue; + } // For freeform tasks, only calculate the stack transform and skip the calculation of // the visible stack indices @@ -392,7 +446,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal break; } } + frontTransform = transform; + frontTransformAtTarget = transformAtTarget; } if (visibleRangeOut != null) { visibleRangeOut[0] = frontMostVisibleIndex; @@ -402,33 +458,48 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** - * Updates the children {@link TaskView}s to match the tasks in the current {@link TaskStack}. - * This call does not update the {@link TaskView}s to their position in the layout except when - * they are initially picked up from the pool, when they will be placed in a suitable initial - * position. + * Binds the visible {@link TaskView}s at the given target scroll. + * + * @see #bindVisibleTaskViews(float, ArraySet) */ - private void bindTaskViewsWithStack(ArraySet ignoreTasksSet) { - final float stackScroll = mStackScroller.getStackScroll(); + void bindVisibleTaskViews(float targetStackScroll) { + bindVisibleTaskViews(targetStackScroll, mIgnoreTasks); + } + + /** + * Synchronizes the set of children {@link TaskView}s to match the visible set of tasks in the + * current {@link TaskStack}. This call does not continue on to update their position to the + * computed {@link TaskViewTransform}s of the visible range, but only ensures that they will + * be added/removed from the view hierarchy and placed in the correct Z order and initial + * position (if not currently on screen). + * + * @param targetStackScroll If provided, will ensure that the set of visible {@link TaskView}s + * includes those visible at the current stack scroll, and all at the + * target stack scroll. + * @param ignoreTasksSet The set of tasks to ignore in this rebinding of the visible + * {@link TaskView}s + */ + void bindVisibleTaskViews(float targetStackScroll, ArraySet ignoreTasksSet) { final int[] visibleStackRange = mTmpVisibleRange; // Get all the task transforms final ArrayList tasks = mStack.getStackTasks(); - final boolean isValidVisibleStackRange = updateStackTransforms(mCurrentTaskTransforms, - tasks, stackScroll, visibleStackRange, ignoreTasksSet); + final boolean isValidVisibleRange = computeVisibleTaskTransforms(mCurrentTaskTransforms, + tasks, mStackScroller.getStackScroll(), targetStackScroll, visibleStackRange, + ignoreTasksSet); // Return all the invisible children to the pool - final List taskViews = getTaskViews(); - final int taskViewCount = taskViews.size(); - int lastFocusedTaskIndex = -1; mTmpTaskViewMap.clear(); - mTmpTaskViewMap.ensureCapacity(tasks.size()); + List taskViews = getTaskViews(); + int lastFocusedTaskIndex = -1; + int taskViewCount = taskViews.size(); for (int i = taskViewCount - 1; i >= 0; i--) { - final TaskView tv = taskViews.get(i); - final Task task = tv.getTask(); - final int taskIndex = mStack.indexOfStackTask(task); + TaskView tv = taskViews.get(i); + Task task = tv.getTask(); + int taskIndex = mStack.indexOfStackTask(task); // Skip ignored tasks - if (ignoreTasksSet.contains(task)) { + if (ignoreTasksSet.contains(task.key)) { continue; } @@ -445,13 +516,13 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } // Pick up all the newly visible children - int lastVisStackIndex = isValidVisibleStackRange ? visibleStackRange[1] : 0; - for (int i = mStack.getTaskCount() - 1; i >= lastVisStackIndex; i--) { - final Task task = tasks.get(i); - final TaskViewTransform transform = mCurrentTaskTransforms.get(i); + int lastVisStackIndex = isValidVisibleRange ? visibleStackRange[1] : 0; + for (int i = tasks.size() - 1; i >= lastVisStackIndex; i--) { + Task task = tasks.get(i); + TaskViewTransform transform = mCurrentTaskTransforms.get(i); // Skip ignored tasks - if (ignoreTasksSet.contains(task)) { + if (ignoreTasksSet.contains(task.key)) { continue; } @@ -502,26 +573,34 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** - * Cancels any existing {@link TaskView} animations, and updates each {@link TaskView} to its - * current position as defined by the {@link TaskStackLayoutAlgorithm}. + * Relayout the the visible {@link TaskView}s to their current transforms as specified by the + * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any + * animations that are current running on those task views, and will ensure that the children + * {@link TaskView}s will match the set of visible tasks in the stack. * - * @param ignoreTasks the set of tasks to ignore in the relayout + * @see #relayoutTaskViews(TaskViewAnimation, ArraySet) */ - private void updateTaskViewsToLayout(TaskViewAnimation animation, Task... ignoreTasks) { - // Keep track of the ignore tasks - ArraySet ignoreTasksSet = mTmpTaskSet; - ignoreTasksSet.clear(); - ignoreTasksSet.ensureCapacity(ignoreTasks.length); - Collections.addAll(ignoreTasksSet, ignoreTasks); + void relayoutTaskViews(TaskViewAnimation animation) { + relayoutTaskViews(animation, mIgnoreTasks); + } + /** + * Relayout the the visible {@link TaskView}s to their current transforms as specified by the + * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any + * animations that are current running on those task views, and will ensure that the children + * {@link TaskView}s will match the set of visible tasks in the stack. + * + * @param ignoreTasksSet the set of tasks to ignore in the relayout + */ + void relayoutTaskViews(TaskViewAnimation animation, ArraySet ignoreTasksSet) { // If we had a deferred animation, cancel that - mDeferredTaskViewUpdateAnimation = null; + mDeferredTaskViewLayoutAnimation = null; // Cancel all task view animations cancelAllTaskViewAnimations(); - // Fetch the current set of TaskViews - bindTaskViewsWithStack(ignoreTasksSet); + // Synchronize the current set of TaskViews + bindVisibleTaskViews(mStackScroller.getStackScroll(), ignoreTasksSet); // Animate them to their final transforms with the given animation List taskViews = getTaskViews(); @@ -531,7 +610,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal final int taskIndex = mStack.indexOfStackTask(tv.getTask()); final TaskViewTransform transform = mCurrentTaskTransforms.get(taskIndex); - if (ignoreTasksSet.contains(tv.getTask())) { + if (ignoreTasksSet.contains(tv.getTask().key)) { continue; } @@ -542,8 +621,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal /** * Posts an update to synchronize the {@link TaskView}s with the stack on the next frame. */ - private void updateTaskViewsToLayoutOnNextFrame(TaskViewAnimation animation) { - mDeferredTaskViewUpdateAnimation = animation; + void relayoutTaskViewsOnNextFrame(TaskViewAnimation animation) { + mDeferredTaskViewLayoutAnimation = animation; postInvalidateOnAnimation(); } @@ -558,13 +637,62 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** - * Cancels all {@link TaskView} animations. + * Returns the current task transforms of all tasks, falling back to the stack layout if there + * is no {@link TaskView} for the task. */ - private void cancelAllTaskViewAnimations() { + public void getCurrentTaskTransforms(ArrayList tasks, + ArrayList transformsOut) { + Utilities.matchTaskListSize(tasks, transformsOut); + for (int i = tasks.size() - 1; i >= 0; i--) { + Task task = tasks.get(i); + TaskViewTransform transform = transformsOut.get(i); + TaskView tv = getChildViewForTask(task); + if (tv != null) { + transform.fillIn(tv); + } else { + mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(), + transform, null); + } + transform.visible = true; + } + } + + /** + * Returns the task transforms for all the tasks in the stack if the stack was at the given + * {@param stackScroll}. + */ + public void getLayoutTaskTransforms(float stackScroll, ArrayList tasks, + ArrayList transformsOut) { + Utilities.matchTaskListSize(tasks, transformsOut); + for (int i = tasks.size() - 1; i >= 0; i--) { + Task task = tasks.get(i); + TaskViewTransform transform = transformsOut.get(i); + mLayoutAlgorithm.getStackTransform(task, stackScroll, transform, null); + transform.visible = true; + } + } + + /** + * Cancels all {@link TaskView} animations. + * + * @see #cancelAllTaskViewAnimations(ArraySet) + */ + void cancelAllTaskViewAnimations() { + cancelAllTaskViewAnimations(mIgnoreTasks); + } + + /** + * Cancels all {@link TaskView} animations. + * + * @param ignoreTasksSet The set of tasks to continue running their animations. + */ + void cancelAllTaskViewAnimations(ArraySet ignoreTasksSet) { List taskViews = getTaskViews(); for (int i = taskViews.size() - 1; i >= 0; i--) { final TaskView tv = taskViews.get(i); - tv.cancelTransformAnimation(); + if (!ignoreTasksSet.contains(tv.getTask().key)) { + tv.cancelTransformAnimation(); + } } } @@ -577,11 +705,22 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Update the clip on each task child List taskViews = getTaskViews(); TaskView tmpTv = null; + TaskView prevVisibleTv = null; int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { TaskView tv = taskViews.get(i); TaskView frontTv = null; int clipBottom = 0; + + if (mIgnoreTasks.contains(tv.getTask().key)) { + // For each of the ignore tasks, update the translationZ of its TaskView to be + // between the translationZ of the tasks immediately underneath it + if (prevVisibleTv != null) { + tv.setTranslationZ(Math.max(tv.getTranslationZ(), + prevVisibleTv.getTranslationZ() + 0.1f)); + } + } + if (i < (taskViewCount - 1) && tv.shouldClipViewInStack()) { // Find the next view to clip against for (int j = i + 1; j < taskViewCount; j++) { @@ -609,33 +748,37 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal if (!config.useHardwareLayers) { tv.mThumbnailView.updateThumbnailVisibility(clipBottom - tv.getPaddingBottom()); } + prevVisibleTv = tv; } mTaskViewsClipDirty = false; } + /** + * Updates the layout algorithm min and max virtual scroll bounds. + * + * @see #updateLayoutAlgorithm(boolean, ArraySet) + */ + void updateLayoutAlgorithm(boolean boundScrollToNewMinMax) { + updateLayoutAlgorithm(boundScrollToNewMinMax, mIgnoreTasks); + } + /** * Updates the min and max virtual scroll bounds. * - * @param ignoreTasks the set of tasks to ignore in the relayout + * @param ignoreTasksSet the set of tasks to ignore in the relayout */ - void updateLayout(boolean boundScrollToNewMinMax, Task... ignoreTasks) { - // Keep track of the ingore tasks - ArraySet ignoreTasksSet = mTmpTaskSet; - ignoreTasksSet.clear(); - ignoreTasksSet.ensureCapacity(ignoreTasks.length); - Collections.addAll(ignoreTasksSet, ignoreTasks); - + void updateLayoutAlgorithm(boolean boundScrollToNewMinMax, + ArraySet ignoreTasksSet) { // Compute the min and max scroll values mLayoutAlgorithm.update(mStack, ignoreTasksSet); - // Update the freeform workspace + // Update the freeform workspace background SystemServicesProxy ssp = Recents.getSystemServices(); if (ssp.hasFreeformWorkspaceSupport()) { mTmpRect.set(mLayoutAlgorithm.mFreeformRect); mFreeformWorkspaceBackground.setBounds(mTmpRect); } - // Debug logging if (boundScrollToNewMinMax) { mStackScroller.boundScroll(); } @@ -671,8 +814,6 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Reset the last focused task state if changed if (mFocusedTask != null) { - resetFocusedTask(mFocusedTask); - // Cancel the timer indicator, if applicable if (showTimerIndicator) { final TaskView tv = getChildViewForTask(mFocusedTask); @@ -680,6 +821,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal tv.getHeaderView().cancelFocusTimerIndicator(); } } + + resetFocusedTask(mFocusedTask); } boolean willScroll = false; @@ -937,41 +1080,27 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Notify accessibility sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED); } - if (mDeferredTaskViewUpdateAnimation != null) { - updateTaskViewsToLayout(mDeferredTaskViewUpdateAnimation); + if (mDeferredTaskViewLayoutAnimation != null) { + relayoutTaskViews(mDeferredTaskViewLayoutAnimation); mTaskViewsClipDirty = true; - mDeferredTaskViewUpdateAnimation = null; + mDeferredTaskViewLayoutAnimation = null; } if (mTaskViewsClipDirty) { clipTaskViews(); } } - /** - * Computes the stack and task rects. - * - * @param ignoreTasks the set of tasks to ignore in the relayout - */ - public void computeRects(Rect taskStackBounds, boolean boundScroll, Task... ignoreTasks) { - // Compute the rects in the stack algorithm - mLayoutAlgorithm.initialize(taskStackBounds, - TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); - - // Update the scroll bounds - updateLayout(boundScroll, ignoreTasks); - } - /** * This is ONLY used from the Recents component to update the dummy stack view for purposes * of getting the task rect to animate to. */ public void updateLayoutForStack(TaskStack stack) { mStack = stack; - updateLayout(false); + updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET); } /** - * Computes the maximum number of visible tasks and thumbnails. Requires that + * Computes the maximum number of visible tasks and thumbnails. Requires that * updateLayoutForStack() is called first. */ public TaskStackLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() { @@ -1002,16 +1131,18 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); - // Compute our stack/task rects - computeRects(mStackBounds, false); + // Compute the rects in the stack algorithm + mLayoutAlgorithm.initialize(mStackBounds, + TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); + updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET); // If this is the first layout, then scroll to the front of the stack, then update the // TaskViews with the stack so that we can lay them out if (mAwaitingFirstLayout) { mStackScroller.setStackScrollToInitialState(); } - mTmpTaskSet.clear(); - bindTaskViewsWithStack(mTmpTaskSet); + // Rebind all the views, including the ignore ones + bindVisibleTaskViews(mStackScroller.getStackScroll(), EMPTY_TASK_SET); // Measure each of the TaskViews mTmpTaskViews.clear(); @@ -1066,7 +1197,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mStackScroller.boundScroll(); } } - updateTaskViewsToLayout(TaskViewAnimation.IMMEDIATE); + // Relayout all of the task views including the ignored ones + relayoutTaskViews(TaskViewAnimation.IMMEDIATE, EMPTY_TASK_SET); clipTaskViews(); if (mAwaitingFirstLayout || !mEnterAnimationComplete) { @@ -1088,9 +1220,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Set the task focused state without requesting view focus, and leave the focus animations // until after the enter-animation + Task launchTask = mStack.getLaunchTarget(); RecentsConfiguration config = Recents.getConfiguration(); RecentsActivityLaunchState launchState = config.getLaunchState(); - int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount()); + int focusedTaskIndex = launchTask != null + ? mStack.indexOfStackTask(launchTask) + : launchState.getInitialFocusTaskIndex(mStack.getTaskCount()); if (focusedTaskIndex != -1) { setFocusedTask(focusedTaskIndex, false /* scrollToTask */, false /* requestViewFocus */); @@ -1106,8 +1241,29 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } public boolean isTouchPointInView(float x, float y, TaskView tv) { - return (tv.getLeft() <= x && x <= tv.getRight()) && - (tv.getTop() <= y && y <= tv.getBottom()); + mTmpRect.set(tv.getLeft(), tv.getTop(), tv.getRight(), tv.getBottom()); + mTmpRect.offset((int) tv.getTranslationX(), (int) tv.getTranslationY()); + return mTmpRect.contains((int) x, (int) y); + } + + /** + * Returns a non-ignored task in the {@param tasks} list that can be used as an achor when + * calculating the scroll position before and after a layout change. + */ + public Task findAnchorTask(List tasks, MutableBoolean isFrontMostTask) { + for (int i = tasks.size() - 1; i >= 0; i--) { + Task task = tasks.get(i); + + // Ignore deleting tasks + if (mIgnoreTasks.contains(task.key)) { + if (i == tasks.size() - 1) { + isFrontMostTask.value = true; + } + continue; + } + return task; + } + return null; } @Override @@ -1152,70 +1308,38 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal @Override public void onStackTaskAdded(TaskStack stack, Task newTask) { // Update the min/max scroll and animate other task views into their new positions - updateLayout(true); + updateLayoutAlgorithm(true /* boundScroll */); // Animate all the tasks into place - updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, + relayoutTaskViews(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, mFastOutSlowInInterpolator)); } + /** + * We expect that the {@link TaskView} associated with the removed task is already hidden. + */ @Override public void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask, - Task newFrontMostTask) { + Task newFrontMostTask, TaskViewAnimation animation) { if (mFocusedTask == removedTask) { resetFocusedTask(removedTask); } - if (!removedTask.isFreeformTask()) { - // Remove the view associated with this task, we can't rely on updateTransforms - // to work here because the task is no longer in the list - TaskView tv = getChildViewForTask(removedTask); - if (tv != null) { - mViewPool.returnViewToPool(tv); - } - - // Get the stack scroll of the task to anchor to (since we are removing something, the - // front most task will be our anchor task) - Task anchorTask = mStack.getStackFrontMostTask(); - float prevAnchorTaskScroll = 0; - if (anchorTask != null) { - prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); - } - - // Update the min/max scroll and animate other task views into their new positions - updateLayout(true); - - if (wasFrontMostTask) { - // Since the max scroll progress is offset from the bottom of the stack, just scroll - // to ensure that the new front most task is now fully visible - mStackScroller.setStackScroll(mLayoutAlgorithm.mMaxScrollP); - } else if (anchorTask != null) { - // Otherwise, offset the scroll by the movement of the anchor task - float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); - float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll); - if (mLayoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED) { - // If we are focused, we don't want the front task to move, but otherwise, we - // allow the back task to move up, and the front task to move back - stackScrollOffset /= 2; - } - mStackScroller.setStackScroll(mStackScroller.getStackScroll() + stackScrollOffset); - mStackScroller.boundScroll(); - } - } else { - // Remove the view associated with this task, we can't rely on updateTransforms - // to work here because the task is no longer in the list - TaskView tv = getChildViewForTask(removedTask); - if (tv != null) { - mViewPool.returnViewToPool(tv); - } - - // Update the min/max scroll and animate other task views into their new positions - updateLayout(true); + // Remove the view associated with this task, we can't rely on updateTransforms + // to work here because the task is no longer in the list + TaskView tv = getChildViewForTask(removedTask); + if (tv != null) { + mViewPool.returnViewToPool(tv); } - // Animate all the tasks into place - updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, - mFastOutSlowInInterpolator)); + // Remove the task from the ignored set + removeIgnoreTask(removedTask); + + // If requested, relayout with the given animation + if (animation != null) { + updateLayoutAlgorithm(true /* boundScroll */); + relayoutTaskViews(animation); + } // Update the new front most task's action button if (mScreenPinningEnabled && newFrontMostTask != null) { @@ -1232,7 +1356,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } @Override - public void onHistoryTaskRemoved(TaskStack stack, Task removedTask) { + public void onHistoryTaskRemoved(TaskStack stack, Task removedTask, + TaskViewAnimation animation) { // To be implemented } @@ -1316,7 +1441,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal @Override public void onTaskViewClipStateChanged(TaskView tv) { - clipTaskViews(); + if (!mTaskViewsClipDirty) { + mTaskViewsClipDirty = true; + invalidate(); + } } /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/ @@ -1324,7 +1452,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal @Override public void onScrollChanged(float prevScroll, float curScroll, TaskViewAnimation animation) { mUIDozeTrigger.poke(); - updateTaskViewsToLayoutOnNextFrame(animation); + if (animation != null) { + relayoutTaskViewsOnNextFrame(animation); + } if (shouldShowHistoryButton() && prevScroll > SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD && @@ -1354,7 +1484,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal tv.dismissTask(); } else { // Otherwise, remove the task from the stack immediately - mStack.removeTask(t); + mStack.removeTask(t, TaskViewAnimation.IMMEDIATE); } } } @@ -1402,7 +1532,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } public final void onBusEvent(TaskViewDismissedEvent event) { - removeTaskViewFromStack(event.taskView); + removeTaskViewFromStack(event.taskView, event.task); EventBus.getDefault().send(new DeleteTaskDataEvent(event.task)); } @@ -1419,7 +1549,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Poke the doze trigger on user interaction mUIDozeTrigger.poke(); if (event.showTimerIndicator && mFocusedTask != null) { - getChildViewForTask(mFocusedTask).getHeaderView().cancelFocusTimerIndicator(); + TaskView tv = getChildViewForTask(mFocusedTask); + if (tv != null) { + tv.getHeaderView().cancelFocusTimerIndicator(); + } } } @@ -1455,23 +1588,29 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } public final void onBusEvent(DragDropTargetChangedEvent event) { + TaskViewAnimation animation = new TaskViewAnimation(250, mFastOutSlowInInterpolator); if (event.dropTarget instanceof TaskStack.DockState) { // Calculate the new task stack bounds that matches the window size that Recents will // have after the drop + addIgnoreTask(event.task); final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget; mStackBounds.set(dockState.getDockedTaskStackBounds(getMeasuredWidth(), getMeasuredHeight(), mDividerSize, mLayoutAlgorithm.mSystemInsets, getResources())); - computeRects(mStackBounds, true /* boundScroll */, event.task /* ignoreTask */); - updateTaskViewsToLayout(new TaskViewAnimation(250, mFastOutSlowInInterpolator), - event.task /* ignoreTask */); + mLayoutAlgorithm.initialize(mStackBounds, + TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); + updateLayoutAlgorithm(true /* boundScroll */); } else { - // Restore the pre-drag task stack bounds + // Restore the pre-drag task stack bounds, but ensure that we don't layout the dragging + // task view, so add it back to the ignore set after updating the layout mStackBounds.set(mStableStackBounds); - computeRects(mStackBounds, true /* boundScroll */); - updateTaskViewsToLayout(new TaskViewAnimation(250, mFastOutSlowInInterpolator), - event.task /* ignoreTask */); + removeIgnoreTask(event.task); + mLayoutAlgorithm.initialize(mStackBounds, + TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack)); + updateLayoutAlgorithm(true /* boundScroll */); + addIgnoreTask(event.task); } + relayoutTaskViews(animation); } public final void onBusEvent(final DragEndEvent event) { @@ -1487,14 +1626,14 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal if (hasChangedStacks) { // Move the task to the right position in the stack (ie. the front of the stack if - // freeform or the front of the stack if fullscreen). Note, we MUST move the tasks + // freeform or the front of the stack if fullscreen). Note, we MUST move the tasks // before we update their stack ids, otherwise, the keys will have changed. if (event.dropTarget == mFreeformWorkspaceDropTarget) { mStack.moveTaskToStack(event.task, FREEFORM_WORKSPACE_STACK_ID); } else if (event.dropTarget == mStackDropTarget) { mStack.moveTaskToStack(event.task, FULLSCREEN_WORKSPACE_STACK_ID); } - updateLayout(true); + updateLayoutAlgorithm(true /* boundScroll */); // Move the task to the new stack in the system after the animation completes event.addPostAnimationCallback(new Runnable() { @@ -1522,11 +1661,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(), mTmpTransform, null); event.getAnimationTrigger().increment(); - updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, + relayoutTaskViews(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, mFastOutSlowInInterpolator)); updateTaskViewToTransform(event.taskView, mTmpTransform, new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, mFastOutSlowInInterpolator, event.getAnimationTrigger().decrementOnAnimationEnd())); + removeIgnoreTask(event.task); } public final void onBusEvent(StackViewScrolledEvent event) { @@ -1593,15 +1733,14 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal * Removes the task from the stack, and updates the focus to the next task in the stack if the * removed TaskView was focused. */ - private void removeTaskViewFromStack(TaskView tv) { - Task task = tv.getTask(); - + private void removeTaskViewFromStack(TaskView tv, Task task) { // Announce for accessibility tv.announceForAccessibility(getContext().getString( - R.string.accessibility_recents_item_dismissed, tv.getTask().title)); + R.string.accessibility_recents_item_dismissed, task.title)); // Remove the task from the stack - mStack.removeTask(task); + mStack.removeTask(task, new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, + mFastOutSlowInInterpolator)); } /** @@ -1622,7 +1761,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** - * Returns the insert index for the task in the current set of task views. If the given task + * Returns the insert index for the task in the current set of task views. If the given task * is already in the task view list, then this method returns the insert index assuming it * is first removed at the previous index. * diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java index 32f02acd9d778..5335b14078dd6 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java @@ -151,6 +151,7 @@ public class TaskStackViewScroller { /** Animates the stack scroll into bounds */ ObjectAnimator animateBoundScroll() { + // TODO: Take duration for snap back float curScroll = getStackScroll(); float newScroll = getBoundedStackScroll(curScroll); if (Float.compare(newScroll, curScroll) != 0) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java index 4813c19894e36..e9f6f398a286b 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java @@ -21,13 +21,16 @@ import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; -import android.util.Log; +import android.util.ArrayMap; +import android.util.MutableBoolean; import android.view.InputDevice; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.SwipeHelper; @@ -37,19 +40,25 @@ import com.android.systemui.recents.events.EventBus; import com.android.systemui.recents.events.activity.HideRecentsEvent; import com.android.systemui.recents.events.ui.StackViewScrolledEvent; import com.android.systemui.recents.events.ui.TaskViewDismissedEvent; +import com.android.systemui.recents.misc.RectFEvaluator; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; +import com.android.systemui.recents.model.Task; import com.android.systemui.statusbar.FlingAnimationUtils; +import java.util.ArrayList; import java.util.List; -/* Handles touch events for a TaskStackView. */ +/** + * Handles touch events for a TaskStackView. + */ class TaskStackViewTouchHandler implements SwipeHelper.Callback { - private static final String TAG = "TaskStackViewTouchHandler"; - private static final boolean DEBUG = false; + private static final int INACTIVE_POINTER_ID = -1; - private static int INACTIVE_POINTER_ID = -1; + private static final RectFEvaluator RECT_EVALUATOR = new RectFEvaluator(); + private static final Interpolator STACK_TRANSFORM_INTERPOLATOR = + new PathInterpolator(0.73f, 0.33f, 0.42f, 0.85f); Context mContext; TaskStackView mSv; @@ -74,6 +83,15 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { final int mWindowTouchSlop; private final StackViewScrolledEvent mStackViewScrolledEvent = new StackViewScrolledEvent(); + + // The current and final set of task transforms, sized to match the list of tasks in the stack + private ArrayList mCurrentTasks = new ArrayList<>(); + private ArrayList mCurrentTaskTransforms = new ArrayList<>(); + private ArrayList mFinalTaskTransforms = new ArrayList<>(); + private ArrayMap mSwipeHelperAnimations = new ArrayMap<>(); + private TaskViewTransform mTmpTransform = new TaskViewTransform(); + private float mTargetStackScroll; + SwipeHelper mSwipeHelper; boolean mInterceptedBySwipeHelper; @@ -97,8 +115,14 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { } @Override - protected void updateSnapBackAnimation(Animator anim) { + protected void prepareDismissAnimation(View v, Animator anim) { + mSwipeHelperAnimations.put(v, anim); + } + + @Override + protected void prepareSnapBackAnimation(View v, Animator anim) { anim.setInterpolator(mSv.mFastOutSlowInInterpolator); + mSwipeHelperAnimations.put(v, anim); } }; mSwipeHelper.setDisableHardwareLayers(true); @@ -119,21 +143,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { } } - /** Returns the view at the specified coordinates */ - TaskView findViewAtPoint(int x, int y) { - List taskViews = mSv.getTaskViews(); - int taskViewCount = taskViews.size(); - for (int i = taskViewCount - 1; i >= 0; i--) { - TaskView tv = taskViews.get(i); - if (tv.getVisibility() == View.VISIBLE) { - if (mSv.isTouchPointInView(x, y, tv)) { - return tv; - } - } - } - return null; - } - /** Touch preprocessing for handling below */ public boolean onInterceptTouchEvent(MotionEvent ev) { // Pass through to swipe helper if we are swiping @@ -179,6 +188,15 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { mScroller.stopBoundScrollAnimation(); Utilities.cancelAnimationWithoutCallbacks(mScrollFlingAnimator); + // Finish any existing task animations from the delete + mSv.cancelAllTaskViewAnimations(); + // Finish any of the swipe helper animations + ArrayMap existingAnimators = new ArrayMap<>(mSwipeHelperAnimations); + for (int i = 0; i < existingAnimators.size(); i++) { + existingAnimators.get(existingAnimators.keyAt(i)).end(); + } + mSwipeHelperAnimations.clear(); + // Initialize the velocity tracker initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); @@ -214,9 +232,6 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y); float curScrollP = mDownScrollP + deltaP; mScroller.setStackScroll(curScrollP); - if (DEBUG) { - Log.d(TAG, "scroll: " + curScrollP); - } mStackViewScrolledEvent.updateY(y - mLastY); EventBus.getDefault().send(mStackViewScrolledEvent); } @@ -343,12 +358,19 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { @Override public View getChildAtPosition(MotionEvent ev) { - return findViewAtPoint((int) ev.getX(), (int) ev.getY()); + TaskView tv = findViewAtPoint((int) ev.getX(), (int) ev.getY()); + if (tv != null && canChildBeDismissed(tv)) { + return tv; + } + return null; } @Override public boolean canChildBeDismissed(View v) { - return true; + // Disallow dismissing an already dismissed task + TaskView tv = (TaskView) v; + return !mSwipeHelperAnimations.containsKey(v) && + (mSv.getStack().indexOfStackTask(tv.getTask()) != -1); } @Override @@ -364,34 +386,113 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } + + // Add this task to the set of tasks we are deleting + mSv.addIgnoreTask(tv.getTask()); + + // Determine if we are animating the other tasks while dismissing this task + mCurrentTasks = mSv.getStack().getStackTasks(); + MutableBoolean isFrontMostTask = new MutableBoolean(false); + Task anchorTask = mSv.findAnchorTask(mCurrentTasks, isFrontMostTask); + TaskStackViewScroller stackScroller = mSv.getScroller(); + if (anchorTask != null) { + // Get the current set of task transforms + mSv.getCurrentTaskTransforms(mCurrentTasks, mCurrentTaskTransforms); + + // Get the stack scroll of the task to anchor to (since we are removing something, the + // front most task will be our anchor task) + float prevAnchorTaskScroll = 0; + boolean pullStackForward = mCurrentTasks.size() > 0; + if (pullStackForward) { + prevAnchorTaskScroll = mSv.getStackAlgorithm().getStackScrollForTask(anchorTask); + } + + // Calculate where the views would be without the deleting tasks + mSv.updateLayoutAlgorithm(false /* boundScroll */); + + float newStackScroll = stackScroller.getStackScroll(); + if (isFrontMostTask.value) { + // Bound the stack scroll to pull tasks forward if necessary + newStackScroll = stackScroller.getBoundedStackScroll(newStackScroll); + } else if (pullStackForward) { + // Otherwise, offset the scroll by the movement of the anchor task + float anchorTaskScroll = mSv.getStackAlgorithm().getStackScrollForTask(anchorTask); + float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll); + if (mSv.getStackAlgorithm().getFocusState() != + TaskStackLayoutAlgorithm.STATE_FOCUSED) { + // If we are focused, we don't want the front task to move, but otherwise, we + // allow the back task to move up, and the front task to move back + stackScrollOffset /= 2; + } + newStackScroll = stackScroller.getBoundedStackScroll(stackScroller.getStackScroll() + + stackScrollOffset); + } + + // Pick up the newly visible views, not including the deleting tasks + mSv.bindVisibleTaskViews(newStackScroll); + + // Get the final set of task transforms (with task removed) + mSv.getLayoutTaskTransforms(newStackScroll, mCurrentTasks, mFinalTaskTransforms); + + // Set the target to scroll towards upon dismissal + mTargetStackScroll = newStackScroll; + + /* + * Post condition: All views that will be visible as a part of the gesture are retrieved + * and at their initial positions. The stack is still at the current + * scroll, but the layout is updated without the task currently being + * dismissed. + */ + } } @Override public boolean updateSwipeProgress(View v, boolean dismissable, float swipeProgress) { + updateTaskViewTransforms(getDismissFraction(v)); return true; } + /** + * Called after the {@link TaskView} is finished animating away. + */ @Override public void onChildDismissed(View v) { TaskView tv = (TaskView) v; + // Re-enable clipping with the stack (we will reuse this view) tv.setClipViewInStack(true); // Re-enable touch events from this task view tv.setTouchEnabled(true); + // Update the scroll to the final scroll position from onBeginDrag() + mSv.getScroller().setStackScroll(mTargetStackScroll, null); // Remove the task view from the stack EventBus.getDefault().send(new TaskViewDismissedEvent(tv.getTask(), tv)); + // Stop tracking this deletion animation + mSwipeHelperAnimations.remove(v); // Keep track of deletions by keyboard MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source", Constants.Metrics.DismissSourceSwipeGesture); } + /** + * Called after the {@link TaskView} is finished animating back into the list. + * onChildDismissed() calls. + */ @Override public void onChildSnappedBack(View v) { TaskView tv = (TaskView) v; + // Re-enable clipping with the stack tv.setClipViewInStack(true); // Re-enable touch events from this task view tv.setTouchEnabled(true); + + // Stop tracking this deleting task, and update the layout to include this task again. The + // stack scroll does not need to be reset, since the scroll has not actually changed in + // onBeginDrag(). + mSv.removeIgnoreTask(tv.getTask()); + mSv.updateLayoutAlgorithm(false /* boundScroll */); + mSwipeHelperAnimations.remove(v); } @Override @@ -414,4 +515,59 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback { return 0; } + /** + * Interpolates the non-deleting tasks to their final transforms from their current transforms. + */ + private void updateTaskViewTransforms(float dismissFraction) { + List taskViews = mSv.getTaskViews(); + int taskViewCount = taskViews.size(); + for (int i = 0; i < taskViewCount; i++) { + TaskView tv = taskViews.get(i); + Task task = tv.getTask(); + + if (mSv.isIgnoredTask(task)) { + continue; + } + + int taskIndex = mCurrentTasks.indexOf(task); + TaskViewTransform fromTransform = mCurrentTaskTransforms.get(taskIndex); + TaskViewTransform toTransform = mFinalTaskTransforms.get(taskIndex); + + mTmpTransform.copyFrom(fromTransform); + // We only really need to interpolate the bounds, progress and translation + mTmpTransform.rect.set(RECT_EVALUATOR.evaluate(dismissFraction, fromTransform.rect, + toTransform.rect)); + mTmpTransform.p = fromTransform.p + (toTransform.p - fromTransform.p) * dismissFraction; + mTmpTransform.translationZ = fromTransform.translationZ + + (toTransform.translationZ - fromTransform.translationZ) * dismissFraction; + + mSv.updateTaskViewToTransform(tv, mTmpTransform, TaskViewAnimation.IMMEDIATE); + } + } + + /** Returns the view at the specified coordinates */ + private TaskView findViewAtPoint(int x, int y) { + List tasks = mSv.getStack().getStackTasks(); + int taskCount = tasks.size(); + for (int i = taskCount - 1; i >= 0; i--) { + TaskView tv = mSv.getChildViewForTask(tasks.get(i)); + if (tv != null && tv.getVisibility() == View.VISIBLE) { + if (mSv.isTouchPointInView(x, y, tv)) { + return tv; + } + } + } + return null; + } + + /** + * Returns the fraction which we should interpolate the other task views based on the dismissal + * of this given task. + * + * TODO: We can interpolate this to adjust when the other tasks should respond to the dismissal + */ + private float getDismissFraction(View v) { + float fraction = Math.min(1f, Math.abs(v.getTranslationX() / mSv.getWidth())); + return STACK_TRANSFORM_INTERPOLATOR.getInterpolation(fraction); + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java index 9b727020404f7..1f8216ffd750d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -162,6 +162,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, /** Resets this TaskView for reuse. */ void reset() { + mHeaderView.reset(); resetViewProperties(); resetNoUserInteractionState(); setClipViewInStack(false); @@ -459,16 +460,14 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public void showActionButton(boolean fadeIn, int fadeInDuration) { mActionButtonView.setVisibility(View.VISIBLE); - if (fadeIn) { - if (mActionButtonView.getAlpha() < 1f) { - mActionButtonView.animate() - .alpha(1f) - .scaleX(1f) - .scaleY(1f) - .setDuration(fadeInDuration) - .setInterpolator(PhoneStatusBar.ALPHA_IN) - .start(); - } + if (fadeIn && mActionButtonView.getAlpha() < 1f) { + mActionButtonView.animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(fadeInDuration) + .setInterpolator(PhoneStatusBar.ALPHA_IN) + .start(); } else { mActionButtonView.setScaleX(1f); mActionButtonView.setScaleY(1f); @@ -484,29 +483,27 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, */ public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, final Animator.AnimatorListener animListener) { - if (fadeOut) { - if (mActionButtonView.getAlpha() > 0f) { - if (scaleDown) { - float toScale = 0.9f; - mActionButtonView.animate() - .scaleX(toScale) - .scaleY(toScale); - } + if (fadeOut && mActionButtonView.getAlpha() > 0f) { + if (scaleDown) { + float toScale = 0.9f; mActionButtonView.animate() - .alpha(0f) - .setDuration(fadeOutDuration) - .setInterpolator(PhoneStatusBar.ALPHA_OUT) - .withEndAction(new Runnable() { - @Override - public void run() { - if (animListener != null) { - animListener.onAnimationEnd(null); - } - mActionButtonView.setVisibility(View.INVISIBLE); - } - }) - .start(); + .scaleX(toScale) + .scaleY(toScale); } + mActionButtonView.animate() + .alpha(0f) + .setDuration(fadeOutDuration) + .setInterpolator(PhoneStatusBar.ALPHA_OUT) + .withEndAction(new Runnable() { + @Override + public void run() { + if (animListener != null) { + animListener.onAnimationEnd(null); + } + mActionButtonView.setVisibility(View.INVISIBLE); + } + }) + .start(); } else { mActionButtonView.setAlpha(0f); mActionButtonView.setVisibility(View.INVISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java index e7717ac1c871a..2563190047f22 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java @@ -16,22 +16,27 @@ package com.android.systemui.recents.views; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.annotation.Nullable; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; -import android.graphics.PorterDuff; import android.graphics.PixelFormat; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.graphics.drawable.RippleDrawable; -import android.support.v4.graphics.ColorUtils; import android.os.CountDownTimer; +import android.support.v4.graphics.ColorUtils; import android.util.AttributeSet; import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewStub; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; @@ -39,10 +44,10 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.android.internal.logging.MetricsLogger; - import com.android.systemui.R; import com.android.systemui.recents.Constants; import com.android.systemui.recents.Recents; +import com.android.systemui.recents.RecentsDebugFlags; import com.android.systemui.recents.events.EventBus; import com.android.systemui.recents.events.activity.LaunchTaskEvent; import com.android.systemui.recents.events.ui.ShowApplicationInfoEvent; @@ -59,6 +64,8 @@ public class TaskViewHeader extends FrameLayout implements View.OnClickListener, View.OnLongClickListener { private static final float HIGHLIGHT_LIGHTNESS_INCREMENT = 0.125f; + private static final float OVERLAY_LIGHTNESS_INCREMENT = -0.0625f; + private static final int OVERLAY_REVEAL_DURATION = 250; private static final long FOCUS_INDICATOR_INTERVAL_MS = 30; /** @@ -69,8 +76,6 @@ public class TaskViewHeader extends FrameLayout private Paint mHighlightPaint = new Paint(); private Paint mBackgroundPaint = new Paint(); - private float[] mTmpHSL = new float[3]; - public HighlightColorDrawable() { mBackgroundPaint.setColor(Color.argb(255, 0, 0, 0)); mBackgroundPaint.setAntiAlias(true); @@ -122,11 +127,16 @@ public class TaskViewHeader extends FrameLayout Task mTask; // Header views - ImageView mMoveTaskButton; - ImageView mDismissButton; ImageView mIconView; TextView mTitleView; - int mMoveTaskTargetStackId = INVALID_STACK_ID; + ImageView mMoveTaskButton; + ImageView mDismissButton; + ViewStub mAppOverlayViewStub; + FrameLayout mAppOverlayView; + ImageView mAppIconView; + ImageView mAppInfoView; + TextView mAppTitleView; + ViewStub mFocusTimerIndicatorStub; ProgressBar mFocusTimerIndicator; // Header drawables @@ -140,21 +150,26 @@ public class TaskViewHeader extends FrameLayout Drawable mDarkFreeformIcon; Drawable mLightFullscreenIcon; Drawable mDarkFullscreenIcon; + Drawable mLightInfoIcon; + Drawable mDarkInfoIcon; int mTaskBarViewLightTextColor; int mTaskBarViewDarkTextColor; + int mMoveTaskTargetStackId = INVALID_STACK_ID; // Header background private HighlightColorDrawable mBackground; + private HighlightColorDrawable mOverlayBackground; + private float[] mTmpHSL = new float[3]; // Header dim, which is only used when task view hardware layers are not used private Paint mDimLayerPaint = new Paint(); Interpolator mFastOutSlowInInterpolator; Interpolator mFastOutLinearInInterpolator; + Interpolator mLinearOutSlowInInterpolator; - long mFocusIndicatorProgress; private CountDownTimer mFocusTimerCountDown; - long mFocusTimerDuration; + private long mFocusTimerDuration; public TaskViewHeader(Context context) { this(context, null); @@ -184,36 +199,45 @@ public class TaskViewHeader extends FrameLayout mDarkFreeformIcon = context.getDrawable(R.drawable.recents_move_task_freeform_dark); mLightFullscreenIcon = context.getDrawable(R.drawable.recents_move_task_fullscreen_light); mDarkFullscreenIcon = context.getDrawable(R.drawable.recents_move_task_fullscreen_dark); + mLightInfoIcon = context.getDrawable(R.drawable.recents_info_light); + mDarkInfoIcon = context.getDrawable(R.drawable.recents_info_dark); mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, com.android.internal.R.interpolator.fast_out_slow_in); mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, com.android.internal.R.interpolator.fast_out_linear_in); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, + com.android.internal.R.interpolator.linear_out_slow_in); // Configure the background and dim mBackground = new HighlightColorDrawable(); mBackground.setColorAndDim(Color.argb(255, 0, 0, 0), 0f); setBackground(mBackground); + mOverlayBackground = new HighlightColorDrawable(); mDimLayerPaint.setColor(Color.argb(255, 0, 0, 0)); mDimLayerPaint.setAntiAlias(true); mFocusTimerDuration = res.getInteger(R.integer.recents_auto_advance_duration); } + /** + * Resets this header along with the TaskView. + */ + public void reset() { + hideAppOverlay(true /* immediate */); + } + @Override protected void onFinishInflate() { // Initialize the icon and description views mIconView = (ImageView) findViewById(R.id.icon); + mIconView.setClickable(false); mIconView.setOnLongClickListener(this); mTitleView = (TextView) findViewById(R.id.title); mDismissButton = (ImageView) findViewById(R.id.dismiss_task); mDismissButton.setOnClickListener(this); mMoveTaskButton = (ImageView) findViewById(R.id.move_task); - mFocusTimerIndicator = (ProgressBar) findViewById(R.id.focus_timer_indicator); - - // Hide the backgrounds if they are ripple drawables - if (mIconView.getBackground() instanceof RippleDrawable) { - mIconView.setBackground(null); - } + mFocusTimerIndicatorStub = (ViewStub) findViewById(R.id.focus_timer_indicator_stub); + mAppOverlayViewStub = (ViewStub) findViewById(R.id.app_overlay_stub); } /** @@ -228,6 +252,7 @@ public class TaskViewHeader extends FrameLayout mTaskViewRect.set(0, 0, width, height); boolean updateMoveTaskButton = mMoveTaskButton.getVisibility() != View.GONE; + boolean isFreeformTask = (mTask != null) && mTask.isFreeformTask(); int appIconWidth = mIconView.getMeasuredWidth(); int activityDescWidth = (mTask != null) ? (int) mTitleView.getPaint().measureText(mTask.title) @@ -239,19 +264,20 @@ public class TaskViewHeader extends FrameLayout // Priority-wise, we show the activity icon first, the dismiss icon if there is room, the // move-task icon if there is room, and then finally, the activity label if there is room - if (width < (appIconWidth + dismissIconWidth)) { + if (isFreeformTask && width < (appIconWidth + dismissIconWidth)) { mTitleView.setVisibility(View.INVISIBLE); if (updateMoveTaskButton) { mMoveTaskButton.setVisibility(View.INVISIBLE); } mDismissButton.setVisibility(View.INVISIBLE); - } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth)) { + } else if (isFreeformTask && width < (appIconWidth + dismissIconWidth + + moveTaskIconWidth)) { mTitleView.setVisibility(View.INVISIBLE); if (updateMoveTaskButton) { mMoveTaskButton.setVisibility(View.INVISIBLE); } mDismissButton.setVisibility(View.VISIBLE); - } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth + + } else if (isFreeformTask && width < (appIconWidth + dismissIconWidth + moveTaskIconWidth + activityDescWidth)) { mTitleView.setVisibility(View.INVISIBLE); if (updateMoveTaskButton) { @@ -272,11 +298,6 @@ public class TaskViewHeader extends FrameLayout invalidate(); } - @Override - protected boolean verifyDrawable(Drawable who) { - return super.verifyDrawable(who) || (who == mBackground); - } - @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); @@ -288,6 +309,10 @@ public class TaskViewHeader extends FrameLayout /** Starts the focus timer. */ public void startFocusTimerIndicator() { + if (mFocusTimerIndicator == null) { + return; + } + mFocusTimerIndicator.setVisibility(View.VISIBLE); mFocusTimerIndicator.setMax((int) mFocusTimerDuration); if (mFocusTimerCountDown == null) { @@ -308,7 +333,11 @@ public class TaskViewHeader extends FrameLayout /** Cancels the focus timer. */ public void cancelFocusTimerIndicator() { - if (mFocusTimerCountDown != null && mFocusTimerIndicator != null) { + if (mFocusTimerIndicator == null) { + return; + } + + if (mFocusTimerCountDown != null) { mFocusTimerCountDown.cancel(); mFocusTimerIndicator.setProgress(0); mFocusTimerIndicator.setVisibility(View.INVISIBLE); @@ -337,6 +366,10 @@ public class TaskViewHeader extends FrameLayout private void updateBackgroundColor(float dimAlpha) { if (mTask != null) { mBackground.setColorAndDim(mTask.colorPrimary, dimAlpha); + // TODO: Consider using the saturation of the color to adjust the lightness as well + ColorUtils.colorToHSL(mTask.colorPrimary, mTmpHSL); + mTmpHSL[2] = Math.min(1f, mTmpHSL[2] + OVERLAY_LIGHTNESS_INCREMENT * (1.0f - dimAlpha)); + mOverlayBackground.setColorAndDim(ColorUtils.HSLToColor(mTmpHSL), dimAlpha); mDimLayerPaint.setAlpha((int) (dimAlpha * 255)); } } @@ -382,10 +415,15 @@ public class TaskViewHeader extends FrameLayout mMoveTaskButton.setOnClickListener(this); } - mFocusTimerIndicator.getProgressDrawable() - .setColorFilter( - getSecondaryColor(t.colorPrimary, t.useLightOnPrimaryColor), - PorterDuff.Mode.SRC_IN); + if (Recents.getDebugFlags().isFastToggleIndicatorEnabled()) { + if (mFocusTimerIndicator == null) { + mFocusTimerIndicator = (ProgressBar) mFocusTimerIndicatorStub.inflate(); + } + mFocusTimerIndicator.getProgressDrawable() + .setColorFilter( + getSecondaryColor(t.colorPrimary, t.useLightOnPrimaryColor), + PorterDuff.Mode.SRC_IN); + } // In accessibility, a single click on the focused app info button will show it if (ssp.isTouchExplorationEnabled()) { @@ -447,8 +485,11 @@ public class TaskViewHeader extends FrameLayout @Override public void onClick(View v) { if (v == mIconView) { - // In accessibility, a single click on the focused app info button will show it - EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask)); + SystemServicesProxy ssp = Recents.getSystemServices(); + if (ssp.isTouchExplorationEnabled()) { + // In accessibility, a single click on the focused app info button will show it + EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask)); + } } else if (v == mDismissButton) { TaskView tv = Utilities.findParent(this, TaskView.class); tv.dismissTask(); @@ -463,15 +504,92 @@ public class TaskViewHeader extends FrameLayout : new Rect(); EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, bounds, mMoveTaskTargetStackId, false)); + } else if (v == mAppInfoView) { + EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask)); + } else if (v == mAppIconView) { + hideAppOverlay(false /* immediate */); } } @Override public boolean onLongClick(View v) { if (v == mIconView) { - EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask)); + showAppOverlay(); + return true; + } else if (v == mAppIconView) { + hideAppOverlay(false /* immediate */); return true; } return false; } + + /** + * Shows the application overlay. + */ + private void showAppOverlay() { + // Skip early if the task is invalid + SystemServicesProxy ssp = Recents.getSystemServices(); + ComponentName cn = mTask.key.getComponent(); + int userId = mTask.key.userId; + ActivityInfo activityInfo = ssp.getActivityInfo(cn, userId); + if (activityInfo == null) { + return; + } + + // Inflate the overlay if necessary + if (mAppOverlayView == null) { + mAppOverlayView = (FrameLayout) mAppOverlayViewStub.inflate(); + mAppOverlayView.setBackground(mOverlayBackground); + mAppIconView = (ImageView) mAppOverlayView.findViewById(R.id.app_icon); + mAppIconView.setOnClickListener(this); + mAppIconView.setOnLongClickListener(this); + mAppInfoView = (ImageView) mAppOverlayView.findViewById(R.id.app_info); + mAppInfoView.setOnClickListener(this); + mAppTitleView = (TextView) mAppOverlayView.findViewById(R.id.app_title); + } + + // Update the overlay contents for the current app + mAppTitleView.setText(ssp.getBadgedApplicationLabel(activityInfo.applicationInfo, userId)); + mAppIconView.setImageDrawable(ssp.getBadgedApplicationIcon(activityInfo.applicationInfo, userId)); + mAppInfoView.setImageDrawable(mTask.useLightOnPrimaryColor + ? mLightInfoIcon + : mDarkInfoIcon); + mAppOverlayView.setVisibility(View.VISIBLE); + + int x = mIconView.getLeft() + mIconView.getWidth() / 2; + int y = mIconView.getTop() + mIconView.getHeight() / 2; + Animator revealAnim = ViewAnimationUtils.createCircularReveal(mAppOverlayView, x, y, 0, + getWidth()); + revealAnim.setDuration(OVERLAY_REVEAL_DURATION); + revealAnim.setInterpolator(mLinearOutSlowInInterpolator); + revealAnim.start(); + } + + /** + * Hide the application overlay. + */ + private void hideAppOverlay(boolean immediate) { + // Skip if we haven't even loaded the overlay yet + if (mAppOverlayView == null) { + return; + } + + if (immediate) { + mAppOverlayView.setVisibility(View.GONE); + } else { + int x = mIconView.getLeft() + mIconView.getWidth() / 2; + int y = mIconView.getTop() + mIconView.getHeight() / 2; + Animator revealAnim = ViewAnimationUtils.createCircularReveal(mAppOverlayView, x, y, + getWidth(), 0); + revealAnim.setDuration(OVERLAY_REVEAL_DURATION); + revealAnim.setInterpolator(mLinearOutSlowInInterpolator); + revealAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAppOverlayView.setVisibility(View.GONE); + } + }); + revealAnim.start(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java index 538c2483cb9f4..85b7c82112a41 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java @@ -88,11 +88,38 @@ public class TaskViewTransform { public float alpha = 1f; public boolean visible = false; - float p = 0f; + + // This is the relative task progress of this task, relative to the stack scroll at which this + // transform was computed + public float p = 0f; // This is a window-space rect used for positioning the task in the stack and freeform workspace public RectF rect = new RectF(); + /** + * Fills int this transform from the state of the given TaskView. + */ + public void fillIn(TaskView tv) { + translationZ = tv.getTranslationZ(); + scale = tv.getScaleX(); + alpha = tv.getAlpha(); + visible = true; + p = tv.getTaskProgress(); + rect.set(tv.getLeft(), tv.getTop(), tv.getRight(), tv.getBottom()); + } + + /** + * Copies the transform state from another {@link TaskViewTransform}. + */ + public void copyFrom(TaskViewTransform other) { + translationZ = other.translationZ; + scale = other.scale; + alpha = other.alpha; + visible = other.visible; + p = other.p; + rect.set(other.rect); + } + /** * Resets the current transform. */