diff --git a/packages/SystemUI/res/drawable/recents_task_view_header_bg.xml b/packages/SystemUI/res/drawable/recents_task_view_header_bg.xml new file mode 100644 index 0000000000000..745af33333df3 --- /dev/null +++ b/packages/SystemUI/res/drawable/recents_task_view_header_bg.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/recents_task_view.xml b/packages/SystemUI/res/layout/recents_task_view.xml index 4a5fffe5ada48..6cfff2e3670fb 100644 --- a/packages/SystemUI/res/layout/recents_task_view.xml +++ b/packages/SystemUI/res/layout/recents_task_view.xml @@ -26,8 +26,7 @@ android:id="@+id/task_view_bar" android:layout_width="match_parent" android:layout_height="@dimen/recents_task_bar_height" - android:layout_gravity="top|center_horizontal" - android:background="@color/recents_task_bar_default_background_color"> + android:layout_gravity="top|center_horizontal"> 96 50 + + 200 true diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java index efb7a2c7f5386..354eb55a18809 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java +++ b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java @@ -345,8 +345,8 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta * Creates the activity options for an app->recents transition. If this method sets the static * screenshot, then we will use that for the transition. */ - ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask) { - + ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask, + boolean isTopTaskHome) { if (Constants.DebugFlags.App.EnableScreenshotAppTransition) { // Recycle the last screenshot consumeLastScreenshot(); @@ -365,7 +365,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta Bitmap firstThumbnail = mSystemServicesProxy.getTaskThumbnail(topTask.id); if (firstThumbnail != null) { // Update the destination rect - Rect toTaskRect = getThumbnailTransitionRect(topTask.id); + Rect toTaskRect = getThumbnailTransitionRect(topTask.id, isTopTaskHome); if (toTaskRect.width() > 0 && toTaskRect.height() > 0) { // Create the new thumbnail for the animation down // XXX: We should find a way to optimize this so we don't need to create a new bitmap @@ -389,7 +389,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta } /** Returns the transition rect for the given task id. */ - Rect getThumbnailTransitionRect(int runningTaskId) { + Rect getThumbnailTransitionRect(int runningTaskId, boolean isTopTaskHome) { // Get the stack of tasks that we are animating into TaskStack stack = RecentsTaskLoader.getShallowTaskStack(mSystemServicesProxy, -1); if (stack.getTaskCount() == 0) { @@ -401,7 +401,8 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta TaskStackViewLayoutAlgorithm algo = tsv.getStackAlgorithm(); Rect taskStackBounds = new Rect(mTaskStackBounds); taskStackBounds.bottom -= mSystemInsets.bottom; - tsv.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds, mTriggeredFromAltTab); + tsv.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds, + mTriggeredFromAltTab, isTopTaskHome); tsv.getScroller().setStackScrollToInitialState(); // Find the running task in the TaskStack @@ -442,7 +443,7 @@ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationSta if (useThumbnailTransition) { // Try starting with a thumbnail transition - ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask); + ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, isTopTaskHome); if (opts != null) { if (sLastScreenshot != null) { startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_FULL_SCREENSHOT); diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java index 417049c7c597f..41e06de801121 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -61,6 +61,7 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView RecentsConfiguration mConfig; boolean mVisible; + long mLastTabKeyEventTime; // Top level views RecentsView mRecentsView; @@ -512,17 +513,33 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_TAB) { - // Focus the next task in the stack - final boolean backward = event.isShiftPressed(); - mRecentsView.focusNextTask(!backward); - return true; - } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { - mRecentsView.focusNextTask(true); - return true; - } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - mRecentsView.focusNextTask(false); - return true; + switch (keyCode) { + case KeyEvent.KEYCODE_TAB: { + boolean hasRepKeyTimeElapsed = (System.currentTimeMillis() - + mLastTabKeyEventTime) > mConfig.altTabKeyDelay; + if (event.getRepeatCount() <= 0 || hasRepKeyTimeElapsed) { + // Focus the next task in the stack + final boolean backward = event.isShiftPressed(); + mRecentsView.focusNextTask(!backward); + mLastTabKeyEventTime = System.currentTimeMillis(); + } + return true; + } + case KeyEvent.KEYCODE_DPAD_UP: { + mRecentsView.focusNextTask(true); + return true; + } + case KeyEvent.KEYCODE_DPAD_DOWN: { + mRecentsView.focusNextTask(false); + return true; + } + case KeyEvent.KEYCODE_DEL: + case KeyEvent.KEYCODE_FORWARD_DEL: { + mRecentsView.dismissFocusedTask(); + return true; + } + default: + break; } // Pass through the debug trigger mDebugTrigger.onKeyEvent(keyCode); diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java index 65e7076e7e84f..3f5018d8290bd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java @@ -112,6 +112,9 @@ public class RecentsConfiguration { public boolean launchedFromHome; public int launchedToTaskId; + /** Misc **/ + public int altTabKeyDelay; + /** Dev options and global settings */ public boolean lockToAppEnabled; public boolean developerOptionsEnabled; @@ -250,6 +253,9 @@ public class RecentsConfiguration { // Nav bar scrim navBarScrimEnterDuration = res.getInteger(R.integer.recents_nav_bar_scrim_enter_duration); + + // Misc + altTabKeyDelay = res.getInteger(R.integer.recents_alt_tab_key_delay); } /** Updates the system insets */ diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java index bd5df28b76a91..4c6b389062a45 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java @@ -89,6 +89,17 @@ public class Utilities { return Math.abs((fgL + 0.05f) / (bgL + 0.05f)); } + /** Returns the base color overlaid with another overlay color with a specified alpha. */ + public static int getColorWithOverlay(int baseColor, int overlayColor, float overlayAlpha) { + return Color.rgb( + (int) (overlayAlpha * Color.red(baseColor) + + (1f - overlayAlpha) * Color.red(overlayColor)), + (int) (overlayAlpha * Color.green(baseColor) + + (1f - overlayAlpha) * Color.green(overlayColor)), + (int) (overlayAlpha * Color.blue(baseColor) + + (1f - overlayAlpha) * Color.blue(overlayColor))); + } + /** Sets some private shadow properties. */ public static void setShadowProperty(String property, String value) throws IllegalAccessException, InvocationTargetException { 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 0269141a1cf60..98bf151b3fabb 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -20,6 +20,7 @@ import android.graphics.Color; import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.NamedCounter; +import com.android.systemui.recents.misc.Utilities; import java.util.ArrayList; import java.util.Collections; @@ -433,10 +434,8 @@ public class TaskStack { float alpha = 1f; for (int j = 0; j < taskCount; j++) { Task t = tasksMap.get(group.mTaskKeys.get(j)); - t.colorPrimary = Color.rgb( - (int) (alpha * Color.red(affiliationColor) + (1f - alpha) * 0xFF), - (int) (alpha * Color.green(affiliationColor) + (1f - alpha) * 0xFF), - (int) (alpha * Color.blue(affiliationColor) + (1f - alpha) * 0xFF)); + t.colorPrimary = Utilities.getColorWithOverlay(affiliationColor, Color.WHITE, + alpha); alpha -= alphaStep; } } 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 34e88608996c2..07a7e74cfdabd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -316,12 +316,11 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV /** Notifies each task view of the user interaction. */ public void onUserInteraction() { // Get the first stack view - TaskStackView stackView = null; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child != mSearchBar) { - stackView = (TaskStackView) child; + TaskStackView stackView = (TaskStackView) child; stackView.onUserInteraction(); } } @@ -330,18 +329,28 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV /** Focuses the next task in the first stack view */ public void focusNextTask(boolean forward) { // Get the first stack view - TaskStackView stackView = null; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child != mSearchBar) { - stackView = (TaskStackView) child; + TaskStackView stackView = (TaskStackView) child; + stackView.focusNextTask(forward); break; } } + } - if (stackView != null) { - stackView.focusNextTask(forward); + /** Dismisses the focused task. */ + public void dismissFocusedTask() { + // Get the first stack view + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child != mSearchBar) { + TaskStackView stackView = (TaskStackView) child; + stackView.dismissFocusedTask(); + break; + } } } 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 46996bba3f82d..4fd91366e3080 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -349,9 +349,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** Updates the min and max virtual scroll bounds */ - void updateMinMaxScroll(boolean boundScrollToNewMinMax, boolean launchedWithAltTab) { + void updateMinMaxScroll(boolean boundScrollToNewMinMax, boolean launchedWithAltTab, + boolean launchedFromHome) { // Compute the min and max scroll values - mLayoutAlgorithm.computeMinMaxScroll(mStack.getTasks(), launchedWithAltTab); + mLayoutAlgorithm.computeMinMaxScroll(mStack.getTasks(), launchedWithAltTab, launchedFromHome); // Debug logging if (boundScrollToNewMinMax) { @@ -366,6 +367,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal /** Focuses the task at the specified index in the stack */ void focusTask(int taskIndex, boolean scrollToNewPosition) { + // Return early if the task is already focused + if (taskIndex == mFocusedTaskIndex) return; + if (0 <= taskIndex && taskIndex < mStack.getTaskCount()) { mFocusedTaskIndex = taskIndex; @@ -406,14 +410,24 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal void focusNextTask(boolean forward) { // Find the next index to focus int numTasks = mStack.getTaskCount(); - if (mFocusedTaskIndex < 0) { - mFocusedTaskIndex = numTasks - 1; - } + if (numTasks == 0) return; + + int nextFocusIndex = numTasks - 1; if (0 <= mFocusedTaskIndex && mFocusedTaskIndex < numTasks) { - mFocusedTaskIndex = Math.max(0, Math.min(numTasks - 1, + nextFocusIndex = Math.max(0, Math.min(numTasks - 1, mFocusedTaskIndex + (forward ? -1 : 1))); } - focusTask(mFocusedTaskIndex, true); + focusTask(nextFocusIndex, true); + } + + /** Dismisses the focused task. */ + public void dismissFocusedTask() { + // Return early if there is no focused task index + if (mFocusedTaskIndex < 0) return; + + Task t = mStack.getTasks().get(mFocusedTaskIndex); + TaskView tv = getChildViewForTask(t); + tv.dismissTask(); } @Override @@ -436,12 +450,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal /** Computes the stack and task rects */ public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds, - boolean launchedWithAltTab) { + boolean launchedWithAltTab, boolean launchedFromHome) { // Compute the rects in the stack algorithm mLayoutAlgorithm.computeRects(windowWidth, windowHeight, taskStackBounds); // Update the scroll bounds - updateMinMaxScroll(false, launchedWithAltTab); + updateMinMaxScroll(false, launchedWithAltTab, launchedFromHome); } /** @@ -456,7 +470,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Compute our stack/task rects Rect taskStackBounds = new Rect(mTaskStackBounds); taskStackBounds.bottom -= mConfig.systemInsets.bottom; - computeRects(width, height, taskStackBounds, mConfig.launchedWithAltTab); + computeRects(width, height, taskStackBounds, mConfig.launchedWithAltTab, + mConfig.launchedFromHome); // If this is the first layout, then scroll to the front of the stack and synchronize the // stack views immediately to load all the views @@ -546,7 +561,11 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // When Alt-Tabbing, we scroll to and focus the previous task if (mConfig.launchedWithAltTab) { - focusTask(Math.max(0, mStack.getTaskCount() - 2), false); + if (mConfig.launchedFromHome) { + focusTask(Math.max(0, mStack.getTaskCount() - 1), false); + } else { + focusTask(Math.max(0, mStack.getTaskCount() - 2), false); + } } } @@ -661,7 +680,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mCb.onTaskViewDismissed(removedTask); // Update the min/max scroll and animate other task views into their new positions - updateMinMaxScroll(true, mConfig.launchedWithAltTab); + updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome); requestSynchronizeStackViewsWithModel(200); // Update the new front most task @@ -859,11 +878,23 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal @Override public void onTaskViewDismissed(TaskView tv) { Task task = tv.getTask(); + int taskIndex = mStack.indexOfTask(task); + boolean taskWasFocused = tv.isFocusedTask(); // Announce for accessibility tv.announceForAccessibility(getContext().getString(R.string.accessibility_recents_item_dismissed, tv.getTask().activityLabel)); // Remove the task from the view mStack.removeTask(task); + // If the dismissed task was focused, then we should focus the next task in front + if (taskWasFocused) { + ArrayList tasks = mStack.getTasks(); + int nextTaskIndex = Math.min(tasks.size() - 1, taskIndex); + if (nextTaskIndex >= 0) { + Task nextTask = tasks.get(nextTaskIndex); + TaskView nextTv = getChildViewForTask(nextTask); + nextTv.setFocusedTask(); + } + } } @Override @@ -876,6 +907,13 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal requestSynchronizeStackViewsWithModel(); } + @Override + public void onTaskViewFocusChanged(TaskView tv, boolean focused) { + if (focused) { + mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); + } + } + /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/ @Override diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java index 6c8de727a3d82..495d00b1fa313 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java @@ -91,7 +91,8 @@ public class TaskStackViewLayoutAlgorithm { /** Computes the minimum and maximum scroll progress values. This method may be called before * the RecentsConfiguration is set, so we need to pass in the alt-tab state. */ - void computeMinMaxScroll(ArrayList tasks, boolean launchedWithAltTab) { + void computeMinMaxScroll(ArrayList tasks, boolean launchedWithAltTab, + boolean launchedFromHome) { // Clear the progress map mTaskProgressMap.clear(); @@ -101,10 +102,16 @@ public class TaskStackViewLayoutAlgorithm { return; } + // Note that we should account for the scale difference of the offsets at the screen bottom int taskHeight = mTaskRect.height(); float pAtBottomOfStackRect = screenYToCurveProgress(mStackVisibleRect.bottom); - float pWithinAffiliateOffset = pAtBottomOfStackRect - - screenYToCurveProgress(mStackVisibleRect.bottom - mWithinAffiliationOffset); + float pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom - + mWithinAffiliationOffset); + float scale = curveProgressToScale(pWithinAffiliateTop); + int scaleYOffset = (int) (((1f - scale) * taskHeight) / 2); + pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom - + mWithinAffiliationOffset + scaleYOffset); + float pWithinAffiliateOffset = pAtBottomOfStackRect - pWithinAffiliateTop; float pBetweenAffiliateOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - mBetweenAffiliationOffset); float pTaskHeightOffset = pAtBottomOfStackRect - @@ -133,8 +140,13 @@ public class TaskStackViewLayoutAlgorithm { mMaxScrollP = pAtFrontMostCardTop - ((1f - pTaskHeightOffset - pNavBarOffset)); mMinScrollP = tasks.size() == 1 ? Math.max(mMaxScrollP, 0f) : 0f; if (launchedWithAltTab) { - // Center the second most task, since that will be focused first - mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f; + if (launchedFromHome) { + // Center the top most task, since that will be focused first + mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f; + } else { + // Center the second top most task, since that will be focused first + mInitialScrollP = pAtSecondFrontMostCardTop - 0.5f; + } } else { mInitialScrollP = pAtFrontMostCardTop - 0.825f; } 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 0a12dab0821ba..2c0dc44f0448a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java @@ -133,7 +133,7 @@ public class TaskStackViewScroller { stopBoundScrollAnimation(); mScrollAnimator = ObjectAnimator.ofFloat(this, "stackScroll", curScroll, newScroll); - mScrollAnimator.setDuration(250); + mScrollAnimator.setDuration(200); // We would have to project the difference into the screen coords, and then use that as the // duration // mScrollAnimator.setDuration(Utilities.calculateTranslationAnimationDuration(newScroll - 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 3b9bcc41ce7e9..236229e6984dd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -48,6 +48,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, public void onTaskViewDismissed(TaskView tv); public void onTaskViewClipStateChanged(TaskView tv); public void onTaskViewFullScreenTransitionCompleted(); + public void onTaskViewFocusChanged(TaskView tv, boolean focused); } RecentsConfiguration mConfig; @@ -62,13 +63,14 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, Task mTask; boolean mTaskDataLoaded; boolean mIsFocused; + boolean mFocusAnimationsEnabled; boolean mIsFullScreenView; boolean mClipViewInStack; AnimateableViewBounds mViewBounds; Paint mLayerPaint = new Paint(); TaskViewThumbnail mThumbnailView; - TaskViewHeader mBarView; + TaskViewHeader mHeaderView; TaskViewFooter mFooterView; View mActionButtonView; TaskViewCallbacks mCb; @@ -124,7 +126,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override protected void onFinishInflate() { // Bind the views - mBarView = (TaskViewHeader) findViewById(R.id.task_view_bar); + mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar); mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail); mActionButtonView = findViewById(R.id.lock_to_app_fab); if (mFooterView != null) { @@ -138,7 +140,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, int height = MeasureSpec.getSize(heightMeasureSpec); // Measure the bar view, thumbnail, and footer - mBarView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + mHeaderView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY)); if (mFooterView != null) { mFooterView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), @@ -218,11 +220,11 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, /** Prepares this task view for the enter-recents animations. This is called earlier in the * first layout because the actual animation into recents may take a long time. */ - public void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, + void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, boolean occludesLaunchTarget, int offscreenY) { if (mConfig.launchedFromAppWithScreenshot) { if (isTaskViewLaunchTargetTask) { - mBarView.prepareEnterRecentsAnimation(); + mHeaderView.prepareEnterRecentsAnimation(); // Hide the footer during the transition in, and animate it out afterwards? if (mFooterView != null) { mFooterView.animateFooterVisibility(false, 0); @@ -234,7 +236,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } else if (mConfig.launchedFromAppWithThumbnail) { if (isTaskViewLaunchTargetTask) { // Hide the front most task bar view so we can animate it in - mBarView.prepareEnterRecentsAnimation(); + mHeaderView.prepareEnterRecentsAnimation(); // Hide the action button if it exists mActionButtonView.setAlpha(0f); // Set the dim to 0 so we can animate it in @@ -256,8 +258,9 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } /** Animates this task view as it enters recents */ - public void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) { + void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) { final TaskViewTransform transform = ctx.currentTaskTransform; + int startDelay = 0; if (mConfig.launchedFromAppWithScreenshot) { if (mTask.isLaunchTarget) { @@ -269,6 +272,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, float scaledWindowInsetTop = (int) (taskScale * windowInsetTop); float scaledTranslationY = taskRect.top + transform.translationY - (scaledWindowInsetTop + scaledYOffset); + startDelay = mConfig.taskViewEnterFromHomeDelay; // Animate the top clip mViewBounds.animateClipTop(windowInsetTop, duration, @@ -276,7 +280,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override public void onAnimationUpdate(ValueAnimator animation) { int y = (Integer) animation.getAnimatedValue(); - mBarView.setTranslationY(y); + mHeaderView.setTranslationY(y); } }); // Animate the bottom or right clip @@ -287,7 +291,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mViewBounds.animateClipBottom(getMeasuredHeight() - (windowInsetTop + size), duration); } // Animate the task bar of the first task view - mBarView.startEnterRecentsAnimation(0, null); + mHeaderView.startEnterRecentsAnimation(0, null); animate() .scaleX(taskScale) .scaleY(taskScale) @@ -304,9 +308,9 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mViewBounds.setClipBottom(0); mViewBounds.setClipRight(0); // Reset the bar translation - mBarView.setTranslationY(0); + mHeaderView.setTranslationY(0); // Enable the thumbnail clip - mThumbnailView.enableTaskBarClip(mBarView); + mThumbnailView.enableTaskBarClip(mHeaderView); // Animate the footer into view (if it is the front most task) animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration); @@ -324,7 +328,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .start(); } else { // Otherwise, just enable the thumbnail clip - mThumbnailView.enableTaskBarClip(mBarView); + mThumbnailView.enableTaskBarClip(mHeaderView); // Animate the footer into view animateFooterVisibility(true, 0); @@ -334,8 +338,8 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } else if (mConfig.launchedFromAppWithThumbnail) { if (mTask.isLaunchTarget) { // Animate the task bar of the first task view - mBarView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay, - mThumbnailView.enableTaskBarClipAsRunnable(mBarView)); + mHeaderView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay, + mThumbnailView.enableTaskBarClipAsRunnable(mHeaderView)); // Animate the dim into view as well ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", getDimFromTaskProgress()); @@ -364,7 +368,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .start(); } else { // Enable the task bar clip - mThumbnailView.enableTaskBarClip(mBarView); + mThumbnailView.enableTaskBarClip(mHeaderView); // Animate the task up if it was occluding the launch target if (ctx.currentTaskOccludesLaunchTarget) { setTranslationY(transform.translationY + mConfig.taskViewAffiliateGroupEnterOffsetPx); @@ -378,7 +382,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .withEndAction(new Runnable() { @Override public void run() { - mThumbnailView.enableTaskBarClip(mBarView); + mThumbnailView.enableTaskBarClip(mHeaderView); // Decrement the post animation trigger ctx.postAnimationTrigger.decrement(); } @@ -387,6 +391,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, ctx.postAnimationTrigger.increment(); } } + startDelay = mConfig.taskBarEnterAnimDelay; } else if (mConfig.launchedFromHome) { // Animate the tasks up @@ -407,7 +412,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .withEndAction(new Runnable() { @Override public void run() { - mThumbnailView.enableTaskBarClip(mBarView); + mThumbnailView.enableTaskBarClip(mHeaderView); // Decrement the post animation trigger ctx.postAnimationTrigger.decrement(); } @@ -417,18 +422,28 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, // Animate the footer into view animateFooterVisibility(true, mConfig.taskViewEnterFromHomeDuration); + startDelay = delay; } else { // Otherwise, just enable the thumbnail clip - mThumbnailView.enableTaskBarClip(mBarView); + mThumbnailView.enableTaskBarClip(mHeaderView); // Animate the footer into view animateFooterVisibility(true, 0); } + + // Enable the focus animations from this point onwards so that they aren't affected by the + // window transitions + postDelayed(new Runnable() { + @Override + public void run() { + enableFocusAnimations(); + } + }, (startDelay / 2)); } /** Animates this task view as it leaves recents by pressing home. */ - public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { + void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { animate() .translationY(ctx.offscreenTranslationY) .setStartDelay(0) @@ -441,11 +456,11 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } /** Animates this task view as it exits recents */ - public void startLaunchTaskAnimation(final Runnable r, boolean isLaunchingTask, - boolean occludesLaunchTarget) { + void startLaunchTaskAnimation(final Runnable r, boolean isLaunchingTask, + boolean occludesLaunchTarget) { if (isLaunchingTask) { // Disable the thumbnail clip and animate the bar out - mBarView.startLaunchTaskAnimation(mThumbnailView.disableTaskBarClipAsRunnable(), r); + mHeaderView.startLaunchTaskAnimation(mThumbnailView.disableTaskBarClipAsRunnable(), r); // Animate the dim if (mDim > 0) { @@ -464,7 +479,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, .start(); } else { // Hide the dismiss button - mBarView.startLaunchTaskDismissAnimation(); + mHeaderView.startLaunchTaskDismissAnimation(); // If this is another view in the task grouping and is in front of the launch task, // animate it away first if (occludesLaunchTarget) { @@ -480,7 +495,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } /** Animates the deletion of this task view */ - public void startDeleteTaskAnimation(final Runnable r) { + void startDeleteTaskAnimation(final Runnable r) { // Disabling clipping with the stack while the view is animating away setClipViewInStack(false); @@ -508,19 +523,33 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, } /** Animates this task view if the user does not interact with the stack after a certain time. */ - public void startNoUserInteractionAnimation() { - mBarView.startNoUserInteractionAnimation(); + void startNoUserInteractionAnimation() { + mHeaderView.startNoUserInteractionAnimation(); } /** Mark this task view that the user does has not interacted with the stack after a certain time. */ - public void setNoUserInteractionState() { - mBarView.setNoUserInteractionState(); + void setNoUserInteractionState() { + mHeaderView.setNoUserInteractionState(); + } + + /** Dismisses this task. */ + void dismissTask() { + // Animate out the view and call the callback + final TaskView tv = this; + startDeleteTaskAnimation(new Runnable() { + @Override + public void run() { + mCb.onTaskViewDismissed(tv); + } + }); + // Hide the footer + animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration); } /** Sets whether this task view is full screen or not. */ void setIsFullScreen(boolean isFullscreen) { mIsFullScreenView = isFullscreen; - mBarView.setIsFullscreen(isFullscreen); + mHeaderView.setIsFullscreen(isFullscreen); if (isFullscreen) { // If we are full screen, then disable the bottom outline clip for the footer mViewBounds.setOutlineClipBottom(0); @@ -614,6 +643,12 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, */ public void setFocusedTask() { mIsFocused = true; + if (mFocusAnimationsEnabled) { + // Focus the header bar + mHeaderView.onTaskViewFocusChanged(true); + } + // Call the callback + mCb.onTaskViewFocusChanged(this, true); // Workaround, we don't always want it focusable in touch mode, but we want the first task // to be focused after the enter-recents animation, which can be triggered from either touch // or keyboard @@ -631,6 +666,12 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (!gainFocus) { mIsFocused = false; + if (mFocusAnimationsEnabled) { + // Un-focus the header bar + mHeaderView.onTaskViewFocusChanged(false); + } + // Call the callback + mCb.onTaskViewFocusChanged(this, false); invalidate(); } } @@ -642,6 +683,16 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, return mIsFocused || isFocused(); } + /** Enables all focus animations. */ + void enableFocusAnimations() { + boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled; + mFocusAnimationsEnabled = true; + if (mIsFocused && !wasFocusAnimationsEnabled) { + // Re-notify the header if we were focused and animations were not previously enabled + mHeaderView.onTaskViewFocusChanged(true); + } + } + /**** TaskCallbacks Implementation ****/ /** Binds this task view to the task */ @@ -662,24 +713,24 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override public void onTaskDataLoaded() { - if (mThumbnailView != null && mBarView != null) { + if (mThumbnailView != null && mHeaderView != null) { // Bind each of the views to the new task data if (mIsFullScreenView) { mThumbnailView.bindToScreenshot(AlternateRecentsComponent.getLastScreenshot()); } else { mThumbnailView.rebindToTask(mTask); } - mBarView.rebindToTask(mTask); + mHeaderView.rebindToTask(mTask); // Rebind any listeners - mBarView.mApplicationIcon.setOnClickListener(this); - mBarView.mDismissButton.setOnClickListener(this); + mHeaderView.mApplicationIcon.setOnClickListener(this); + mHeaderView.mDismissButton.setOnClickListener(this); if (mFooterView != null) { mFooterView.setOnClickListener(this); } mActionButtonView.setOnClickListener(this); if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { if (mConfig.developerOptionsEnabled) { - mBarView.mApplicationIcon.setOnLongClickListener(this); + mHeaderView.mApplicationIcon.setOnLongClickListener(this); } } } @@ -688,20 +739,20 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override public void onTaskDataUnloaded() { - if (mThumbnailView != null && mBarView != null) { + if (mThumbnailView != null && mHeaderView != null) { // Unbind each of the views from the task data and remove the task callback mTask.setCallbacks(null); mThumbnailView.unbindFromTask(); - mBarView.unbindFromTask(); + mHeaderView.unbindFromTask(); // Unbind any listeners - mBarView.mApplicationIcon.setOnClickListener(null); - mBarView.mDismissButton.setOnClickListener(null); + mHeaderView.mApplicationIcon.setOnClickListener(null); + mHeaderView.mDismissButton.setOnClickListener(null); if (mFooterView != null) { mFooterView.setOnClickListener(null); } mActionButtonView.setOnClickListener(null); if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { - mBarView.mApplicationIcon.setOnLongClickListener(null); + mHeaderView.mApplicationIcon.setOnLongClickListener(null); } } mTaskDataLoaded = false; @@ -734,18 +785,10 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, postDelayed(new Runnable() { @Override public void run() { - if (Constants.DebugFlags.App.EnableTaskFiltering && v == mBarView.mApplicationIcon) { + if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) { mCb.onTaskViewAppIconClicked(tv); - } else if (v == mBarView.mDismissButton) { - // Animate out the view and call the callback - startDeleteTaskAnimation(new Runnable() { - @Override - public void run() { - mCb.onTaskViewDismissed(tv); - } - }); - // Hide the footer - tv.animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration); + } else if (v == mHeaderView.mDismissButton) { + dismissTask(); } else { mCb.onTaskViewClicked(tv, tv.getTask(), (v == mFooterView || v == mActionButtonView)); @@ -758,7 +801,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override public boolean onLongClick(View v) { - if (v == mBarView.mApplicationIcon) { + if (v == mHeaderView.mApplicationIcon) { mCb.onTaskViewAppInfoClicked(this); return true; } 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 03fc16ee637f9..4b095493bd699 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java @@ -16,9 +16,16 @@ package com.android.systemui.recents.views; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; @@ -28,12 +35,14 @@ import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; +import android.view.ViewOutlineProvider; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.Task; @@ -46,10 +55,15 @@ class TaskViewHeader extends FrameLayout { ImageView mApplicationIcon; TextView mActivityDescription; + RippleDrawable mBackground; + ColorDrawable mBackgroundColor; Drawable mLightDismissDrawable; Drawable mDarkDismissDrawable; + ValueAnimator mBackgroundColorAnimator; boolean mIsFullscreen; + boolean mCurrentPrimaryColorIsDark; + int mCurrentPrimaryColor; static Paint sHighlightPaint; @@ -69,6 +83,13 @@ class TaskViewHeader extends FrameLayout { super(context, attrs, defStyleAttr, defStyleRes); mConfig = RecentsConfiguration.getInstance(); setWillNotDraw(false); + setClipToOutline(true); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); + } + }); // Load the dismiss resources Resources res = context.getResources(); @@ -107,6 +128,15 @@ class TaskViewHeader extends FrameLayout { mApplicationIcon.setBackground(null); } } + + mBackgroundColor = new ColorDrawable(0); + // Copy the ripple drawable since we are going to be manipulating it + mBackground = (RippleDrawable) + getResources().getDrawable(R.drawable.recents_task_view_header_bg); + mBackground = (RippleDrawable) mBackground.mutate().getConstantState().newDrawable(); + mBackground.setColor(ColorStateList.valueOf(0)); + mBackground.setDrawableByLayerId(mBackground.getId(0), mBackgroundColor); + setBackground(mBackground); } @Override @@ -130,6 +160,12 @@ class TaskViewHeader extends FrameLayout { return false; } + /** Returns the secondary color for a primary color. */ + int getSecondaryColor(int primaryColor, boolean useLightOverlayColor) { + int overlayColor = useLightOverlayColor ? Color.WHITE : Color.BLACK; + return Utilities.getColorWithOverlay(primaryColor, overlayColor, 0.8f); + } + /** Binds the bar view to the task */ void rebindToTask(Task t) { // If an activity icon is defined, then we use that as the primary icon to show in the bar, @@ -147,8 +183,10 @@ class TaskViewHeader extends FrameLayout { int existingBgColor = (getBackground() instanceof ColorDrawable) ? ((ColorDrawable) getBackground()).getColor() : 0; if (existingBgColor != t.colorPrimary) { - setBackgroundColor(t.colorPrimary); + mBackgroundColor.setColor(t.colorPrimary); } + mCurrentPrimaryColor = t.colorPrimary; + mCurrentPrimaryColorIsDark = t.useLightOnPrimaryColor; mActivityDescription.setTextColor(t.useLightOnPrimaryColor ? mConfig.taskBarViewLightTextColor : mConfig.taskBarViewDarkTextColor); mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ? @@ -165,12 +203,12 @@ class TaskViewHeader extends FrameLayout { /** Prepares this task view for the enter-recents animations. This is called earlier in the * first layout because the actual animation into recents may take a long time. */ - public void prepareEnterRecentsAnimation() { + void prepareEnterRecentsAnimation() { setVisibility(View.INVISIBLE); } /** Animates this task bar as it enters recents */ - public void startEnterRecentsAnimation(int delay, Runnable postAnimRunnable) { + void startEnterRecentsAnimation(int delay, Runnable postAnimRunnable) { // Animate the task bar of the first task view setVisibility(View.VISIBLE); setTranslationY(-getMeasuredHeight()); @@ -184,7 +222,7 @@ class TaskViewHeader extends FrameLayout { } /** Animates this task bar as it exits recents */ - public void startLaunchTaskAnimation(Runnable preAnimRunnable, final Runnable postAnimRunnable) { + void startLaunchTaskAnimation(Runnable preAnimRunnable, final Runnable postAnimRunnable) { // Animate the task bar out of the first task view animate() .translationY(-getMeasuredHeight()) @@ -202,7 +240,7 @@ class TaskViewHeader extends FrameLayout { } /** Animates this task bar dismiss button when launching a task. */ - public void startLaunchTaskDismissAnimation() { + void startLaunchTaskDismissAnimation() { if (mDismissButton.getVisibility() == View.VISIBLE) { mDismissButton.animate().cancel(); mDismissButton.animate() @@ -216,7 +254,7 @@ class TaskViewHeader extends FrameLayout { } /** Animates this task bar if the user does not interact with the stack after a certain time. */ - public void startNoUserInteractionAnimation() { + void startNoUserInteractionAnimation() { mDismissButton.setVisibility(View.VISIBLE); mDismissButton.setAlpha(0f); mDismissButton.animate() @@ -229,11 +267,78 @@ class TaskViewHeader extends FrameLayout { } /** Mark this task view that the user does has not interacted with the stack after a certain time. */ - public void setNoUserInteractionState() { + void setNoUserInteractionState() { if (mDismissButton.getVisibility() != View.VISIBLE) { mDismissButton.animate().cancel(); mDismissButton.setVisibility(View.VISIBLE); mDismissButton.setAlpha(1f); } } + + /** Notifies the associated TaskView has been focused. */ + void onTaskViewFocusChanged(boolean focused) { + boolean isRunning = false; + if (mBackgroundColorAnimator != null) { + isRunning = mBackgroundColorAnimator.isRunning(); + mBackgroundColorAnimator.removeAllUpdateListeners(); + mBackgroundColorAnimator.cancel(); + } + if (focused) { + int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); + int[][] states = new int[][] { + new int[] { android.R.attr.state_enabled }, + new int[] { android.R.attr.state_pressed } + }; + int[] newStates = new int[]{ + android.R.attr.state_enabled, + android.R.attr.state_pressed + }; + int[] colors = new int[] { + secondaryColor, + secondaryColor + }; + mBackground.setColor(new ColorStateList(states, colors)); + mBackground.setState(newStates); + // Pulse the background color + int currentColor = mBackgroundColor.getColor(); + int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); + mBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), lightPrimaryColor, + currentColor); + mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mBackground.setState(new int[] {}); + } + }); + mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mBackgroundColor.setColor((Integer) animation.getAnimatedValue()); + } + }); + mBackgroundColorAnimator.setRepeatCount(ValueAnimator.INFINITE); + mBackgroundColorAnimator.setRepeatMode(ValueAnimator.REVERSE); + mBackgroundColorAnimator.setStartDelay(750); + mBackgroundColorAnimator.setDuration(750); + mBackgroundColorAnimator.start(); + } else { + if (isRunning) { + // Restore the background color + int currentColor = mBackgroundColor.getColor(); + mBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), currentColor, + mCurrentPrimaryColor); + mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mBackgroundColor.setColor((Integer) animation.getAnimatedValue()); + } + }); + mBackgroundColorAnimator.setRepeatCount(0); + mBackgroundColorAnimator.setDuration(150); + mBackgroundColorAnimator.start(); + } else { + mBackground.setState(new int[] {}); + } + } + } }