• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.quickstep.views;
17 
18 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
19 
20 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
21 
22 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
23 import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
24 import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
25 import static com.android.quickstep.TaskAdapter.ITEM_TYPE_TASK;
26 import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY;
27 import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
28 import static com.android.quickstep.util.RemoteAnimationProvider.getLayer;
29 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
30 
31 import android.animation.Animator;
32 import android.animation.AnimatorListenerAdapter;
33 import android.animation.AnimatorSet;
34 import android.animation.ObjectAnimator;
35 import android.animation.PropertyValuesHolder;
36 import android.animation.ValueAnimator;
37 import android.content.Context;
38 import android.content.res.Resources;
39 import android.graphics.Matrix;
40 import android.graphics.Rect;
41 import android.graphics.RectF;
42 import android.graphics.drawable.Drawable;
43 import android.util.ArraySet;
44 import android.util.AttributeSet;
45 import android.util.FloatProperty;
46 import android.view.View;
47 import android.view.ViewDebug;
48 import android.view.ViewTreeObserver;
49 import android.view.animation.PathInterpolator;
50 import android.widget.FrameLayout;
51 
52 import androidx.annotation.NonNull;
53 import androidx.annotation.Nullable;
54 import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
55 import androidx.recyclerview.widget.DefaultItemAnimator;
56 import androidx.recyclerview.widget.ItemTouchHelper;
57 import androidx.recyclerview.widget.LinearLayoutManager;
58 import androidx.recyclerview.widget.RecyclerView;
59 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
60 import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
61 import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
62 
63 import com.android.launcher3.BaseActivity;
64 import com.android.launcher3.Insettable;
65 import com.android.launcher3.R;
66 import com.android.launcher3.util.Themes;
67 import com.android.quickstep.ContentFillItemAnimator;
68 import com.android.quickstep.RecentsModel;
69 import com.android.quickstep.RecentsToActivityHelper;
70 import com.android.quickstep.TaskActionController;
71 import com.android.quickstep.TaskAdapter;
72 import com.android.quickstep.TaskHolder;
73 import com.android.quickstep.TaskListLoader;
74 import com.android.quickstep.TaskSwipeCallback;
75 import com.android.quickstep.util.MultiValueUpdateListener;
76 import com.android.systemui.shared.recents.model.Task;
77 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
78 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
79 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
80 
81 import java.util.ArrayList;
82 import java.util.List;
83 import java.util.Objects;
84 import java.util.Optional;
85 
86 /**
87  * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
88  * base.
89  */
90 public final class IconRecentsView extends FrameLayout implements Insettable {
91 
92     public static final FloatProperty<IconRecentsView> CONTENT_ALPHA =
93             new FloatProperty<IconRecentsView>("contentAlpha") {
94                 @Override
95                 public void setValue(IconRecentsView view, float v) {
96                     ALPHA.set(view, v);
97                     if (view.getVisibility() != VISIBLE && v > 0) {
98                         view.setVisibility(VISIBLE);
99                     } else if (view.getVisibility() != GONE && v == 0){
100                         view.setVisibility(GONE);
101                     }
102                 }
103 
104                 @Override
105                 public Float get(IconRecentsView view) {
106                     return ALPHA.get(view);
107                 }
108             };
109     private static final long CROSSFADE_DURATION = 300;
110     private static final long LAYOUT_ITEM_ANIMATE_IN_DURATION = 150;
111     private static final long LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN = 40;
112     private static final long ITEM_ANIMATE_OUT_DURATION = 150;
113     private static final long ITEM_ANIMATE_OUT_DELAY_BETWEEN = 40;
114     private static final float ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO = .25f;
115     private static final long CLEAR_ALL_FADE_DELAY = 120;
116 
117     private static final long REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION = 300;
118     private static final long REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION = 400;
119     private static final long REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY = 200;
120     private static final long REMOTE_TO_RECENTS_ITEM_FADE_DURATION = 217;
121     private static final long REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY = 33;
122 
123     private static final PathInterpolator FAST_OUT_SLOW_IN_1 =
124             new PathInterpolator(.4f, 0f, 0f, 1f);
125     private static final PathInterpolator FAST_OUT_SLOW_IN_2 =
126             new PathInterpolator(.5f, 0f, 0f, 1f);
127     private static final LinearOutSlowInInterpolator OUT_SLOW_IN =
128             new LinearOutSlowInInterpolator();
129 
130     public static final long REMOTE_APP_TO_OVERVIEW_DURATION =
131             REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION;
132 
133     /**
134      * A ratio representing the view's relative placement within its padded space. For example, 0
135      * is top aligned and 0.5 is centered vertically.
136      */
137     @ViewDebug.ExportedProperty(category = "launcher")
138 
139     private final Context mContext;
140     private final TaskListLoader mTaskLoader;
141     private final TaskAdapter mTaskAdapter;
142     private final LinearLayoutManager mTaskLayoutManager;
143     private final TaskActionController mTaskActionController;
144     private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
145     private final ContentFillItemAnimator mLoadingContentItemAnimator =
146             new ContentFillItemAnimator();
147     private final BaseActivity mActivity;
148     private final Drawable mStatusBarForegroundScrim;
149 
150     private RecentsToActivityHelper mActivityHelper;
151     private RecyclerView mTaskRecyclerView;
152     private View mShowingContentView;
153     private View mEmptyView;
154     private View mContentView;
155     private boolean mTransitionedFromApp;
156     private boolean mUsingRemoteAnimation;
157     private boolean mStartedEnterAnimation;
158     private boolean mShowStatusBarForegroundScrim;
159     private AnimatorSet mLayoutAnimation;
160     private final ArraySet<View> mLayingOutViews = new ArraySet<>();
161     private Rect mInsets;
162     private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
163         ArrayList<TaskItemView> itemViews = getTaskViews();
164         for (int i = 0, size = itemViews.size(); i < size; i++) {
165             TaskItemView taskView = itemViews.get(i);
166             TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
167             Optional<Task> optTask = taskHolder.getTask();
168             if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
169                 Task task = optTask.get();
170                 // Update thumbnail on the task.
171                 task.thumbnail = thumbnailData;
172                 taskView.setThumbnail(thumbnailData);
173                 return task;
174             }
175         }
176         return null;
177     };
178 
IconRecentsView(Context context, AttributeSet attrs)179     public IconRecentsView(Context context, AttributeSet attrs) {
180         super(context, attrs);
181         mActivity = BaseActivity.fromContext(context);
182         mContext = context;
183         mStatusBarForegroundScrim  =
184                 Themes.getAttrDrawable(mContext, R.attr.workspaceStatusBarScrim);
185         mTaskLoader = new TaskListLoader(mContext);
186         mTaskAdapter = new TaskAdapter(mTaskLoader);
187         mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
188         mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter,
189                 mActivity.getStatsLogManager());
190         mTaskAdapter.setActionController(mTaskActionController);
191         mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
192         RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
193     }
194 
195     @Override
onFinishInflate()196     protected void onFinishInflate() {
197         super.onFinishInflate();
198         if (mTaskRecyclerView == null) {
199             mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
200             mTaskRecyclerView.setAdapter(mTaskAdapter);
201             mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
202             ItemTouchHelper helper = new ItemTouchHelper(
203                     new TaskSwipeCallback(holder -> {
204                         mTaskActionController.removeTask(holder);
205                         if (mTaskLoader.getCurrentTaskList().isEmpty()) {
206                             mActivityHelper.leaveRecents();
207                         }
208                     }));
209             helper.attachToRecyclerView(mTaskRecyclerView);
210             mTaskRecyclerView.addOnChildAttachStateChangeListener(
211                     new OnChildAttachStateChangeListener() {
212                         @Override
213                         public void onChildViewAttachedToWindow(@NonNull View view) {
214                             if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
215                                 // Child view was added that is not part of current layout animation
216                                 // so restart the animation.
217                                 animateFadeInLayoutAnimation();
218                             }
219                         }
220 
221                         @Override
222                         public void onChildViewDetachedFromWindow(@NonNull View view) { }
223                     });
224             mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
225             mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
226                     () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
227             ItemDecoration marginDecorator = new ItemDecoration() {
228                 @Override
229                 public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
230                         @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
231                     // TODO: Determine if current margins cause off screen item to be fully off
232                     // screen and if so, modify them so that it is partially off screen.
233                     int itemType = parent.getChildViewHolder(view).getItemViewType();
234                     Resources res = getResources();
235                     switch (itemType) {
236                         case ITEM_TYPE_CLEAR_ALL:
237                             outRect.top = (int) res.getDimension(
238                                     R.dimen.clear_all_item_view_top_margin);
239                             outRect.bottom = (int) res.getDimension(
240                                     R.dimen.clear_all_item_view_bottom_margin);
241                             break;
242                         case ITEM_TYPE_TASK:
243                             int desiredTopMargin = (int) res.getDimension(
244                                     R.dimen.task_item_top_margin);
245                             if (mTaskRecyclerView.getChildAdapterPosition(view) ==
246                                     state.getItemCount() - 1) {
247                                 // Only add top margin to top task view if insets aren't enough.
248                                 if (mInsets.top < desiredTopMargin) {
249                                     outRect.top = desiredTopMargin - mInsets.bottom;
250                                 }
251                                 return;
252                             }
253                             outRect.top = desiredTopMargin;
254                             break;
255                         default:
256                     }
257                 }
258             };
259             mTaskRecyclerView.addItemDecoration(marginDecorator);
260 
261             mEmptyView = findViewById(R.id.recent_task_empty_view);
262             mContentView = mTaskRecyclerView;
263             mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
264                 @Override
265                 public void onChanged() {
266                     updateContentViewVisibility();
267                 }
268 
269                 @Override
270                 public void onItemRangeRemoved(int positionStart, int itemCount) {
271                     updateContentViewVisibility();
272                 }
273             });
274         }
275     }
276 
277     @Override
setEnabled(boolean enabled)278     public void setEnabled(boolean enabled) {
279         super.setEnabled(enabled);
280         int childCount = mTaskRecyclerView.getChildCount();
281         for (int i = 0; i < childCount; i++) {
282             mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
283         }
284     }
285 
286     /**
287      * Set activity helper for the view to callback to.
288      *
289      * @param helper the activity helper
290      */
setRecentsToActivityHelper(@onNull RecentsToActivityHelper helper)291     public void setRecentsToActivityHelper(@NonNull RecentsToActivityHelper helper) {
292         mActivityHelper = helper;
293     }
294 
295     /**
296      * Logic for when we know we are going to overview/recents and will be putting up the recents
297      * view. This should be used to prepare recents (e.g. load any task data, etc.) before it
298      * becomes visible.
299      */
onBeginTransitionToOverview()300     public void onBeginTransitionToOverview() {
301         mStartedEnterAnimation = false;
302         if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
303             // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
304             // all tasks should be visible to fill up the screen in portrait mode and the view will
305             // not be scrollable.
306             mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
307         }
308         if (!mUsingRemoteAnimation) {
309             scheduleFadeInLayoutAnimation();
310         }
311         // Load any task changes
312         if (!mTaskLoader.needsToLoad()) {
313             return;
314         }
315         mTaskAdapter.setIsShowingLoadingUi(true);
316         mTaskAdapter.notifyDataSetChanged();
317         mTaskLoader.loadTaskList(tasks -> {
318             int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
319             mTaskAdapter.setIsShowingLoadingUi(false);
320             int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
321             if (numEmptyItems < numActualItems) {
322                 throw new IllegalStateException("There are less empty item views than the number "
323                         + "of items to animate to.");
324             }
325             // Possible that task list loads faster than adapter changes propagate to layout so
326             // only start content fill animation if there aren't any pending adapter changes and
327             // we've started the on enter layout animation.
328             boolean needsContentFillAnimation =
329                     !mTaskRecyclerView.hasPendingAdapterUpdates() && mStartedEnterAnimation;
330             if (needsContentFillAnimation) {
331                 // Set item animator for content filling animation. The item animator will switch
332                 // back to the default on completion
333                 mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
334                 mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
335                         numEmptyItems - numActualItems);
336                 mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
337                         CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
338             } else {
339                 // Notify change without animating.
340                 mTaskAdapter.notifyDataSetChanged();
341             }
342         });
343     }
344 
345     /**
346      * Set whether we transitioned to recents from the most recent app.
347      *
348      * @param transitionedFromApp true if transitioned from the most recent app, false otherwise
349      */
setTransitionedFromApp(boolean transitionedFromApp)350     public void setTransitionedFromApp(boolean transitionedFromApp) {
351         mTransitionedFromApp = transitionedFromApp;
352     }
353 
354     /**
355      * Set whether we're using a custom remote animation. If so, we will not do the default layout
356      * animation when entering recents and instead wait for the remote app surface to be ready to
357      * use.
358      *
359      * @param usingRemoteAnimation true if doing a remote animation, false o/w
360      */
setUsingRemoteAnimation(boolean usingRemoteAnimation)361     public void setUsingRemoteAnimation(boolean usingRemoteAnimation) {
362         mUsingRemoteAnimation = usingRemoteAnimation;
363     }
364 
365     /**
366      * Handles input from the overview button. Launch the most recent task unless we just came from
367      * the app. In that case, we launch the next most recent.
368      */
handleOverviewCommand()369     public void handleOverviewCommand() {
370         List<Task> tasks = mTaskLoader.getCurrentTaskList();
371         int tasksSize = tasks.size();
372         if (tasksSize == 0) {
373             // Do nothing
374             return;
375         }
376         Task taskToLaunch;
377         if (mTransitionedFromApp && tasksSize > 1) {
378             // Launch the next most recent app
379             taskToLaunch = tasks.get(1);
380         } else {
381             // Launch the most recent app
382             taskToLaunch = tasks.get(0);
383         }
384 
385         // See if view for this task is attached, and if so, animate launch from that view.
386         ArrayList<TaskItemView> itemViews = getTaskViews();
387         for (int i = 0, size = itemViews.size(); i < size; i++) {
388             TaskItemView taskView = itemViews.get(i);
389             TaskHolder holder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
390             if (Objects.equals(holder.getTask(), Optional.of(taskToLaunch))) {
391                 mTaskActionController.launchTaskFromView(holder);
392                 return;
393             }
394         }
395 
396         // Otherwise, just use a basic launch animation.
397         mTaskActionController.launchTask(taskToLaunch);
398     }
399 
400     /**
401      * Set whether or not to show the scrim in between the view and the top insets. This only works
402      * if the view is being insetted in the first place.
403      *
404      * The scrim is added to the activity's root view to prevent animations on this view
405      * affecting the scrim. As a result, it is the activity's responsibility to show/hide this
406      * scrim as appropriate.
407      *
408      * @param showStatusBarForegroundScrim true to show the scrim, false to hide
409      */
setShowStatusBarForegroundScrim(boolean showStatusBarForegroundScrim)410     public void setShowStatusBarForegroundScrim(boolean showStatusBarForegroundScrim) {
411         mShowStatusBarForegroundScrim = showStatusBarForegroundScrim;
412         if (mShowStatusBarForegroundScrim != showStatusBarForegroundScrim) {
413             updateStatusBarScrim();
414         }
415     }
416 
updateStatusBarScrim()417     private void updateStatusBarScrim() {
418         boolean shouldShow = mInsets.top != 0 && mShowStatusBarForegroundScrim;
419         mActivity.getDragLayer().setForeground(shouldShow ? mStatusBarForegroundScrim : null);
420     }
421 
422     /**
423      * Get the bottom most task view to animate to.
424      *
425      * @return the task view
426      */
getBottomTaskView()427     private @Nullable TaskItemView getBottomTaskView() {
428         int childCount = mTaskRecyclerView.getChildCount();
429         for (int i = 0; i < childCount; i++) {
430             View view = mTaskRecyclerView.getChildAt(i);
431             if (mTaskRecyclerView.getChildViewHolder(view).getItemViewType() == ITEM_TYPE_TASK) {
432                 return (TaskItemView) view;
433             }
434         }
435         return null;
436     }
437 
438     /**
439      * Whether this view has processed all data changes and is ready to animate from the app to
440      * the overview.
441      *
442      * @return true if ready to animate app to overview, false otherwise
443      */
isReadyForRemoteAnim()444     public boolean isReadyForRemoteAnim() {
445         return !mTaskRecyclerView.hasPendingAdapterUpdates();
446     }
447 
448     /**
449      * Set a callback for whenever this view is ready to do a remote animation from the app to
450      * overview. See {@link #isReadyForRemoteAnim()}.
451      *
452      * @param callback callback to run when view is ready to animate
453      */
setOnReadyForRemoteAnimCallback(onReadyForRemoteAnimCallback callback)454     public void setOnReadyForRemoteAnimCallback(onReadyForRemoteAnimCallback callback) {
455         mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
456                 new ViewTreeObserver.OnGlobalLayoutListener() {
457                     @Override
458                     public void onGlobalLayout() {
459                         if (isReadyForRemoteAnim()) {
460                             callback.onReadyForRemoteAnim();
461                             mTaskRecyclerView.getViewTreeObserver().
462                                     removeOnGlobalLayoutListener(this);
463                         }
464                     }
465                 });
466     }
467 
468     /**
469      * Clear all tasks and animate out.
470      */
animateClearAllTasks()471     private void animateClearAllTasks() {
472         setEnabled(false);
473         ArrayList<TaskItemView> itemViews = getTaskViews();
474 
475         AnimatorSet clearAnim = new AnimatorSet();
476         long currentDelay = 0;
477 
478         // Animate each item view to the right and fade out.
479         for (int i = 0, size = itemViews.size(); i < size; i++) {
480             TaskItemView itemView = itemViews.get(i);
481             PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
482                     0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
483             PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
484             ObjectAnimator itemAnim = ObjectAnimator.ofPropertyValuesHolder(itemView,
485                     transXproperty, alphaProperty);
486             itemAnim.setDuration(ITEM_ANIMATE_OUT_DURATION);
487             itemAnim.setStartDelay(currentDelay);
488 
489             clearAnim.play(itemAnim);
490             currentDelay += ITEM_ANIMATE_OUT_DELAY_BETWEEN;
491         }
492 
493         // Animate view fading and leave recents when faded enough.
494         ValueAnimator contentAlpha = ValueAnimator.ofFloat(1.0f, 0f)
495                 .setDuration(CROSSFADE_DURATION);
496         contentAlpha.setStartDelay(CLEAR_ALL_FADE_DELAY);
497         contentAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
498             private boolean mLeftRecents = false;
499 
500             @Override
501             public void onAnimationUpdate(ValueAnimator valueAnimator) {
502                 mContentView.setAlpha((float) valueAnimator.getAnimatedValue());
503                 // Leave recents while fading out.
504                 if ((float) valueAnimator.getAnimatedValue() < .5f && !mLeftRecents) {
505                     mActivityHelper.leaveRecents();
506                     mLeftRecents = true;
507                 }
508             }
509         });
510 
511         clearAnim.play(contentAlpha);
512         clearAnim.addListener(new AnimatorListenerAdapter() {
513             @Override
514             public void onAnimationEnd(Animator animation) {
515                 for (int i = 0, size = itemViews.size(); i < size; i++) {
516                     TaskItemView itemView = itemViews.get(i);
517                     itemView.setTranslationX(0);
518                     itemView.setAlpha(1.0f);
519                 }
520                 setEnabled(true);
521                 mContentView.setVisibility(GONE);
522                 mTaskActionController.clearAllTasks();
523             }
524         });
525         clearAnim.start();
526     }
527 
528     /**
529      * Get attached task item views ordered by most recent.
530      *
531      * @return array list of attached task item views
532      */
getTaskViews()533     private ArrayList<TaskItemView> getTaskViews() {
534         int taskCount = mTaskRecyclerView.getChildCount();
535         ArrayList<TaskItemView> itemViews = new ArrayList<>();
536         for (int i = 0; i < taskCount; i ++) {
537             View child = mTaskRecyclerView.getChildAt(i);
538             if (child instanceof TaskItemView) {
539                 itemViews.add((TaskItemView) child);
540             }
541         }
542         return itemViews;
543     }
544 
545     /**
546      * Update the content view so that the appropriate view is shown based off the current list
547      * of tasks.
548      */
updateContentViewVisibility()549     private void updateContentViewVisibility() {
550         int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
551         if (mShowingContentView != mEmptyView && taskListSize == 0) {
552             mShowingContentView = mEmptyView;
553             crossfadeViews(mEmptyView, mContentView);
554         }
555         if (mShowingContentView != mContentView && taskListSize > 0) {
556             mShowingContentView = mContentView;
557             crossfadeViews(mContentView, mEmptyView);
558         }
559     }
560 
561     /**
562      * Animate views so that one view fades in while the other fades out.
563      *
564      * @param fadeInView view that should fade in
565      * @param fadeOutView view that should fade out
566      */
crossfadeViews(View fadeInView, View fadeOutView)567     private void crossfadeViews(View fadeInView, View fadeOutView) {
568         fadeInView.animate().cancel();
569         fadeInView.setVisibility(VISIBLE);
570         fadeInView.setAlpha(0f);
571         fadeInView.animate()
572                 .alpha(1f)
573                 .setDuration(CROSSFADE_DURATION)
574                 .setListener(null);
575 
576         fadeOutView.animate().cancel();
577         fadeOutView.animate()
578                 .alpha(0f)
579                 .setDuration(CROSSFADE_DURATION)
580                 .setListener(new AnimatorListenerAdapter() {
581                     @Override
582                     public void onAnimationEnd(Animator animation) {
583                         fadeOutView.setVisibility(GONE);
584                     }
585                 });
586     }
587 
588     /**
589      * Schedule a one-shot layout animation on the next layout. Separate from
590      * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
591      * view properties themselves, allowing more controllable behavior and making it easier to
592      * manage when the animation conflicts with another animation.
593      */
scheduleFadeInLayoutAnimation()594     private void scheduleFadeInLayoutAnimation() {
595         mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
596                 new ViewTreeObserver.OnGlobalLayoutListener() {
597                     @Override
598                     public void onGlobalLayout() {
599                         animateFadeInLayoutAnimation();
600                         mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
601                     }
602                 });
603     }
604 
605     /**
606      * Start animating the layout animation where items fade in.
607      */
animateFadeInLayoutAnimation()608     private void animateFadeInLayoutAnimation() {
609         if (mLayoutAnimation != null) {
610             // If layout animation still in progress, cancel and restart.
611             mLayoutAnimation.cancel();
612         }
613         ArrayList<TaskItemView> views = getTaskViews();
614         int delay = 0;
615         mLayoutAnimation = new AnimatorSet();
616         for (int i = 0, size = views.size(); i < size; i++) {
617             TaskItemView view = views.get(i);
618             view.setAlpha(0.0f);
619             Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
620             alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
621             alphaAnim.addListener(new AnimatorListenerAdapter() {
622                 @Override
623                 public void onAnimationEnd(Animator animation) {
624                     view.setAlpha(1.0f);
625                     mLayingOutViews.remove(view);
626                 }
627             });
628             delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
629             mLayoutAnimation.play(alphaAnim);
630             mLayingOutViews.add(view);
631         }
632         mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
633             @Override
634             public void onAnimationEnd(Animator animation) {
635                 mLayoutAnimation = null;
636             }
637         });
638         mLayoutAnimation.start();
639         mStartedEnterAnimation = true;
640     }
641 
642     /**
643      * Play remote app to recents animation when the app is the home activity. We use a simple
644      * cross-fade here. Note this is only used if the home activity is a separate app than the
645      * recents activity.
646      *
647      * @param anim animator set
648      * @param homeTarget the home surface thats closing
649      * @param recentsTarget the surface containing recents
650      */
playRemoteHomeToRecentsAnimation(@onNull AnimatorSet anim, @NonNull RemoteAnimationTargetCompat homeTarget, @NonNull RemoteAnimationTargetCompat recentsTarget)651     public void playRemoteHomeToRecentsAnimation(@NonNull AnimatorSet anim,
652             @NonNull RemoteAnimationTargetCompat homeTarget,
653             @NonNull RemoteAnimationTargetCompat recentsTarget) {
654         SyncRtSurfaceTransactionApplierCompat surfaceApplier =
655                 new SyncRtSurfaceTransactionApplierCompat(this);
656 
657         SurfaceParams[] params = new SurfaceParams[2];
658         int boostedMode = MODE_CLOSING;
659 
660         ValueAnimator remoteHomeAnim = ValueAnimator.ofFloat(0, 1);
661         remoteHomeAnim.setDuration(REMOTE_APP_TO_OVERVIEW_DURATION);
662 
663         remoteHomeAnim.addUpdateListener(valueAnimator -> {
664             float val = (float) valueAnimator.getAnimatedValue();
665             float alpha;
666             RemoteAnimationTargetCompat visibleTarget;
667             RemoteAnimationTargetCompat invisibleTarget;
668             if (val < .5f) {
669                 visibleTarget = homeTarget;
670                 invisibleTarget = recentsTarget;
671                 alpha = 1 - (val * 2);
672             } else {
673                 visibleTarget = recentsTarget;
674                 invisibleTarget = homeTarget;
675                 alpha = (val - .5f) * 2;
676             }
677             params[0] = new SurfaceParams(visibleTarget.leash, alpha, null /* matrix */,
678                     null /* windowCrop */, getLayer(visibleTarget, boostedMode),
679                     0 /* cornerRadius */);
680             params[1] = new SurfaceParams(invisibleTarget.leash, 0.0f, null /* matrix */,
681                     null /* windowCrop */, getLayer(invisibleTarget, boostedMode),
682                     0 /* cornerRadius */);
683             surfaceApplier.scheduleApply(params);
684         });
685         anim.play(remoteHomeAnim);
686         animateFadeInLayoutAnimation();
687     }
688 
689     /**
690      * Play remote animation from app to recents. This should scale the currently closing app down
691      * to the recents thumbnail.
692      *
693      * @param anim animator set
694      * @param appTarget the app surface thats closing
695      * @param recentsTarget the surface containing recents
696      */
playRemoteAppToRecentsAnimation(@onNull AnimatorSet anim, @NonNull RemoteAnimationTargetCompat appTarget, @NonNull RemoteAnimationTargetCompat recentsTarget)697     public void playRemoteAppToRecentsAnimation(@NonNull AnimatorSet anim,
698             @NonNull RemoteAnimationTargetCompat appTarget,
699             @NonNull RemoteAnimationTargetCompat recentsTarget) {
700         TaskItemView bottomView = getBottomTaskView();
701         if (bottomView == null) {
702             // This can be null if there were previously 0 tasks and the recycler view has not had
703             // enough time to take in the data change, bind a new view, and lay out the new view.
704             // TODO: Have a fallback to animate to
705             anim.play(ValueAnimator.ofInt(0, 1).setDuration(REMOTE_APP_TO_OVERVIEW_DURATION));
706             return;
707         }
708         final Matrix appMatrix = new Matrix();
709         playRemoteTransYAnim(anim, appMatrix);
710         playRemoteAppScaleDownAnim(anim, appMatrix, appTarget, recentsTarget,
711                 bottomView.getThumbnailView());
712         playRemoteTaskListFadeIn(anim, bottomView);
713         mStartedEnterAnimation = true;
714     }
715 
716     /**
717      * Play translation Y animation for the remote app to recents animation. Animates over all task
718      * views as well as the closing app, easing them into their final vertical positions.
719      *
720      * @param anim animator set to play on
721      * @param appMatrix transformation matrix for the closing app surface
722      */
playRemoteTransYAnim(@onNull AnimatorSet anim, @NonNull Matrix appMatrix)723     private void playRemoteTransYAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix) {
724         final ArrayList<TaskItemView> views = getTaskViews();
725 
726         // Start Y translation from about halfway through the tasks list to the bottom thumbnail.
727         float taskHeight = getResources().getDimension(R.dimen.task_item_height);
728         float totalTransY = -(MAX_TASKS_TO_DISPLAY / 2.0f - 1) * taskHeight;
729         for (int i = 0, size = views.size(); i < size; i++) {
730             views.get(i).setTranslationY(totalTransY);
731         }
732 
733         ValueAnimator transYAnim = ValueAnimator.ofFloat(totalTransY, 0);
734         transYAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION);
735         transYAnim.setInterpolator(FAST_OUT_SLOW_IN_2);
736         transYAnim.addUpdateListener(valueAnimator -> {
737             float transY = (float) valueAnimator.getAnimatedValue();
738             for (int i = 0, size = views.size(); i < size; i++) {
739                 views.get(i).setTranslationY(transY);
740             }
741             appMatrix.postTranslate(0, transY - totalTransY);
742         });
743         transYAnim.addListener(new AnimatorListenerAdapter() {
744             @Override
745             public void onAnimationEnd(Animator animation) {
746                 for (int i = 0, size = views.size(); i < size; i++) {
747                     views.get(i).setTranslationY(0);
748                 }
749             }
750         });
751         anim.play(transYAnim);
752     }
753 
754     /**
755      * Play the scale down animation for the remote app to recents animation where the app surface
756      * scales down to where the thumbnail is.
757      *
758      * @param anim animator set to play on
759      * @param appMatrix transformation matrix for the app surface
760      * @param appTarget closing app target
761      * @param recentsTarget opening recents target
762      * @param thumbnailView thumbnail view to animate to
763      */
playRemoteAppScaleDownAnim(@onNull AnimatorSet anim, @NonNull Matrix appMatrix, @NonNull RemoteAnimationTargetCompat appTarget, @NonNull RemoteAnimationTargetCompat recentsTarget, @NonNull View thumbnailView)764     private void playRemoteAppScaleDownAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix,
765             @NonNull RemoteAnimationTargetCompat appTarget,
766             @NonNull RemoteAnimationTargetCompat recentsTarget,
767             @NonNull View thumbnailView) {
768         // Identify where the entering remote app should animate to.
769         Rect endRect = new Rect();
770         thumbnailView.getGlobalVisibleRect(endRect);
771         Rect appBounds = appTarget.sourceContainerBounds;
772         RectF currentAppRect = new RectF();
773 
774         SyncRtSurfaceTransactionApplierCompat surfaceApplier =
775                 new SyncRtSurfaceTransactionApplierCompat(this);
776 
777         // Keep recents visible throughout the animation.
778         SurfaceParams[] params = new SurfaceParams[2];
779         // Closing app should stay on top.
780         int boostedMode = MODE_CLOSING;
781         params[0] = new SurfaceParams(recentsTarget.leash, 1f, null /* matrix */,
782                 null /* windowCrop */, getLayer(recentsTarget, boostedMode), 0 /* cornerRadius */);
783 
784         ValueAnimator remoteAppAnim = ValueAnimator.ofInt(0, 1);
785         remoteAppAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION);
786         remoteAppAnim.addUpdateListener(new MultiValueUpdateListener() {
787             private final FloatProp mScaleX;
788             private final FloatProp mScaleY;
789             private final FloatProp mTranslationX;
790             private final FloatProp mTranslationY;
791             private final FloatProp mAlpha;
792 
793             {
794                 // Scale down and move to view location.
795                 float endScaleX = ((float) endRect.width()) / appBounds.width();
796                 mScaleX = new FloatProp(1f, endScaleX, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
797                         FAST_OUT_SLOW_IN_1);
798                 float endScaleY = ((float) endRect.height()) / appBounds.height();
799                 mScaleY = new FloatProp(1f, endScaleY, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
800                         FAST_OUT_SLOW_IN_1);
801                 float endTranslationX = endRect.left -
802                         (appBounds.width() - thumbnailView.getWidth()) / 2.0f;
803                 mTranslationX = new FloatProp(0, endTranslationX, 0,
804                         REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_1);
805                 float endTranslationY = endRect.top -
806                         (appBounds.height() - thumbnailView.getHeight()) / 2.0f;
807                 mTranslationY = new FloatProp(0, endTranslationY, 0,
808                         REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_2);
809                 mAlpha = new FloatProp(1.0f, 0, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
810                         ACCEL_2);
811             }
812 
813             @Override
814             public void onUpdate(float percent) {
815                 Matrix m = new Matrix();
816                 m.preScale(mScaleX.value, mScaleY.value,
817                         appBounds.width() / 2.0f, appBounds.height() / 2.0f);
818                 m.postTranslate(mTranslationX.value, mTranslationY.value);
819                 appMatrix.preConcat(m);
820                 params[1] = new SurfaceParams(appTarget.leash, mAlpha.value, appMatrix,
821                         null /* windowCrop */, getLayer(appTarget, boostedMode),
822                         0 /* cornerRadius */);
823                 surfaceApplier.scheduleApply(params);
824 
825                 m.mapRect(currentAppRect, new RectF(appBounds));
826                 setViewToRect(thumbnailView, new RectF(endRect), currentAppRect);
827                 appMatrix.reset();
828             }
829         });
830         remoteAppAnim.addListener(new AnimatorListenerAdapter() {
831             @Override
832             public void onAnimationEnd(Animator animation) {
833                 thumbnailView.setTranslationY(0);
834                 thumbnailView.setTranslationX(0);
835                 thumbnailView.setScaleX(1);
836                 thumbnailView.setScaleY(1);
837             }
838         });
839         anim.play(remoteAppAnim);
840     }
841 
842     /**
843      * Play task list fade in animation as part of remote app to recents animation. This animation
844      * ensures that the task views in the recents list fade in from bottom to top.
845      *
846      * @param anim animator set to play on
847      * @param appTaskView the task view associated with the remote app closing
848      */
playRemoteTaskListFadeIn(@onNull AnimatorSet anim, @NonNull TaskItemView appTaskView)849     private void playRemoteTaskListFadeIn(@NonNull AnimatorSet anim,
850             @NonNull TaskItemView appTaskView) {
851         long delay = REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY;
852         int childCount = mTaskRecyclerView.getChildCount();
853         for (int i = 0; i < childCount; i++) {
854             ValueAnimator fadeAnim = ValueAnimator.ofFloat(0, 1.0f);
855             fadeAnim.setDuration(REMOTE_TO_RECENTS_ITEM_FADE_DURATION).setInterpolator(OUT_SLOW_IN);
856             fadeAnim.setStartDelay(delay);
857             View view = mTaskRecyclerView.getChildAt(i);
858             if (Objects.equals(view, appTaskView)) {
859                 // Only animate icon and text for the view with snapshot animating in
860                 final View icon = appTaskView.getIconView();
861                 final View label = appTaskView.getLabelView();
862 
863                 icon.setAlpha(0.0f);
864                 label.setAlpha(0.0f);
865 
866                 fadeAnim.addUpdateListener(alphaVal -> {
867                     float val = alphaVal.getAnimatedFraction();
868 
869                     icon.setAlpha(val);
870                     label.setAlpha(val);
871                 });
872                 fadeAnim.addListener(new AnimatorListenerAdapter() {
873                     @Override
874                     public void onAnimationEnd(Animator animation) {
875                         icon.setAlpha(1.0f);
876                         label.setAlpha(1.0f);
877                     }
878                 });
879             } else {
880                 // Otherwise, fade in the entire view.
881                 view.setAlpha(0.0f);
882                 fadeAnim.addUpdateListener(alphaVal -> {
883                     float val = alphaVal.getAnimatedFraction();
884                     view.setAlpha(val);
885                 });
886                 fadeAnim.addListener(new AnimatorListenerAdapter() {
887                     @Override
888                     public void onAnimationEnd(Animator animation) {
889                         view.setAlpha(1.0f);
890                     }
891                 });
892             }
893             anim.play(fadeAnim);
894 
895             int itemType = mTaskRecyclerView.getChildViewHolder(view).getItemViewType();
896             if (itemType == ITEM_TYPE_CLEAR_ALL) {
897                 // Don't add delay. Clear all should animate at same time as next view.
898                 continue;
899             }
900             delay += REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY;
901         }
902     }
903 
904     /**
905      * Set view properties so that the view fits to the target rect.
906      *
907      * @param view view to set
908      * @param origRect original rect that view was located
909      * @param targetRect rect to set to
910      */
setViewToRect(View view, RectF origRect, RectF targetRect)911     private void setViewToRect(View view, RectF origRect, RectF targetRect) {
912         float dX = targetRect.left - origRect.left;
913         float dY = targetRect.top - origRect.top;
914         view.setTranslationX(dX);
915         view.setTranslationY(dY);
916 
917         float scaleX = targetRect.width() / origRect.width();
918         float scaleY = targetRect.height() / origRect.height();
919         view.setPivotX(0);
920         view.setPivotY(0);
921         view.setScaleX(scaleX);
922         view.setScaleY(scaleY);
923     }
924 
925     @Override
setInsets(Rect insets)926     public void setInsets(Rect insets) {
927         mInsets = insets;
928         mTaskRecyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom);
929         mTaskRecyclerView.invalidateItemDecorations();
930         if (mInsets.top != 0) {
931             updateStatusBarScrim();
932         }
933     }
934 
935     /**
936      * Callback for when this view is ready for a remote animation from app to overview.
937      */
938     public interface onReadyForRemoteAnimCallback {
939 
onReadyForRemoteAnim()940         void onReadyForRemoteAnim();
941     }
942 }
943