• 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.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
21 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
22 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS;
23 import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS;
24 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
25 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.ObjectAnimator;
30 import android.animation.PropertyValuesHolder;
31 import android.content.Context;
32 import android.graphics.Canvas;
33 import android.graphics.drawable.Drawable;
34 import android.util.AttributeSet;
35 import android.util.Property;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.animation.Interpolator;
40 
41 import androidx.annotation.FloatRange;
42 import androidx.annotation.Nullable;
43 import androidx.annotation.Px;
44 
45 import com.android.launcher3.AbstractFloatingView;
46 import com.android.launcher3.Utilities;
47 import com.android.launcher3.anim.AnimatedFloat;
48 import com.android.launcher3.anim.Interpolators;
49 import com.android.launcher3.touch.BaseSwipeDetector;
50 import com.android.launcher3.touch.SingleAxisSwipeDetector;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Optional;
55 
56 /**
57  * Extension of {@link AbstractFloatingView} with common methods for sliding in from bottom.
58  *
59  * @param <T> Type of ActivityContext inflating this view.
60  */
61 public abstract class AbstractSlideInView<T extends Context & ActivityContext>
62         extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {
63 
64     protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
65             new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
66 
67                 @Override
68                 public Float get(AbstractSlideInView view) {
69                     return view.mTranslationShift;
70                 }
71 
72                 @Override
73                 public void set(AbstractSlideInView view, Float value) {
74                     view.setTranslationShift(value);
75                 }
76             };
77     protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
78     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
79     private static final float VIEW_NO_SCALE = 1f;
80 
81     protected final T mActivityContext;
82 
83     protected final SingleAxisSwipeDetector mSwipeDetector;
84     protected final ObjectAnimator mOpenCloseAnimator;
85 
86     protected ViewGroup mContent;
87     protected final View mColorScrim;
88     protected Interpolator mScrollInterpolator;
89 
90     // range [0, 1], 0=> completely open, 1=> completely closed
91     protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
92 
93     protected boolean mNoIntercept;
94     protected @Nullable OnCloseListener mOnCloseBeginListener;
95     protected List<OnCloseListener> mOnCloseListeners = new ArrayList<>();
96 
97     private final AnimatedFloat mSlideInViewScale =
98             new AnimatedFloat(this::onScaleProgressChanged, VIEW_NO_SCALE);
99     private boolean mIsBackProgressing;
100     @Nullable private Drawable mContentBackground;
101 
AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr)102     public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) {
103         super(context, attrs, defStyleAttr);
104         mActivityContext = ActivityContext.lookupContext(context);
105 
106         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
107         mSwipeDetector = new SingleAxisSwipeDetector(context, this,
108                 SingleAxisSwipeDetector.VERTICAL);
109 
110         mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
111         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
112             @Override
113             public void onAnimationEnd(Animator animation) {
114                 mSwipeDetector.finishedScrolling();
115                 announceAccessibilityChanges();
116             }
117         });
118         int scrimColor = getScrimColor(context);
119         mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
120     }
121 
setContentBackground(Drawable drawable)122     protected void setContentBackground(Drawable drawable) {
123         mContentBackground = drawable;
124     }
125 
attachToContainer()126     protected void attachToContainer() {
127         if (mColorScrim != null) {
128             getPopupContainer().addView(mColorScrim);
129         }
130         getPopupContainer().addView(this);
131     }
132 
133     /**
134      * Returns a scrim color for a sliding view. if returned value is -1, no scrim is added.
135      */
getScrimColor(Context context)136     protected int getScrimColor(Context context) {
137         return -1;
138     }
139 
140     /**
141      * Returns the range in height that the slide in view can be dragged.
142      */
getShiftRange()143     protected float getShiftRange() {
144         return mContent.getHeight();
145     }
146 
setTranslationShift(float translationShift)147     protected void setTranslationShift(float translationShift) {
148         mTranslationShift = translationShift;
149         mContent.setTranslationY(mTranslationShift * getShiftRange());
150         if (mColorScrim != null) {
151             mColorScrim.setAlpha(1 - mTranslationShift);
152         }
153         invalidate();
154     }
155 
156     @Override
onControllerInterceptTouchEvent(MotionEvent ev)157     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
158         if (mNoIntercept) {
159             return false;
160         }
161 
162         int directionsToDetectScroll = mSwipeDetector.isIdleState()
163                 ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
164         mSwipeDetector.setDetectableScrollConditions(
165                 directionsToDetectScroll, false);
166         mSwipeDetector.onTouchEvent(ev);
167         return mSwipeDetector.isDraggingOrSettling() || !isEventOverContent(ev);
168     }
169 
170     @Override
onControllerTouchEvent(MotionEvent ev)171     public boolean onControllerTouchEvent(MotionEvent ev) {
172         mSwipeDetector.onTouchEvent(ev);
173         if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState()
174                 && !isOpeningAnimationRunning()) {
175             // If we got ACTION_UP without ever starting swipe, close the panel.
176             if (!isEventOverContent(ev)) {
177                 close(true);
178             }
179         }
180         return true;
181     }
182 
183     @Override
onBackProgressed(@loatRangefrom = 0.0, to = 1.0) float progress)184     public void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float progress) {
185         super.onBackProgressed(progress);
186         float deceleratedProgress =
187                 Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(progress);
188         mIsBackProgressing = progress > 0f;
189         mSlideInViewScale.updateValue(PREDICTIVE_BACK_MIN_SCALE
190                 + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1 - deceleratedProgress));
191     }
192 
onScaleProgressChanged()193     private void onScaleProgressChanged() {
194         float scaleProgress = mSlideInViewScale.value;
195         SCALE_PROPERTY.set(this, scaleProgress);
196         setClipChildren(!mIsBackProgressing);
197         mContent.setClipChildren(!mIsBackProgressing);
198         invalidate();
199     }
200 
201     @Override
onBackInvoked()202     public void onBackInvoked() {
203         super.onBackInvoked();
204         animateSlideInViewToNoScale();
205     }
206 
207     @Override
onBackCancelled()208     public void onBackCancelled() {
209         super.onBackCancelled();
210         animateSlideInViewToNoScale();
211     }
212 
animateSlideInViewToNoScale()213     protected void animateSlideInViewToNoScale() {
214         mSlideInViewScale.animateToValue(1f)
215                 .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS)
216                 .start();
217     }
218 
219     @Override
dispatchDraw(Canvas canvas)220     protected void dispatchDraw(Canvas canvas) {
221         drawScaledBackground(canvas);
222         super.dispatchDraw(canvas);
223     }
224 
225     /** Draw scaled background during predictive back animation. */
drawScaledBackground(Canvas canvas)226     protected void drawScaledBackground(Canvas canvas) {
227         if (mContentBackground == null) {
228             return;
229         }
230         mContentBackground.setBounds(
231                 mContent.getLeft(),
232                 mContent.getTop() + (int) mContent.getTranslationY(),
233                 mContent.getRight(),
234                 mContent.getBottom() + (mIsBackProgressing ? getBottomOffsetPx() : 0));
235         mContentBackground.draw(canvas);
236     }
237 
238     /** Return extra space revealed during predictive back animation. */
239     @Px
getBottomOffsetPx()240     protected int getBottomOffsetPx() {
241         final int height = getMeasuredHeight();
242         return (int) ((height / PREDICTIVE_BACK_MIN_SCALE - height) / 2);
243     }
244 
245     /**
246      * Returns {@code true} if the touch event is over the visible area of the bottom sheet.
247      *
248      * By default will check if the touch event is over {@code mContent}, subclasses should override
249      * this method if the visible area of the bottom sheet is different from {@code mContent}.
250      */
isEventOverContent(MotionEvent ev)251     protected boolean isEventOverContent(MotionEvent ev) {
252         return getPopupContainer().isEventOverView(mContent, ev);
253     }
254 
isOpeningAnimationRunning()255     private boolean isOpeningAnimationRunning() {
256         return mIsOpen && mOpenCloseAnimator.isRunning();
257     }
258 
259     /* SingleAxisSwipeDetector.Listener */
260 
261     @Override
onDragStart(boolean start, float startDisplacement)262     public void onDragStart(boolean start, float startDisplacement) { }
263 
264     @Override
onDrag(float displacement)265     public boolean onDrag(float displacement) {
266         float range = getShiftRange();
267         displacement = Utilities.boundToRange(displacement, 0, range);
268         setTranslationShift(displacement / range);
269         return true;
270     }
271 
272     @Override
onDragEnd(float velocity)273     public void onDragEnd(float velocity) {
274         float successfulShiftThreshold = mActivityContext.getDeviceProfile().isTablet
275                 ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS : SUCCESS_TRANSITION_PROGRESS;
276         if ((mSwipeDetector.isFling(velocity) && velocity > 0)
277                 || mTranslationShift > successfulShiftThreshold) {
278             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
279             mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
280                     velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
281             close(true);
282         } else {
283             mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
284                     TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
285             mOpenCloseAnimator.setDuration(
286                     BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
287                     .setInterpolator(Interpolators.DEACCEL);
288             mOpenCloseAnimator.start();
289         }
290     }
291 
292     /** Callback invoked when the view is beginning to close (e.g. close animation is started). */
setOnCloseBeginListener(@ullable OnCloseListener onCloseBeginListener)293     public void setOnCloseBeginListener(@Nullable OnCloseListener onCloseBeginListener) {
294         mOnCloseBeginListener = onCloseBeginListener;
295     }
296 
297     /** Registers an {@link OnCloseListener}. */
addOnCloseListener(OnCloseListener listener)298     public void addOnCloseListener(OnCloseListener listener) {
299         mOnCloseListeners.add(listener);
300     }
301 
handleClose(boolean animate, long defaultDuration)302     protected void handleClose(boolean animate, long defaultDuration) {
303         if (!mIsOpen) {
304             return;
305         }
306         Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed);
307 
308         if (!animate) {
309             mOpenCloseAnimator.cancel();
310             setTranslationShift(TRANSLATION_SHIFT_CLOSED);
311             onCloseComplete();
312             return;
313         }
314         mOpenCloseAnimator.setValues(
315                 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
316         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
317             @Override
318             public void onAnimationEnd(Animator animation) {
319                 mOpenCloseAnimator.removeListener(this);
320                 onCloseComplete();
321             }
322         });
323         if (mSwipeDetector.isIdleState()) {
324             mOpenCloseAnimator
325                     .setDuration(defaultDuration)
326                     .setInterpolator(getIdleInterpolator());
327         } else {
328             mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
329         }
330         mOpenCloseAnimator.start();
331     }
332 
getIdleInterpolator()333     protected Interpolator getIdleInterpolator() {
334         return Interpolators.ACCEL;
335     }
336 
onCloseComplete()337     protected void onCloseComplete() {
338         mIsOpen = false;
339         getPopupContainer().removeView(this);
340         if (mColorScrim != null) {
341             getPopupContainer().removeView(mColorScrim);
342         }
343         mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed);
344     }
345 
getPopupContainer()346     protected BaseDragLayer getPopupContainer() {
347         return mActivityContext.getDragLayer();
348     }
349 
createColorScrim(Context context, int bgColor)350     protected View createColorScrim(Context context, int bgColor) {
351         View view = new View(context);
352         view.forceHasOverlappingRendering(false);
353         view.setBackgroundColor(bgColor);
354 
355         BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT);
356         lp.ignoreInsets = true;
357         view.setLayoutParams(lp);
358 
359         return view;
360     }
361 
362     /**
363      * Interface to report that the {@link AbstractSlideInView} has closed.
364      */
365     public interface OnCloseListener {
366 
367         /**
368          * Called when {@link AbstractSlideInView} closes.
369          */
onSlideInViewClosed()370         void onSlideInViewClosed();
371     }
372 }
373