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