summaryrefslogtreecommitdiffstats
path: root/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
blob: 07faa4bbdd96b8f0d74d0edf820596cff13d2ae2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.quickstep.views;

import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;

import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;

import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
import static com.android.quickstep.TaskAdapter.ITEM_TYPE_TASK;
import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;

import com.android.launcher3.BaseActivity;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.util.Themes;
import com.android.quickstep.ContentFillItemAnimator;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsToActivityHelper;
import com.android.quickstep.TaskActionController;
import com.android.quickstep.TaskAdapter;
import com.android.quickstep.TaskHolder;
import com.android.quickstep.TaskListLoader;
import com.android.quickstep.TaskSwipeCallback;
import com.android.systemui.shared.recents.model.Task;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
 * base.
 */
public final class IconRecentsView extends FrameLayout implements Insettable {

    public static final FloatProperty<IconRecentsView> CONTENT_ALPHA =
            new FloatProperty<IconRecentsView>("contentAlpha") {
                @Override
                public void setValue(IconRecentsView view, float v) {
                    ALPHA.set(view, v);
                    if (view.getVisibility() != VISIBLE && v > 0) {
                        view.setVisibility(VISIBLE);
                    } else if (view.getVisibility() != GONE && v == 0){
                        view.setVisibility(GONE);
                    }
                }

                @Override
                public Float get(IconRecentsView view) {
                    return ALPHA.get(view);
                }
            };
    private static final long CROSSFADE_DURATION = 300;
    private static final long LAYOUT_ITEM_ANIMATE_IN_DURATION = 150;
    private static final long LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN = 40;
    private static final long ITEM_ANIMATE_OUT_DURATION = 150;
    private static final long ITEM_ANIMATE_OUT_DELAY_BETWEEN = 40;
    private static final float ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO = .25f;
    private static final long CLEAR_ALL_FADE_DELAY = 120;

    /**
     * A ratio representing the view's relative placement within its padded space. For example, 0
     * is top aligned and 0.5 is centered vertically.
     */
    @ViewDebug.ExportedProperty(category = "launcher")

    private final Context mContext;
    private final TaskListLoader mTaskLoader;
    private final TaskAdapter mTaskAdapter;
    private final LinearLayoutManager mTaskLayoutManager;
    private final TaskActionController mTaskActionController;
    private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
    private final ContentFillItemAnimator mLoadingContentItemAnimator =
            new ContentFillItemAnimator();
    private final BaseActivity mActivity;
    private final Drawable mStatusBarForegroundScrim;

