1 /*
2  * Copyright (C) 2017 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.views;
17 
18 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
19 
20 import static com.android.app.animation.Interpolators.LINEAR;
21 import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
22 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
23 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
24 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS;
25 import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS;
26 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
27 
28 import android.animation.Animator;
29 import android.animation.ObjectAnimator;
30 import android.animation.ValueAnimator;
31 import android.content.Context;
32 import android.graphics.Canvas;
33 import android.graphics.Outline;
34 import android.graphics.drawable.Drawable;
35 import android.os.Build;
36 import android.util.AttributeSet;
37 import android.util.FloatProperty;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.ViewOutlineProvider;
42 import android.view.animation.Interpolator;
43 import android.window.BackEvent;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.Nullable;
47 import androidx.annotation.Px;
48 import androidx.annotation.RequiresApi;
49 
50 import com.android.app.animation.Interpolators;
51 import com.android.launcher3.AbstractFloatingView;
52 import com.android.launcher3.Utilities;
53 import com.android.launcher3.anim.AnimatedFloat;
54 import com.android.launcher3.anim.AnimatorListeners;
55 import com.android.launcher3.anim.AnimatorPlaybackController;
56 import com.android.launcher3.anim.PendingAnimation;
57 import com.android.launcher3.touch.BaseSwipeDetector;
58 import com.android.launcher3.touch.SingleAxisSwipeDetector;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.Optional;
63 
64 /**
65  * Extension of {@link AbstractFloatingView} with common methods for sliding in from bottom.
66  *
67  * @param <T> Type of ActivityContext inflating this view.
68  */
69 public abstract class AbstractSlideInView<T extends Context & ActivityContext>
70         extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {
71 
72     protected static final FloatProperty<AbstractSlideInView<?>> TRANSLATION_SHIFT =
73             new FloatProperty<>("translationShift") {
74 
75                 @Override
76                 public Float get(AbstractSlideInView view) {
77                     return view.mTranslationShift;
78                 }
79 
80                 @Override
81                 public void setValue(AbstractSlideInView view, float value) {
82                     view.setTranslationShift(value);
83                 }
84             };
85     protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
86     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
87     private static final int DEFAULT_DURATION = 300;
88 
89     protected final T mActivityContext;
90 
91     protected final SingleAxisSwipeDetector mSwipeDetector;
92     protected @NonNull AnimatorPlaybackController mOpenCloseAnimation;
93 
94     protected ViewGroup mContent;
95     protected final @Nullable View mColorScrim;
96 
97     /**
98      * Interpolator for {@link #mOpenCloseAnimation} when we are closing due to dragging downwards.
99      */
100     private Interpolator mScrollInterpolator;
101     private long mScrollDuration;
102     /**
103      * End progress for {@link #mOpenCloseAnimation} when we are closing due to dragging downloads.
104      * <p>
105      * There are two cases that determine this value:
106      * <ol>
107      *     <li>
108      *         If the drag interrupts the opening transition (i.e. {@link #mToTranslationShift}
109      *         is {@link #TRANSLATION_SHIFT_OPENED}), we need to animate back to {@code 0} to
110      *         reverse the animation that was paused at {@link #onDragStart(boolean, float)}.
111      *     </li>
112      *     <li>
113      *         If the drag started after the view is fully opened (i.e.
114      *         {@link #mToTranslationShift} is {@link #TRANSLATION_SHIFT_CLOSED}), the animation
115      *         that was set up at {@link #onDragStart(boolean, float)} for closing the view
116      *         should go forward to {@code 1}.
117      *     </li>
118      * </ol>
119      */
120     private float mScrollEndProgress;
121 
122     // range [0, 1], 0=> completely open, 1=> completely closed
123     protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
124     protected float mFromTranslationShift;
125     protected float mToTranslationShift;
126     /** {@link #mOpenCloseAnimation} progress at {@link #onDragStart(boolean, float)}. */
127     private float mDragStartProgress;
128 
129     protected boolean mNoIntercept;
130     protected @Nullable OnCloseListener mOnCloseBeginListener;
131     protected List<OnCloseListener> mOnCloseListeners = new ArrayList<>();
132 
133     /**
134      * How far through a "user initiated dismissal" the UI is. e.g. Predictive back, swipe to home,
135      * 0 is regular state, 1 is fully dismissed.
136      */
137     protected final AnimatedFloat mSwipeToDismissProgress =
138             new AnimatedFloat(this::onUserSwipeToDismissProgressChanged, 0f);
139     protected boolean mIsDismissInProgress;
140     private View mViewToAnimateInSwipeToDismiss = this;
141     private @Nullable Drawable mContentBackground;
142     private @Nullable View mContentBackgroundParentView;
143 
144     protected final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
145         @Override
146         public void getOutline(View view, Outline outline) {
147             outline.setRect(
148                     0,
149                     0,
150                     view.getMeasuredWidth(),
151                     view.getMeasuredHeight() + getBottomOffsetPx()
152             );
153         }
154     };
155 
AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr)156     public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) {
157         super(context, attrs, defStyleAttr);
158         mActivityContext = ActivityContext.lookupContext(context);
159 
160         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
161         mScrollDuration = DEFAULT_DURATION;
162         mSwipeDetector = new SingleAxisSwipeDetector(context, this,
163                 SingleAxisSwipeDetector.VERTICAL);
164 
165         mOpenCloseAnimation = new PendingAnimation(0).createPlaybackController();
166 
167         int scrimColor = getScrimColor(context);
168         mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
169     }
170 
171     /**
172      * Sets up a {@link #mOpenCloseAnimation} for opening with default parameters.
173      *
174      * @see #setUpOpenCloseAnimation(float, float, long)
175      */
setUpDefaultOpenAnimation()176     protected final AnimatorPlaybackController setUpDefaultOpenAnimation() {
177         AnimatorPlaybackController animation = setUpOpenCloseAnimation(
178                 TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, DEFAULT_DURATION);
179         animation.getAnimationPlayer().setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
180         return animation;
181     }
182 
183     /**
184      * Sets up a {@link #mOpenCloseAnimation} for opening with a given duration.
185      *
186      * @see #setUpOpenCloseAnimation(float, float, long)
187      */
setUpOpenAnimation(long duration)188     protected final AnimatorPlaybackController setUpOpenAnimation(long duration) {
189         return setUpOpenCloseAnimation(
190                 TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, duration);
191     }
192 
setUpCloseAnimation(long duration)193     private AnimatorPlaybackController setUpCloseAnimation(long duration) {
194         return setUpOpenCloseAnimation(
195                 TRANSLATION_SHIFT_OPENED, TRANSLATION_SHIFT_CLOSED, duration);
196     }
197 
198     /**
199      * Initializes a new {@link #mOpenCloseAnimation}.
200      *
201      * @param fromTranslationShift translation shift to animate from.
202      * @param toTranslationShift   translation shift to animate to.
203      * @param duration             animation duration.
204      * @return {@link #mOpenCloseAnimation}
205      */
setUpOpenCloseAnimation( float fromTranslationShift, float toTranslationShift, long duration)206     private AnimatorPlaybackController setUpOpenCloseAnimation(
207             float fromTranslationShift, float toTranslationShift, long duration) {
208         mFromTranslationShift = fromTranslationShift;
209         mToTranslationShift = toTranslationShift;
210 
211         PendingAnimation animation = new PendingAnimation(duration);
212         animation.addEndListener(b -> {
213             mSwipeDetector.finishedScrolling();
214             announceAccessibilityChanges();
215         });
216 
217         animation.addFloat(
218                 this, TRANSLATION_SHIFT, fromTranslationShift, toTranslationShift, LINEAR);
219         if (mColorScrim != null) {
220             animation.setViewAlpha(mColorScrim, 1 - toTranslationShift, getScrimInterpolator());
221         }
222         onOpenCloseAnimationPending(animation);
223 
224         mOpenCloseAnimation = animation.createPlaybackController();
225         return mOpenCloseAnimation;
226     }
227 
228     /**
229      * Invoked when a {@link #mOpenCloseAnimation} is being set up.
230      * <p>
231      * Subclasses can override this method to modify the animation before it's used to create a
232      * {@link AnimatorPlaybackController}.
233      */
onOpenCloseAnimationPending(PendingAnimation animation)234     protected void onOpenCloseAnimationPending(PendingAnimation animation) {}
235 
attachToContainer()236     protected void attachToContainer() {
237         if (mColorScrim != null) {
238             getPopupContainer().addView(mColorScrim);
239         }
240         getPopupContainer().addView(this);
241     }
242 
243     /**
244      * Returns a scrim color for a sliding view. if returned value is -1, no scrim is added.
245      */
getScrimColor(Context context)246     protected int getScrimColor(Context context) {
247         return -1;
248     }
249 
250     /**
251      * Returns the range in height that the slide in view can be dragged.
252      */
getShiftRange()253     protected float getShiftRange() {
254         return mContent.getHeight();
255     }
256 
setTranslationShift(float translationShift)257     protected void setTranslationShift(float translationShift) {
258         mTranslationShift = translationShift;
259         mContent.setTranslationY(mTranslationShift * getShiftRange());
260         invalidate();
261     }
262 
263     @Override
onControllerInterceptTouchEvent(MotionEvent ev)264     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
265         if (mNoIntercept) {
266             return false;
267         }
268 
269         int directionsToDetectScroll = mSwipeDetector.isIdleState()
270                 ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
271         mSwipeDetector.setDetectableScrollConditions(
272                 directionsToDetectScroll, false);
273         mSwipeDetector.onTouchEvent(ev);
274         return mSwipeDetector.isDraggingOrSettling() || !isEventOverContent(ev);
275     }
276 
277     @Override
onControllerTouchEvent(MotionEvent ev)278     public boolean onControllerTouchEvent(MotionEvent ev) {
279         mSwipeDetector.onTouchEvent(ev);
280         if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState()
281                 && !isOpeningAnimationRunning()) {
282             // If we got ACTION_UP without ever starting swipe, close the panel.
283             if (!isEventOverContent(ev)) {
284                 close(true);
285             }
286         }
287         return true;
288     }
289 
290     @Override
291     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
onBackStarted(BackEvent backEvent)292     public void onBackStarted(BackEvent backEvent) {
293         super.onBackStarted(backEvent);
294         mViewToAnimateInSwipeToDismiss = shouldAnimateContentViewInBackSwipe() ? mContent : this;
295     }
296 
297     @Override
298     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
onBackProgressed(BackEvent backEvent)299     public void onBackProgressed(BackEvent backEvent) {
300         final float progress = backEvent.getProgress();
301         float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(progress);
302         mSwipeToDismissProgress.updateValue(deceleratedProgress);
303     }
304 
305     /**
306      * During predictive back swipe, the default behavior is to scale {@link AbstractSlideInView}
307      * during back swipe. This method allow subclass to scale {@link #mContent}, typically to exit
308      * search mode.
309      *
310      * <p>Note that this method can be expensive, and should only be called from
311      * {@link #onBackStarted(BackEvent)}, not from {@link #onBackProgressed(BackEvent)}.
312      */
shouldAnimateContentViewInBackSwipe()313     protected boolean shouldAnimateContentViewInBackSwipe() {
314         return false;
315     }
316 
onUserSwipeToDismissProgressChanged()317     protected void onUserSwipeToDismissProgressChanged() {
318         float progress = mSwipeToDismissProgress.value;
319         mIsDismissInProgress = progress > 0f;
320 
321         float scale = PREDICTIVE_BACK_MIN_SCALE + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1f - progress);
322         SCALE_PROPERTY.set(mViewToAnimateInSwipeToDismiss, scale);
323         setClipChildren(!mIsDismissInProgress);
324         setClipToPadding(!mIsDismissInProgress);
325         mContent.setClipChildren(!mIsDismissInProgress);
326         mContent.setClipToPadding(!mIsDismissInProgress);
327         invalidate();
328     }
329 
330     @Override
onBackCancelled()331     public void onBackCancelled() {
332         super.onBackCancelled();
333         animateSwipeToDismissProgressToStart();
334     }
335 
animateSwipeToDismissProgressToStart()336     protected void animateSwipeToDismissProgressToStart() {
337         ObjectAnimator objectAnimator = mSwipeToDismissProgress.animateToValue(0f)
338                 .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS);
339 
340         // If we are animating a different view, we should reset the animating view back to
341         // AbstractSlideInView as it is the default view to animate.
342         if (this != mViewToAnimateInSwipeToDismiss) {
343             objectAnimator.addListener(new Animator.AnimatorListener() {
344                 @Override
345                 public void onAnimationCancel(Animator animator) {
346                     mViewToAnimateInSwipeToDismiss = AbstractSlideInView.this;
347                 }
348 
349                 @Override
350                 public void onAnimationEnd(Animator animator) {
351                     mViewToAnimateInSwipeToDismiss = AbstractSlideInView.this;
352                 }
353 
354                 @Override
355                 public void onAnimationRepeat(Animator animator) {}
356 
357                 @Override
358                 public void onAnimationStart(Animator animator) {}
359             });
360         }
361 
362         objectAnimator.start();
363     }
364 
365     @Override
dispatchDraw(Canvas canvas)366     protected void dispatchDraw(Canvas canvas) {
367         drawScaledBackground(canvas);
368         super.dispatchDraw(canvas);
369     }
370 
371     /**
372      * Set slide in view's background {@link Drawable} which will be draw onto a parent view in
373      * {@link #dispatchDraw(Canvas)}
374      */
setContentBackgroundWithParent( @onNull Drawable drawable, @NonNull View parentView)375     protected void setContentBackgroundWithParent(
376             @NonNull Drawable drawable, @NonNull View parentView) {
377         mContentBackground = drawable;
378         mContentBackgroundParentView = parentView;
379     }
380 
381     /** Draw scaled background during predictive back animation. */
drawScaledBackground(Canvas canvas)382     private void drawScaledBackground(Canvas canvas) {
383         if (mContentBackground == null || mContentBackgroundParentView == null) {
384             return;
385         }
386         mContentBackground.setBounds(
387                 mContentBackgroundParentView.getLeft(),
388                 mContentBackgroundParentView.getTop() + (int) mContent.getTranslationY(),
389                 mContentBackgroundParentView.getRight(),
390                 mContentBackgroundParentView.getBottom()
391                         + (mIsDismissInProgress ? getBottomOffsetPx() : 0));
392         mContentBackground.draw(canvas);
393     }
394 
395     /** Return extra space revealed during predictive back animation. */
396     @Px
getBottomOffsetPx()397     protected int getBottomOffsetPx() {
398         return (int) (getMeasuredHeight() * (1 - PREDICTIVE_BACK_MIN_SCALE));
399     }
400 
401     /**
402      * Returns {@code true} if the touch event is over the visible area of the bottom sheet.
403      *
404      * By default will check if the touch event is over {@code mContent}, subclasses should override
405      * this method if the visible area of the bottom sheet is different from {@code mContent}.
406      */
isEventOverContent(MotionEvent ev)407     protected boolean isEventOverContent(MotionEvent ev) {
408         return getPopupContainer().isEventOverView(mContent, ev);
409     }
410 
isOpeningAnimationRunning()411     private boolean isOpeningAnimationRunning() {
412         return mIsOpen && mOpenCloseAnimation.getAnimationPlayer().isRunning();
413     }
414 
415     /* SingleAxisSwipeDetector.Listener */
416 
417     @Override
onDragStart(boolean start, float startDisplacement)418     public void onDragStart(boolean start, float startDisplacement) {
419         if (mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
420             mOpenCloseAnimation.pause();
421             mDragStartProgress = mOpenCloseAnimation.getProgressFraction();
422         } else {
423             setUpCloseAnimation(DEFAULT_DURATION);
424             mDragStartProgress = 0;
425         }
426     }
427 
428     @Override
onDrag(float displacement)429     public boolean onDrag(float displacement) {
430         float progress = mDragStartProgress
431                 + Math.signum(mToTranslationShift - mFromTranslationShift)
432                 * (displacement / getShiftRange());
433         mOpenCloseAnimation.setPlayFraction(Utilities.boundToRange(progress, 0, 1));
434         return true;
435     }
436 
437     @Override
onDragEnd(float velocity)438     public void onDragEnd(float velocity) {
439         float successfulShiftThreshold = mActivityContext.getDeviceProfile().isTablet
440                 ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS : SUCCESS_TRANSITION_PROGRESS;
441         if ((mSwipeDetector.isFling(velocity) && velocity > 0)
442                 || mTranslationShift > successfulShiftThreshold) {
443             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
444             mScrollDuration = BaseSwipeDetector.calculateDuration(
445                     velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift);
446             mScrollEndProgress = mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 0 : 1;
447             close(true);
448         } else {
449             ValueAnimator animator = mOpenCloseAnimation.getAnimationPlayer();
450             animator.setInterpolator(Interpolators.DECELERATE);
451             animator.setFloatValues(
452                     mOpenCloseAnimation.getProgressFraction(),
453                     mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 1 : 0);
454             animator.setDuration(BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
455                     .start();
456         }
457     }
458 
459     /** Callback invoked when the view is beginning to close (e.g. close animation is started). */
setOnCloseBeginListener(@ullable OnCloseListener onCloseBeginListener)460     public void setOnCloseBeginListener(@Nullable OnCloseListener onCloseBeginListener) {
461         mOnCloseBeginListener = onCloseBeginListener;
462     }
463 
464     /** Registers an {@link OnCloseListener}. */
addOnCloseListener(OnCloseListener listener)465     public void addOnCloseListener(OnCloseListener listener) {
466         mOnCloseListeners.add(listener);
467     }
468 
handleClose(boolean animate, long defaultDuration)469     protected void handleClose(boolean animate, long defaultDuration) {
470         if (!mIsOpen) {
471             return;
472         }
473         Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed);
474 
475         if (!animate) {
476             mOpenCloseAnimation.pause();
477             setTranslationShift(TRANSLATION_SHIFT_CLOSED);
478             onCloseComplete();
479             return;
480         }
481 
482         final ValueAnimator animator;
483         if (mSwipeDetector.isIdleState()) {
484             setUpCloseAnimation(defaultDuration);
485             animator = mOpenCloseAnimation.getAnimationPlayer();
486             animator.setInterpolator(getIdleInterpolator());
487         } else {
488             animator = mOpenCloseAnimation.getAnimationPlayer();
489             animator.setInterpolator(mScrollInterpolator);
490             animator.setDuration(mScrollDuration);
491             mOpenCloseAnimation.getAnimationPlayer().setFloatValues(
492                     mOpenCloseAnimation.getProgressFraction(), mScrollEndProgress);
493         }
494 
495         animator.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
496         animator.start();
497     }
498 
getIdleInterpolator()499     protected Interpolator getIdleInterpolator() {
500         return Interpolators.ACCELERATE;
501     }
502 
getScrimInterpolator()503     protected Interpolator getScrimInterpolator() {
504         return LINEAR;
505     }
506 
onCloseComplete()507     protected void onCloseComplete() {
508         mIsOpen = false;
509         getPopupContainer().removeView(this);
510         if (mColorScrim != null) {
511             getPopupContainer().removeView(mColorScrim);
512         }
513         mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed);
514     }
515 
getPopupContainer()516     protected BaseDragLayer getPopupContainer() {
517         return mActivityContext.getDragLayer();
518     }
519 
createColorScrim(Context context, int bgColor)520     protected View createColorScrim(Context context, int bgColor) {
521         View view = new View(context);
522         view.forceHasOverlappingRendering(false);
523         view.setBackgroundColor(bgColor);
524 
525         BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT);
526         lp.ignoreInsets = true;
527         view.setLayoutParams(lp);
528 
529         return view;
530     }
531 
532     /**
533      * Interface to report that the {@link AbstractSlideInView} has closed.
534      */
535     public interface OnCloseListener {
536 
537         /**
538          * Called when {@link AbstractSlideInView} closes.
539          */
onSlideInViewClosed()540         void onSlideInViewClosed();
541     }
542 }
543