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.launcher3.uioverrides.touchcontrollers;
17 
18 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
19 import static com.android.launcher3.LauncherState.NORMAL;
20 import static com.android.launcher3.LauncherState.OVERVIEW;
21 import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
22 import static com.android.launcher3.LauncherState.QUICK_SWITCH;
23 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
24 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
25 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
26 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
27 import static com.android.launcher3.anim.Interpolators.LINEAR;
28 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
29 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
31 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
32 import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
33 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
34 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
35 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
36 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
37 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
38 import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
39 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
40 import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
41 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
42 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
43 import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
44 import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC;
45 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
46 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
47 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
48 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
49 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
50 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
51 
52 import android.animation.Animator;
53 import android.animation.Animator.AnimatorListener;
54 import android.animation.AnimatorListenerAdapter;
55 import android.animation.ValueAnimator;
56 import android.graphics.PointF;
57 import android.view.MotionEvent;
58 import android.view.animation.Interpolator;
59 
60 import com.android.launcher3.BaseQuickstepLauncher;
61 import com.android.launcher3.LauncherState;
62 import com.android.launcher3.R;
63 import com.android.launcher3.Utilities;
64 import com.android.launcher3.anim.AnimatorPlaybackController;
65 import com.android.launcher3.anim.PendingAnimation;
66 import com.android.launcher3.states.StateAnimationConfig;
67 import com.android.launcher3.touch.BaseSwipeDetector;
68 import com.android.launcher3.touch.BothAxesSwipeDetector;
69 import com.android.launcher3.util.TouchController;
70 import com.android.quickstep.AnimatedFloat;
71 import com.android.quickstep.SystemUiProxy;
72 import com.android.quickstep.util.AnimatorControllerWithResistance;
73 import com.android.quickstep.util.LayoutUtils;
74 import com.android.quickstep.util.MotionPauseDetector;
75 import com.android.quickstep.util.VibratorWrapper;
76 import com.android.quickstep.util.WorkspaceRevealAnim;
77 import com.android.quickstep.views.LauncherRecentsView;
78 import com.android.quickstep.views.RecentsView;
79 
80 /**
81  * Handles quick switching to a recent task from the home screen. To give as much flexibility to
82  * the user as possible, also handles swipe up and hold to go to overview and swiping back home.
83  */
84 public class NoButtonQuickSwitchTouchController implements TouchController,
85         BothAxesSwipeDetector.Listener {
86 
87     private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
88     private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_3;
89     private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
90     private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR;
91     private static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
92 
93     private final BaseQuickstepLauncher mLauncher;
94     private final BothAxesSwipeDetector mSwipeDetector;
95     private final float mXRange;
96     private final float mYRange;
97     private final float mMaxYProgress;
98     private final MotionPauseDetector mMotionPauseDetector;
99     private final float mMotionPauseMinDisplacement;
100     private final LauncherRecentsView mRecentsView;
101     protected final AnimatorListener mClearStateOnCancelListener =
102             newCancelListener(this::clearState);
103 
104     private boolean mNoIntercept;
105     private LauncherState mStartState;
106 
107     private boolean mIsHomeScreenVisible = true;
108 
109     // As we drag, we control 3 animations: one to get non-overview components out of the way,
110     // and the other two to set overview properties based on x and y progress.
111     private AnimatorPlaybackController mNonOverviewAnim;
112     private AnimatorPlaybackController mXOverviewAnim;
113     private AnimatedFloat mYOverviewAnim;
114 
NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher)115     public NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher) {
116         mLauncher = launcher;
117         mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
118         mRecentsView = mLauncher.getOverviewPanel();
119         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
120         mYRange = LayoutUtils.getShelfTrackingDistance(
121             mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
122         mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
123         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
124         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
125                 R.dimen.motion_pause_detector_min_displacement_from_app);
126     }
127 
128     @Override
onControllerInterceptTouchEvent(MotionEvent ev)129     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
130         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
131             mNoIntercept = !canInterceptTouch(ev);
132             if (mNoIntercept) {
133                 return false;
134             }
135 
136             // Only detect horizontal swipe for intercept, then we will allow swipe up as well.
137             mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
138                     false /* ignoreSlopWhenSettling */);
139         }
140 
141         if (mNoIntercept) {
142             return false;
143         }
144 
145         onControllerTouchEvent(ev);
146         return mSwipeDetector.isDraggingOrSettling();
147     }
148 
149     @Override
onControllerTouchEvent(MotionEvent ev)150     public boolean onControllerTouchEvent(MotionEvent ev) {
151         return mSwipeDetector.onTouchEvent(ev);
152     }
153 
canInterceptTouch(MotionEvent ev)154     private boolean canInterceptTouch(MotionEvent ev) {
155         if (!mLauncher.isInState(LauncherState.NORMAL)) {
156             return false;
157         }
158         if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
159             return false;
160         }
161         int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
162         if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
163             return false;
164         }
165         return true;
166     }
167 
168     @Override
onDragStart(boolean start)169     public void onDragStart(boolean start) {
170         mMotionPauseDetector.clear();
171         if (start) {
172             mStartState = mLauncher.getStateManager().getState();
173 
174             mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
175 
176             // We have detected horizontal drag start, now allow swipe up as well.
177             mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP,
178                     false /* ignoreSlopWhenSettling */);
179 
180             setupAnimators();
181         }
182     }
183 
onMotionPauseDetected()184     private void onMotionPauseDetected() {
185         VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
186     }
187 
setupAnimators()188     private void setupAnimators() {
189         // Animate the non-overview components (e.g. workspace, shelf) out of the way.
190         StateAnimationConfig nonOverviewBuilder = new StateAnimationConfig();
191         nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR);
192         nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR);
193         nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_SCALE, FADE_OUT_INTERPOLATOR);
194         nonOverviewBuilder.setInterpolator(ANIM_DEPTH, FADE_OUT_INTERPOLATOR);
195         nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR);
196         updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder);
197         mNonOverviewAnim.dispatchOnStart();
198 
199         if (mRecentsView.getTaskViewCount() == 0) {
200             mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
201                 if (!isEmpty && mSwipeDetector.isDraggingState()) {
202                     // We have loaded tasks, update the animators to start at the correct scale etc.
203                     setupOverviewAnimators();
204                 }
205             });
206         }
207 
208         setupOverviewAnimators();
209     }
210 
211     /** Create state animation to control non-overview components. */
updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config)212     private void updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config) {
213         config.duration = (long) (Math.max(mXRange, mYRange) * 2);
214         config.animFlags |= SKIP_OVERVIEW | SKIP_SCRIM;
215         mNonOverviewAnim = mLauncher.getStateManager()
216                 .createAnimationToNewWorkspace(toState, config);
217         mNonOverviewAnim.getTarget().addListener(mClearStateOnCancelListener);
218     }
219 
setupOverviewAnimators()220     private void setupOverviewAnimators() {
221         final LauncherState fromState = QUICK_SWITCH;
222         final LauncherState toState = OVERVIEW;
223 
224         // Set RecentView's initial properties.
225         RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
226         ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, 1f);
227         mRecentsView.setContentAlpha(1);
228         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
229         mLauncher.getActionsView().getVisibilityAlpha().setValue(
230                 (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
231 
232         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
233         // As we drag right, animate the following properties:
234         //   - RecentsView translationX
235         //   - OverviewScrim
236         //   - RecentsView fade (if it's empty)
237         PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
238         xAnim.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1], LINEAR);
239         // Use QuickSwitchState instead of OverviewState to determine scrim color,
240         // since we need to take potential taskbar into account.
241         xAnim.setViewBackgroundColor(mLauncher.getScrimView(),
242                 QUICK_SWITCH.getWorkspaceScrimColor(mLauncher), LINEAR);
243         if (mRecentsView.getTaskViewCount() == 0) {
244             xAnim.addFloat(mRecentsView, CONTENT_ALPHA, 0f, 1f, LINEAR);
245         }
246         mXOverviewAnim = xAnim.createPlaybackController();
247         mXOverviewAnim.dispatchOnStart();
248 
249         // As we drag up, animate the following properties:
250         //   - RecentsView scale
251         //   - RecentsView fullscreenProgress
252         PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
253         yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
254                 SCALE_DOWN_INTERPOLATOR);
255         yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
256                 toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
257         AnimatorPlaybackController yNormalController = yAnim.createPlaybackController();
258         AnimatorControllerWithResistance yAnimWithResistance = AnimatorControllerWithResistance
259                 .createForRecents(yNormalController, mLauncher,
260                         mRecentsView.getPagedViewOrientedState(), mLauncher.getDeviceProfile(),
261                         mRecentsView, RECENTS_SCALE_PROPERTY, mRecentsView,
262                         TASK_SECONDARY_TRANSLATION);
263         mYOverviewAnim = new AnimatedFloat(() -> {
264             if (mYOverviewAnim != null) {
265                 yAnimWithResistance.setProgress(mYOverviewAnim.value, mMaxYProgress);
266             }
267         });
268         yNormalController.dispatchOnStart();
269     }
270 
271     @Override
onDrag(PointF displacement, MotionEvent ev)272     public boolean onDrag(PointF displacement, MotionEvent ev) {
273         float xProgress = Math.max(0, displacement.x) / mXRange;
274         float yProgress = Math.max(0, -displacement.y) / mYRange;
275         yProgress = Utilities.mapRange(yProgress, Y_ANIM_MIN_PROGRESS, 1f);
276 
277         boolean wasHomeScreenVisible = mIsHomeScreenVisible;
278         if (wasHomeScreenVisible && mNonOverviewAnim != null) {
279             mNonOverviewAnim.setPlayFraction(xProgress);
280         }
281         mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress)
282                 <= 1 - ALPHA_CUTOFF_THRESHOLD;
283 
284         mMotionPauseDetector.setDisallowPause(-displacement.y < mMotionPauseMinDisplacement);
285         mMotionPauseDetector.addPosition(ev);
286 
287         if (mXOverviewAnim != null) {
288             mXOverviewAnim.setPlayFraction(xProgress);
289         }
290         if (mYOverviewAnim != null) {
291             mYOverviewAnim.updateValue(yProgress);
292         }
293         return true;
294     }
295 
296     @Override
onDragEnd(PointF velocity)297     public void onDragEnd(PointF velocity) {
298         boolean horizontalFling = mSwipeDetector.isFling(velocity.x);
299         boolean verticalFling = mSwipeDetector.isFling(velocity.y);
300         boolean noFling = !horizontalFling && !verticalFling;
301         if (mMotionPauseDetector.isPaused() && noFling) {
302             cancelAnimations();
303 
304             StateAnimationConfig config = new StateAnimationConfig();
305             config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
306             Animator overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
307                     mStartState, OVERVIEW, config);
308             overviewAnim.addListener(new AnimatorListenerAdapter() {
309                 @Override
310                 public void onAnimationEnd(Animator animation) {
311                     onAnimationToStateCompleted(OVERVIEW);
312                 }
313             });
314             overviewAnim.start();
315 
316             // Create an empty state transition so StateListeners get onStateTransitionStart().
317             mLauncher.getStateManager().createAnimationToNewWorkspace(
318                     OVERVIEW, config.duration, StateAnimationConfig.SKIP_ALL_ANIMATIONS)
319                     .dispatchOnStart();
320             return;
321         }
322 
323         final LauncherState targetState;
324         if (horizontalFling && verticalFling) {
325             if (velocity.x < 0) {
326                 // Flinging left and up or down both go back home.
327                 targetState = NORMAL;
328             } else {
329                 if (velocity.y > 0) {
330                     // Flinging right and down goes to quick switch.
331                     targetState = QUICK_SWITCH;
332                 } else {
333                     // Flinging up and right could go either home or to quick switch.
334                     // Determine the target based on the higher velocity.
335                     targetState = Math.abs(velocity.x) > Math.abs(velocity.y)
336                         ? QUICK_SWITCH : NORMAL;
337                 }
338             }
339         } else if (horizontalFling) {
340             targetState = velocity.x > 0 ? QUICK_SWITCH : NORMAL;
341         } else if (verticalFling) {
342             targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL;
343         } else {
344             // If user isn't flinging, just snap to the closest state.
345             boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
346             boolean passedVerticalThreshold = mYOverviewAnim.value > 1f;
347             targetState = passedHorizontalThreshold && !passedVerticalThreshold
348                     ? QUICK_SWITCH : NORMAL;
349         }
350 
351         // Animate the various components to the target state.
352 
353         float xProgress = mXOverviewAnim.getProgressFraction();
354         float startXProgress = Utilities.boundToRange(xProgress
355                 + velocity.x * getSingleFrameMs(mLauncher) / mXRange, 0f, 1f);
356         final float endXProgress = targetState == NORMAL ? 0 : 1;
357         long xDuration = BaseSwipeDetector.calculateDuration(velocity.x,
358                 Math.abs(endXProgress - startXProgress));
359         ValueAnimator xOverviewAnim = mXOverviewAnim.getAnimationPlayer();
360         xOverviewAnim.setFloatValues(startXProgress, endXProgress);
361         xOverviewAnim.setDuration(xDuration)
362                 .setInterpolator(scrollInterpolatorForVelocity(velocity.x));
363         mXOverviewAnim.dispatchOnStart();
364 
365         boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
366 
367         float yProgress = mYOverviewAnim.value;
368         float startYProgress = Utilities.boundToRange(yProgress
369                 - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, mMaxYProgress);
370         final float endYProgress;
371         if (flingUpToNormal) {
372             endYProgress = 1;
373         } else if (targetState == NORMAL) {
374             // Keep overview at its current scale/translationY as it slides off the screen.
375             endYProgress = startYProgress;
376         } else {
377             endYProgress = 0;
378         }
379         float yDistanceToCover = Math.abs(endYProgress - startYProgress) * mYRange;
380         long yDuration = (long) (yDistanceToCover / Math.max(1f, Math.abs(velocity.y)));
381         ValueAnimator yOverviewAnim = mYOverviewAnim.animateToValue(startYProgress, endYProgress);
382         yOverviewAnim.setDuration(yDuration);
383         mYOverviewAnim.updateValue(startYProgress);
384 
385         ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
386         if (flingUpToNormal && !mIsHomeScreenVisible) {
387             // We are flinging to home while workspace is invisible, run the same staggered
388             // animation as from an app.
389             StateAnimationConfig config = new StateAnimationConfig();
390             // Update mNonOverviewAnim to do nothing so it doesn't interfere.
391             config.animFlags = SKIP_ALL_ANIMATIONS;
392             updateNonOverviewAnim(targetState, config);
393             nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
394             mNonOverviewAnim.dispatchOnStart();
395 
396             new WorkspaceRevealAnim(mLauncher, false /* animateOverviewScrim */).start();
397         } else {
398             boolean canceled = targetState == NORMAL;
399             if (canceled) {
400                 // Let the state manager know that the animation didn't go to the target state,
401                 // but don't clean up yet (we already clean up when the animation completes).
402                 mNonOverviewAnim.getTarget().removeListener(mClearStateOnCancelListener);
403                 mNonOverviewAnim.dispatchOnCancel();
404             }
405             float startProgress = mNonOverviewAnim.getProgressFraction();
406             float endProgress = canceled ? 0 : 1;
407             nonOverviewAnim.setFloatValues(startProgress, endProgress);
408             mNonOverviewAnim.dispatchOnStart();
409         }
410         if (targetState == QUICK_SWITCH) {
411             // Navigating to quick switch, add scroll feedback since the first time is not
412             // considered a scroll by the RecentsView.
413             VibratorWrapper.INSTANCE.get(mLauncher).vibrate(
414                     RecentsView.SCROLL_VIBRATION_PRIMITIVE,
415                     RecentsView.SCROLL_VIBRATION_PRIMITIVE_SCALE,
416                     RecentsView.SCROLL_VIBRATION_FALLBACK);
417         }
418 
419         nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
420         mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState));
421 
422         cancelAnimations();
423         xOverviewAnim.start();
424         yOverviewAnim.start();
425         nonOverviewAnim.start();
426     }
427 
onAnimationToStateCompleted(LauncherState targetState)428     private void onAnimationToStateCompleted(LauncherState targetState) {
429         mLauncher.getStatsLogManager().logger()
430                 .withSrcState(LAUNCHER_STATE_HOME)
431                 .withDstState(targetState.statsLogOrdinal)
432                 .log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal,
433                         targetState.ordinal > mStartState.ordinal
434                                 ? LAUNCHER_UNKNOWN_SWIPEUP
435                                 : LAUNCHER_UNKNOWN_SWIPEDOWN));
436         mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
437     }
438 
cancelAnimations()439     private void cancelAnimations() {
440         if (mNonOverviewAnim != null) {
441             mNonOverviewAnim.getAnimationPlayer().cancel();
442         }
443         if (mXOverviewAnim != null) {
444             mXOverviewAnim.getAnimationPlayer().cancel();
445         }
446         if (mYOverviewAnim != null) {
447             mYOverviewAnim.cancelAnimation();
448         }
449         mMotionPauseDetector.clear();
450     }
451 
clearState()452     private void clearState() {
453         cancelAnimations();
454         mNonOverviewAnim = null;
455         mXOverviewAnim = null;
456         mYOverviewAnim = null;
457         mIsHomeScreenVisible = true;
458         mSwipeDetector.finishedScrolling();
459         mRecentsView.setOnEmptyMessageUpdatedListener(null);
460     }
461 }
462