    private RecentsToActivityHelper mActivityHelper;
    private RecyclerView mTaskRecyclerView;
    private View mShowingContentView;
    private View mEmptyView;
    private View mContentView;
    private boolean mTransitionedFromApp;
    private AnimatorSet mLayoutAnimation;
    private final ArraySet<View> mLayingOutViews = new ArraySet<>();
    private Rect mInsets;
    private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
        ArrayList<TaskItemView> itemViews = getTaskViews();
        for (int i = 0, size = itemViews.size(); i < size; i++) {
            TaskItemView taskView = itemViews.get(i);
            TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
            Optional<Task> optTask = taskHolder.getTask();
            if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
                Task task = optTask.get();
                // Update thumbnail on the task.
                task.thumbnail = thumbnailData;
                taskView.setThumbnail(thumbnailData);
                return task;
            }
        }
        return null;
    };

    public IconRecentsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mActivity = BaseActivity.fromContext(context);
        mContext = context;
        mStatusBarForegroundScrim  =
                Themes.getAttrDrawable(mContext, R.attr.workspaceStatusBarScrim);
        mTaskLoader = new TaskListLoader(mContext);
        mTaskAdapter = new TaskAdapter(mTaskLoader);
        mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
        mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
        mTaskAdapter.setActionController(mTaskActionController);
        mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
        RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (mTaskRecyclerView == null) {
            mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
            mTaskRecyclerView.setAdapter(mTaskAdapter);
            mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
            ItemTouchHelper helper = new ItemTouchHelper(
                    new TaskSwipeCallback(holder -> {
                        mTaskActionController.removeTask(holder);
                        if (mTaskLoader.getCurrentTaskList().isEmpty()) {
                            mActivityHelper.leaveRecents();
                        }
                    }));
            helper.attachToRecyclerView(mTaskRecyclerView);
            mTaskRecyclerView.addOnChildAttachStateChangeListener(
                    new OnChildAttachStateChangeListener() {
                        @Override
                        public void onChildViewAttachedToWindow(@NonNull View view) {
                            if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
                                // Child view was added that is not part of current layout animation
                                // so restart the animation.
                                animateFadeInLayoutAnimation();
                            }
                        }

                        @Override
                        public void onChildViewDetachedFromWindow(@NonNull View view) { }
                    });
            mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
            mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
                    () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
            ItemDecoration marginDecorator = new ItemDecoration() {
                @Override
                public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                        @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
                    // TODO: Determine if current margins cause off screen item to be fully off
                    // screen and if so, modify them so that it is partially off screen.
                    int itemType = parent.getChildViewHolder(view).getItemViewType();
                    Resources res = getResources();
                    switch (itemType) {
                        case ITEM_TYPE_CLEAR_ALL:
                            outRect.top = (int) res.getDimension(
                                    R.dimen.clear_all_item_view_top_margin);
                            int desiredBottomMargin = (int) res.getDimension(
                                    R.dimen.clear_all_item_view_bottom_margin);
                            // Only add bottom margin if insets aren't enough.
                            if (mInsets.bottom < desiredBottomMargin) {
                                outRect.bottom = desiredBottomMargin - mInsets.bottom;
                            }
                            break;
                        case ITEM_TYPE_TASK:
                            int desiredTopMargin = (int) res.getDimension(
                                    R.dimen.task_item_top_margin);
                            if (mTaskRecyclerView.getChildAdapterPosition(view) ==
                                    state.getItemCount() - 1) {
                                // Only add top margin to top task view if insets aren't enough.
                                if (mInsets.top < desiredTopMargin) {
                                    outRect.top = desiredTopMargin - mInsets.bottom;
                                }
                                return;
                            }
                            outRect.top = desiredTopMargin;
                            break;
                        default:
                    }
                }
            };
            mTaskRecyclerView.addItemDecoration(marginDecorator);

            mEmptyView = findViewById(R.id.recent_task_empty_view);
            mContentView = mTaskRecyclerView;
            mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
                @Override
                public void onChanged() {
                    updateContentViewVisibility();
                }

                @Override
                public void onItemRangeRemoved(int positionStart, int itemCount) {
                    updateContentViewVisibility();
                }
            });
        }
    }

    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        int childCount = mTaskRecyclerView.getChildCount();
        for (int i = 0; i < childCount; i++) {
            mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
        }
    }

    /**
     * Set activity helper for the view to callback to.
     *
     * @param helper the activity helper
     */
    public void setRecentsToActivityHelper(@NonNull RecentsToActivityHelper helper) {
        mActivityHelper = helper;
    }

    /**
     * Logic for when we know we are going to overview/recents and will be putting up the recents
     * view. This should be used to prepare recents (e.g. load any task data, etc.) before it
     * becomes visible.
     */
    public void onBeginTransitionToOverview() {
        if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
            // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
            // all tasks should be visible to fill up the screen in portrait mode and the view will
            // not be scrollable.
            mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
        }
        scheduleFadeInLayoutAnimation();
        // Load any task changes
        if (!mTaskLoader.needsToLoad()) {
            return;
        }
        mTaskAdapter.setIsShowingLoadingUi(true);
        mTaskAdapter.notifyDataSetChanged();
        mTaskLoader.loadTaskList(tasks -> {
            int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
            mTaskAdapter.setIsShowingLoadingUi(false);
            int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
            if (numEmptyItems < numActualItems) {
                throw new IllegalStateException("There are less empty item views than the number "
                        + "of items to animate to.");
            }
            // Possible that task list loads faster than adapter changes propagate to layout so
            // only start content fill animation if there aren't any pending adapter changes.
            if (!mTaskRecyclerView.hasPendingAdapterUpdates()) {
                // Set item animator for content filling animation. The item animator will switch
                // back to the default on completion
                mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
            }
            mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
                    numEmptyItems - numActualItems);
            mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
                    CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
        });
    }

    /**
     * Set whether we transitioned to recents from the most recent app.
     *
     * @param transitionedFromApp true if transitioned from the most recent app, false otherwise
     */
    public void setTransitionedFromApp(boolean transitionedFromApp) {
        mTransitionedFromApp = transitionedFromApp;
    }

    /**
     * Handles input from the overview button. Launch the most recent task unless we just came from
     * the app. In that case, we launch the next most recent.
     */
    public void handleOverviewCommand() {
        List<Task> tasks = mTaskLoader.getCurrentTaskList();
        int tasksSize = tasks.size();
        if (tasksSize == 0) {
            // Do nothing
            return;
        }
        Task taskToLaunch;
        if (mTransitionedFromApp && tasksSize > 1) {
            // Launch the next most recent app
            taskToLaunch = tasks.get(1);
        } else {
            // Launch the most recent app
            taskToLaunch = tasks.get(0);
        }

        // See if view for this task is attached, and if so, animate launch from that view.
        ArrayList<TaskItemView> itemViews = getTaskViews();
        for (int i = 0, size = itemViews.size(); i < size; i++) {
            TaskItemView taskView = itemViews.get(i);
            TaskHolder holder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
            if (Objects.equals(holder.getTask(), Optional.of(taskToLaunch))) {
                mTaskActionController.launchTaskFromView(holder);
                return;
            }
        }

        // Otherwise, just use a basic launch animation.
        mTaskActionController.launchTask(taskToLaunch);
    }

    /**
     * Set whether or not to show the scrim in between the view and the top insets. This only works
     * if the view is being insetted in the first place.
     *
     * The scrim is added to the activity's root view to prevent animations on this view
     * affecting the scrim. As a result, it is the activity's responsibility to show/hide this
     * scrim as appropriate.
     *
     * @param showStatusBarForegroundScrim true to show the scrim, false to hide
     */
    public void setShowStatusBarForegroundScrim(boolean showStatusBarForegroundScrim) {
        boolean shouldShow = mInsets.top != 0 && showStatusBarForegroundScrim;
        mActivity.getDragLayer().setForeground(shouldShow ? mStatusBarForegroundScrim : null);
    }

    /**
     * Get the bottom most thumbnail view to animate to.
     *
     * @return the thumbnail view if laid out
     */
    public @Nullable View getBottomThumbnailView() {
        ArrayList<TaskItemView> taskViews = getTaskViews();
        if (taskViews.isEmpty()) {
            return null;
        }
        TaskItemView view = taskViews.get(0);
        return view.getThumbnailView();
    }

    /**
     * Whether this view has processed all data changes and is ready to animate from the app to
     * the overview.
     *
     * @return true if ready to animate app to overview, false otherwise
     */
    public boolean isReadyForRemoteAnim() {
        return !mTaskRecyclerView.hasPendingAdapterUpdates();
    }

    /**
     * Set a callback for whenever this view is ready to do a remote animation from the app to
     * overview. See {@link #isReadyForRemoteAnim()}.
     *
     * @param callback callback to run when view is ready to animate
     */
    public void setOnReadyForRemoteAnimCallback(onReadyForRemoteAnimCallback callback) {
        mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
                new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        if (isReadyForRemoteAnim()) {
                            callback.onReadyForRemoteAnim();
                            mTaskRecyclerView.getViewTreeObserver().
                                    removeOnGlobalLayoutListener(this);
                        }
                    }
                });
    }

    /**
     * Clear all tasks and animate out.
     */
    private void animateClearAllTasks() {
        setEnabled(false);
        ArrayList<TaskItemView> itemViews = getTaskViews();

        AnimatorSet clearAnim = new AnimatorSet();
        long currentDelay = 0;

        // Animate each item view to the right and fade out.
        for (int i = 0, size = itemViews.size(); i < size; i++) {
            TaskItemView itemView = itemViews.get(i);
            PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
                    0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
            PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
            ObjectAnimator itemAnim = ObjectAnimator.ofPropertyValuesHolder(itemView,
                    transXproperty, alphaProperty);
            itemAnim.setDuration(ITEM_ANIMATE_OUT_DURATION);
            itemAnim.setStartDelay(currentDelay);

            clearAnim.play(itemAnim);
            currentDelay += ITEM_ANIMATE_OUT_DELAY_BETWEEN;
        }

        // Animate view fading and leave recents when faded enough.
        ValueAnimator contentAlpha = ValueAnimator.ofFloat(1.0f, 0f)
                .setDuration(CROSSFADE_DURATION);
        contentAlpha.setStartDelay(CLEAR_ALL_FADE_DELAY);
        contentAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            private boolean mLeftRecents = false;

            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mContentView.setAlpha((float) valueAnimator.getAnimatedValue());
                // Leave recents while fading out.
                if ((float) valueAnimator.getAnimatedValue() < .5f && !mLeftRecents) {
                    mActivityHelper.leaveRecents();
                    mLeftRecents = true;
                }
            }
        });

        clearAnim.play(contentAlpha);
        clearAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                for (int i = 0, size = itemViews.size(); i < size; i++) {
                    TaskItemView itemView = itemViews.get(i);
                    itemView.setTranslationX(0);
                    itemView.setAlpha(1.0f);
                }
                setEnabled(true);
                mContentView.setVisibility(GONE);
                mTaskActionController.clearAllTasks();
            }
        });
        clearAnim.start();
    }

    /**
     * Get attached task item views ordered by most recent.
     *
     * @return array list of attached task item views
     */
    private ArrayList<TaskItemView> getTaskViews() {
        int taskCount = mTaskRecyclerView.getChildCount();
        ArrayList<TaskItemView> itemViews = new ArrayList<>();
        for (int i = 0; i < taskCount; i ++) {
            View child = mTaskRecyclerView.getChildAt(i);
            if (child instanceof TaskItemView) {
                itemViews.add((TaskItemView) child);
            }
        }
        return itemViews;
    }

    /**
     * Update the content view so that the appropriate view is shown based off the current list
     * of tasks.
     */
    private void updateContentViewVisibility() {
        int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
        if (mShowingContentView != mEmptyView && taskListSize == 0) {
            mShowingContentView = mEmptyView;
            crossfadeViews(mEmptyView, mContentView);
        }
        if (mShowingContentView != mContentView && taskListSize > 0) {
            mShowingContentView = mContentView;
            crossfadeViews(mContentView, mEmptyView);
        }
    }

    /**
     * Animate views so that one view fades in while the other fades out.
     *
     * @param fadeInView view that should fade in
     * @param fadeOutView view that should fade out
     */
    private void crossfadeViews(View fadeInView, View fadeOutView) {
        fadeInView.animate().cancel();
        fadeInView.setVisibility(VISIBLE);
        fadeInView.setAlpha(0f);
        fadeInView.animate()
                .alpha(1f)
                .setDuration(CROSSFADE_DURATION)
                .setListener(null);

        fadeOutView.animate().cancel();
        fadeOutView.animate()
                .alpha(0f)
                .setDuration(CROSSFADE_DURATION)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        fadeOutView.setVisibility(GONE);
                    }
                });
    }

    /**
     * Schedule a one-shot layout animation on the next layout. Separate from
     * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
     * view properties themselves, allowing more controllable behavior and making it easier to
     * manage when the animation conflicts with another animation.
     */
    private void scheduleFadeInLayoutAnimation() {
        mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
                new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        animateFadeInLayoutAnimation();
                        mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    }
                });
    }

    /**
     * Start animating the layout animation where items fade in.
     */
    private void animateFadeInLayoutAnimation() {
        if (mLayoutAnimation != null) {
            // If layout animation still in progress, cancel and restart.
            mLayoutAnimation.cancel();
        }
        ArrayList<TaskItemView> views = getTaskViews();
        int delay = 0;
        mLayoutAnimation = new AnimatorSet();
        for (int i = 0, size = views.size(); i < size; i++) {
            TaskItemView view = views.get(i);
            view.setAlpha(0.0f);
            Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
            alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
            alphaAnim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    view.setAlpha(1.0f);
                    mLayingOutViews.remove(view);
                }
            });
            delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
            mLayoutAnimation.play(alphaAnim);
            mLayingOutViews.add(view);
        }
        mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mLayoutAnimation = null;
            }
        });
        mLayoutAnimation.start();
    }

    @Override
    public void setInsets(Rect insets) {
        mInsets = insets;
        mTaskRecyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom);
        mTaskRecyclerView.invalidateItemDecorations();
    }

    /**
     * Callback for when this view is ready for a remote animation from app to overview.
     */
    public interface onReadyForRemoteAnimCallback {

        void onReadyForRemoteAnim();
    }
}