Merge changes I91c58397,I7243f30b,I0f6e9dbf,I1eb0864a
* changes:
Adding reveal transition to show app-overlay.
Fixing issue with multiple focus timer animations running.
Fixing several regressions with affiliated tasks.
Starting the dismiss animation in parallel with the gesture.
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 0000000..b1a2242
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recents_info_dark.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48.0dp"
+ android:height="48.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/recents_task_bar_dark_icon_color"
+ android:pathData="M12.000000,2.000000C6.500000,2.000000 2.000000,6.500000 2.000000,12.000000s4.500000,10.000000 10.000000,10.000000c5.500000,0.000000 10.000000,-4.500000 10.000000,-10.000000S17.500000,2.000000 12.000000,2.000000zM13.000000,17.000000l-2.000000,0.000000l0.000000,-6.000000l2.000000,0.000000L13.000000,17.000000zM13.000000,9.000000l-2.000000,0.000000L11.000000,7.000000l2.000000,0.000000L13.000000,9.000000z"/>
+</vector>
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 0000000..bc58c3b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recents_info_light.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48.0dp"
+ android:height="48.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M12.000000,2.000000C6.500000,2.000000 2.000000,6.500000 2.000000,12.000000s4.500000,10.000000 10.000000,10.000000c5.500000,0.000000 10.000000,-4.500000 10.000000,-10.000000S17.500000,2.000000 12.000000,2.000000zM13.000000,17.000000l-2.000000,0.000000l0.000000,-6.000000l2.000000,0.000000L13.000000,17.000000zM13.000000,9.000000l-2.000000,0.000000L11.000000,7.000000l2.000000,0.000000L13.000000,9.000000z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/recents_task_view_header.xml b/packages/SystemUI/res/layout/recents_task_view_header.xml
index 07ac39a..a94e781 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" />
- <ProgressBar
- android:id="@+id/focus_timer_indicator"
- style="?android:attr/progressBarStyleHorizontal"
- android:layout_width="match_parent"
- android:layout_height="5dp"
- android:layout_gravity="bottom"
- android:indeterminateOnly="false"
- android:visibility="invisible" />
+
+ <!-- The progress indicator shows if auto-paging is enabled -->
+ <ViewStub android:id="@+id/focus_timer_indicator_stub"
+ android:inflatedId="@+id/focus_timer_indicator"
+ android:layout="@layout/recents_task_view_header_progress_bar"
+ android:layout_width="match_parent"
+ android:layout_height="5dp"
+ android:layout_gravity="bottom" />
+
+ <!-- The app overlay shows as the user long-presses on the app icon -->
+ <ViewStub android:id="@+id/app_overlay_stub"
+ android:inflatedId="@+id/app_overlay"
+ android:layout="@layout/recents_task_view_header_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
</com.android.systemui.recents.views.TaskViewHeader>
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 0000000..5cdde9e
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone">
+ <com.android.systemui.recents.views.FixedSizeImageView
+ android:id="@+id/app_icon"
+ android:contentDescription="@string/recents_app_info_button_label"
+ android:layout_width="@dimen/recents_task_view_application_icon_size"
+ android:layout_height="@dimen/recents_task_view_application_icon_size"
+ android:layout_marginStart="8dp"
+ android:layout_gravity="center_vertical|start"
+ android:padding="8dp" />
+ <TextView
+ android:id="@+id/app_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:layout_marginStart="64dp"
+ android:layout_marginEnd="112dp"
+ android:textSize="16sp"
+ android:textColor="#ffffffff"
+ android:text="@string/recents_empty_message"
+ android:fontFamily="sans-serif-medium"
+ android:singleLine="true"
+ android:maxLines="2"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+ <com.android.systemui.recents.views.FixedSizeImageView
+ android:id="@+id/app_info"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginEnd="4dp"
+ android:layout_gravity="center_vertical|end"
+ android:padding="12dp"
+ android:background="@drawable/recents_button_bg"
+ android:src="@drawable/recents_info_light" />
+</FrameLayout>
\ 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 0000000..e740458
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<ProgressBar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:indeterminateOnly="false"
+ android:visibility="gone" />
\ 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 6795da4..17ff195 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -35,7 +35,7 @@
<!-- Recents: The relative range of visible tasks from the current scroll position
while the stack is focused. -->
- <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+ <item name="recents_layout_focused_range_min" format="float" type="integer">-3</item>
<item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
<!-- Recents: The relative range of visible tasks from the current scroll position
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e98ec82..7a87314 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -147,6 +147,9 @@
<!-- The duration for animating the task decorations in after transitioning from an app. -->
<integer name="recents_task_enter_from_app_duration">200</integer>
+ <!-- The duration for animating the task decorations in after transitioning from an app. -->
+ <integer name="recents_task_enter_from_affiliated_app_duration">125</integer>
+
<!-- The duration for animating the task decorations out before transitioning to an app. -->
<integer name="recents_task_exit_to_app_duration">125</integer>
@@ -194,7 +197,7 @@
<!-- Recents: The relative range of visible tasks from the current scroll position
while the stack is focused. -->
- <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+ <item name="recents_layout_focused_range_min" format="float" type="integer">-3</item>
<item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
<!-- Recents: The relative range of visible tasks from the current scroll position
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 47eb05a..91ca289 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -210,7 +210,7 @@
<dimen name="recents_task_view_highlight">1dp</dimen>
<!-- The amount to offset when animating into an affiliate group. -->
- <dimen name="recents_task_view_affiliate_group_enter_offset">64dp</dimen>
+ <dimen name="recents_task_view_affiliate_group_enter_offset">32dp</dimen>
<!-- The height of a task view bar. -->
<dimen name="recents_task_bar_height">56dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index d28da41..fd4161f 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -367,6 +367,7 @@
}
anim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
+ updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
mCallback.onChildDismissed(view);
if (endAction != null) {
endAction.run();
@@ -381,9 +382,17 @@
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 @@
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 db55f28..3a3b19d 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 @@
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 @@
loader.loadTasks(this, plan, loadOpts);
TaskStack stack = plan.getTaskStack();
- ArrayList<Task> 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 @@
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 3151fd7..3866967 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 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 7c25d24..3eee087 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 @@
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
private TaskViewTransform getThumbnailTransitionTransform(TaskStack stack,
TaskStackView stackView, int runningTaskId, Task runningTaskOut) {
// Find the running task in the TaskStack
- Task task = null;
- ArrayList<Task> 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 @@
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 80597bc..f6655c7 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.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 @@
private Context mContext;
private LayoutInflater mLayoutInflater;
- private final List<Task> mTasks = new ArrayList<>();
private final List<Row> mRows = new ArrayList<>();
private final SparseIntArray mTaskRowCount = new SparseIntArray();
private TaskStack mStack;
@@ -268,8 +268,8 @@
public void onTaskRemoved(Task task, int position) {
// Since this is removed from the history, we need to update the stack as well to ensure
- // that the model is correct
- mStack.removeTask(task);
+ // that the model is correct. Since the stack is hidden, we can update it immediately.
+ mStack.removeTask(task, TaskViewAnimation.IMMEDIATE);
removeTaskRow(position);
if (mRows.isEmpty()) {
dismissHistory();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java b/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java
new file mode 100644
index 0000000..72511de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.recents.misc;
+
+import android.animation.TypeEvaluator;
+import android.graphics.RectF;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>RectF</code> values.
+ */
+public class RectFEvaluator implements TypeEvaluator<RectF> {
+
+ private RectF mRect = new RectF();
+
+ /**
+ * This function returns the result of linearly interpolating the start and
+ * end Rect values, with <code>fraction</code> 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).
+ *
+ * <p>The object returned will be the <code>reuseRect</code> passed into the constructor.</p>
+ *
+ * @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
+ * <code>fraction</code> 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 adc1e92..3f52ae8 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.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 @@
}
}
- /** 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,
@@ -610,6 +613,22 @@
}
/**
+ * 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.
*/
public Drawable getBadgedTaskDescriptionIcon(ActivityManager.TaskDescription taskDescription,
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 822ad77..9cdd703 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.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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
* - 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<Task> allTasks = new ArrayList<>();
if (mRawTasks == null) {
preloadRawTasks(isTopTaskHome);
}
+ SparseArray<Task.TaskKey> 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 @@
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 @@
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 @@
// 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 28338d83..44ad239 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 @@
}
/** 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 @@
// 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 @@
* 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 @@
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 29e7077..193bd17 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 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 @@
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 327cdf8..c73273e 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.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 @@
* 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 @@
}
}
- /** 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 @@
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 @@
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 e727652..e448101 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 @@
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 @@
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 2930f4d..ccbb329 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 @@
// 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 @@
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 @@
List<TaskView> 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 @@
// 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 @@
} 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 68ff63c..e99509c 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 @@
* Computes the minimum and maximum scroll progress values and the progress values for each task
* in the stack.
*/
- void update(TaskStack stack, ArraySet<Task> ignoreTasksSet) {
+ void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet) {
SystemServicesProxy ssp = Recents.getSystemServices();
// Clear the progress map
@@ -393,7 +393,7 @@
ArrayList<Task> 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 @@
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 @@
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 @@
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 @@
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 @@
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 7583a19..809d4ee 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.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.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 @@
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<Task.TaskKey> EMPTY_TASK_SET = new ArraySet<>();
+
TaskStack mStack;
TaskStackLayoutAlgorithm mLayoutAlgorithm;
TaskStackViewScroller mStackScroller;
@@ -116,7 +118,8 @@
ArrayList<TaskView> mTaskViews = new ArrayList<>();
ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>();
- TaskViewAnimation mDeferredTaskViewUpdateAnimation = null;
+ ArraySet<Task.TaskKey> mIgnoreTasks = new ArraySet<>();
+ TaskViewAnimation mDeferredTaskViewLayoutAnimation = null;
DozeTrigger mUIDozeTrigger;
Task mFocusedTask;
@@ -137,7 +140,6 @@
int[] mTmpVisibleRange = new int[2];
Rect mTmpRect = new Rect();
ArrayMap<Task.TaskKey, TaskView> mTmpTaskViewMap = new ArrayMap<>();
- ArraySet<Task> mTmpTaskSet = new ArraySet<>();
List<TaskView> mTmpTaskViews = new ArrayList<>();
TaskViewTransform mTmpTransform = new TaskViewTransform();
LayoutInflater mInflater;
@@ -345,30 +347,82 @@
}
/**
- * 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<TaskViewTransform> taskTransforms,
- ArrayList<Task> tasks, float stackScroll,
- int[] visibleRangeOut, ArraySet<Task> 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<TaskViewTransform> taskTransforms,
+ ArrayList<Task> tasks, float curStackScroll, float targetStackScroll,
+ int[] visibleRangeOut, ArraySet<Task.TaskKey> 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 @@
break;
}
}
+
frontTransform = transform;
+ frontTransformAtTarget = transformAtTarget;
}
if (visibleRangeOut != null) {
visibleRangeOut[0] = frontMostVisibleIndex;
@@ -402,33 +458,48 @@
}
/**
- * 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<Task.TaskKey>)
*/
- private void bindTaskViewsWithStack(ArraySet<Task> 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<Task.TaskKey> ignoreTasksSet) {
final int[] visibleStackRange = mTmpVisibleRange;
// Get all the task transforms
final ArrayList<Task> 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<TaskView> taskViews = getTaskViews();
- final int taskViewCount = taskViews.size();
- int lastFocusedTaskIndex = -1;
mTmpTaskViewMap.clear();
- mTmpTaskViewMap.ensureCapacity(tasks.size());
+ List<TaskView> 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 @@
}
// 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 @@
}
/**
- * 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<Task.TaskKey>)
*/
- private void updateTaskViewsToLayout(TaskViewAnimation animation, Task... ignoreTasks) {
- // Keep track of the ignore tasks
- ArraySet<Task> 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<Task.TaskKey> 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<TaskView> taskViews = getTaskViews();
@@ -531,7 +610,7 @@
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 @@
/**
* 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 @@
}
/**
- * 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<Task> tasks,
+ ArrayList<TaskViewTransform> 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<Task> tasks,
+ ArrayList<TaskViewTransform> 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<Task.TaskKey>)
+ */
+ void cancelAllTaskViewAnimations() {
+ cancelAllTaskViewAnimations(mIgnoreTasks);
+ }
+
+ /**
+ * Cancels all {@link TaskView} animations.
+ *
+ * @param ignoreTasksSet The set of tasks to continue running their animations.
+ */
+ void cancelAllTaskViewAnimations(ArraySet<Task.TaskKey> ignoreTasksSet) {
List<TaskView> 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 @@
// Update the clip on each task child
List<TaskView> 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 @@
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<Task.TaskKey>)
+ */
+ 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<Task> ignoreTasksSet = mTmpTaskSet;
- ignoreTasksSet.clear();
- ignoreTasksSet.ensureCapacity(ignoreTasks.length);
- Collections.addAll(ignoreTasksSet, ignoreTasks);
-
+ void updateLayoutAlgorithm(boolean boundScrollToNewMinMax,
+ ArraySet<Task.TaskKey> 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 @@
// 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 @@
tv.getHeaderView().cancelFocusTimerIndicator();
}
}
+
+ resetFocusedTask(mFocusedTask);
}
boolean willScroll = false;
@@ -937,10 +1080,10 @@
// 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();
@@ -948,30 +1091,16 @@
}
/**
- * 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 @@
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 @@
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 @@
// 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 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<Task> 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 @@
@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 @@
}
@Override
- public void onHistoryTaskRemoved(TaskStack stack, Task removedTask) {
+ public void onHistoryTaskRemoved(TaskStack stack, Task removedTask,
+ TaskViewAnimation animation) {
// To be implemented
}
@@ -1316,7 +1441,10 @@
@Override
public void onTaskViewClipStateChanged(TaskView tv) {
- clipTaskViews();
+ if (!mTaskViewsClipDirty) {
+ mTaskViewsClipDirty = true;
+ invalidate();
+ }
}
/**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/
@@ -1324,7 +1452,9 @@
@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 @@
tv.dismissTask();
} else {
// Otherwise, remove the task from the stack immediately
- mStack.removeTask(t);
+ mStack.removeTask(t, TaskViewAnimation.IMMEDIATE);
}
}
}
@@ -1402,7 +1532,7 @@
}
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 @@
// 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 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 @@
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 @@
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 @@
* 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 @@
}
/**
- * 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 32f02ac..5335b14 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 @@
/** 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 4813c19..e9f6f39 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.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.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 @@
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<Task> mCurrentTasks = new ArrayList<>();
+ private ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>();
+ private ArrayList<TaskViewTransform> mFinalTaskTransforms = new ArrayList<>();
+ private ArrayMap<View, Animator> mSwipeHelperAnimations = new ArrayMap<>();
+ private TaskViewTransform mTmpTransform = new TaskViewTransform();
+ private float mTargetStackScroll;
+
SwipeHelper mSwipeHelper;
boolean mInterceptedBySwipeHelper;
@@ -97,8 +115,14 @@
}
@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 @@
}
}
- /** Returns the view at the specified coordinates */
- TaskView findViewAtPoint(int x, int y) {
- List<TaskView> 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 @@
mScroller.stopBoundScrollAnimation();
Utilities.cancelAnimationWithoutCallbacks(mScrollFlingAnimator);
+ // Finish any existing task animations from the delete
+ mSv.cancelAllTaskViewAnimations();
+ // Finish any of the swipe helper animations
+ ArrayMap<View, Animator> 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 @@
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 @@
@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 @@
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 @@
return 0;
}
+ /**
+ * Interpolates the non-deleting tasks to their final transforms from their current transforms.
+ */
+ private void updateTaskViewTransforms(float dismissFraction) {
+ List<TaskView> 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<Task> 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 9b72702..1f8216f 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 @@
/** Resets this TaskView for reuse. */
void reset() {
+ mHeaderView.reset();
resetViewProperties();
resetNoUserInteractionState();
setClipViewInStack(false);
@@ -459,16 +460,14 @@
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 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 e7717ac..2563190 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.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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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) {
@@ -273,11 +299,6 @@
}
@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 @@
/** 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 @@
/** 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 @@
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 @@
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 @@
@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 @@
: 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 538c248..85b7c82 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
@@ -88,12 +88,39 @@
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.
*/
public void reset() {