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.anim.Interpolators.scrollInterpolatorForVelocity;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ObjectAnimator;
25 import android.animation.PropertyValuesHolder;
26 import android.content.Context;
27 import android.util.AttributeSet;
28 import android.util.Property;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.animation.Interpolator;
33 
34 import com.android.launcher3.AbstractFloatingView;
35 import com.android.launcher3.Utilities;
36 import com.android.launcher3.anim.Interpolators;
37 import com.android.launcher3.touch.BaseSwipeDetector;
38 import com.android.launcher3.touch.SingleAxisSwipeDetector;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * Extension of {@link AbstractFloatingView} with common methods for sliding in from bottom.
45  *
46  * @param <T> Type of ActivityContext inflating this view.
47  */
48 public abstract class AbstractSlideInView<T extends Context & ActivityContext>
49         extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {
50 
51     protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
52             new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
53 
54                 @Override
55                 public Float get(AbstractSlideInView view) {
56                     return view.mTranslationShift;
57                 }
58 
59                 @Override
60                 public void set(AbstractSlideInView view, Float value) {
61                     view.setTranslationShift(value);
62                 }
63             };
64     protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
65     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
66 
67     protected final T mActivityContext;
68 
69     protected final SingleAxisSwipeDetector mSwipeDetector;
70     protected final ObjectAnimator mOpenCloseAnimator;
71 
72     protected ViewGroup mContent;
73     protected final View mColorScrim;
74     protected Interpolator mScrollInterpolator;
75 
76     // range [0, 1], 0=> completely open, 1=> completely closed
77     protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
78 
79     protected boolean mNoIntercept;
80     protected List<OnCloseListener> mOnCloseListeners = new ArrayList<>();
81 
AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr)82     public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) {
83         super(context, attrs, defStyleAttr);
84         mActivityContext = ActivityContext.lookupContext(context);
85 
86         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
87         mSwipeDetector = new SingleAxisSwipeDetector(context, this,
88                 SingleAxisSwipeDetector.VERTICAL);
89 
90         mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
91         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
92             @Override
93             public void onAnimationEnd(Animator animation) {
94                 mSwipeDetector.finishedScrolling();
95                 announceAccessibilityChanges();
96             }
97         });
98         int scrimColor = getScrimColor(context);
99         mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
100     }
101 
attachToContainer()102     protected void attachToContainer() {
103         if (mColorScrim != null) {
104             getPopupContainer().addView(mColorScrim);
105         }
106         getPopupContainer().addView(this);
107     }
108 
109     /**
110      * Returns a scrim color for a sliding view. if returned value is -1, no scrim is added.
111      */
getScrimColor(Context context)112     protected int getScrimColor(Context context) {
113         return -1;
114     }
115 
setTranslationShift(float translationShift)116     protected void setTranslationShift(float translationShift) {
117         mTranslationShift = translationShift;
118         mContent.setTranslationY(mTranslationShift * mContent.getHeight());
119         if (mColorScrim != null) {
120             mColorScrim.setAlpha(1 - mTranslationShift);
121         }
122     }
123 
124     @Override
onControllerInterceptTouchEvent(MotionEvent ev)125     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
126         if (mNoIntercept) {
127             return false;
128         }
129 
130         int directionsToDetectScroll = mSwipeDetector.isIdleState()
131                 ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
132         mSwipeDetector.setDetectableScrollConditions(
133                 directionsToDetectScroll, false);
134         mSwipeDetector.onTouchEvent(ev);
135         return mSwipeDetector.isDraggingOrSettling()
136                 || !getPopupContainer().isEventOverView(mContent, ev);
137     }
138 
139     @Override
onControllerTouchEvent(MotionEvent ev)140     public boolean onControllerTouchEvent(MotionEvent ev) {
141         mSwipeDetector.onTouchEvent(ev);
142         if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState()
143                 && !isOpeningAnimationRunning()) {
144             // If we got ACTION_UP without ever starting swipe, close the panel.
145             if (!getPopupContainer().isEventOverView(mContent, ev)) {
146                 close(true);
147             }
148         }
149         return true;
150     }
151 
isOpeningAnimationRunning()152     private boolean isOpeningAnimationRunning() {
153         return mIsOpen && mOpenCloseAnimator.isRunning();
154     }
155 
156     /* SingleAxisSwipeDetector.Listener */
157 
158     @Override
onDragStart(boolean start, float startDisplacement)159     public void onDragStart(boolean start, float startDisplacement) { }
160 
161     @Override
onDrag(float displacement)162     public boolean onDrag(float displacement) {
163         float range = mContent.getHeight();
164         displacement = Utilities.boundToRange(displacement, 0, range);
165         setTranslationShift(displacement / range);
166         return true;
167     }
168 
169     @Override
onDragEnd(float velocity)170     public void onDragEnd(float velocity) {
171         if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > 0.5f) {
172             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
173             mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
174                     velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
175             close(true);
176         } else {
177             mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
178                     TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
179             mOpenCloseAnimator.setDuration(
180                     BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
181                     .setInterpolator(Interpolators.DEACCEL);
182             mOpenCloseAnimator.start();
183         }
184     }
185 
186     /** Registers an {@link OnCloseListener}. */
addOnCloseListener(OnCloseListener listener)187     public void addOnCloseListener(OnCloseListener listener) {
188         mOnCloseListeners.add(listener);
189     }
190 
handleClose(boolean animate, long defaultDuration)191     protected void handleClose(boolean animate, long defaultDuration) {
192         if (!mIsOpen) {
193             return;
194         }
195         if (!animate) {
196             mOpenCloseAnimator.cancel();
197             setTranslationShift(TRANSLATION_SHIFT_CLOSED);
198             onCloseComplete();
199             return;
200         }
201         mOpenCloseAnimator.setValues(
202                 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
203         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
204             @Override
205             public void onAnimationEnd(Animator animation) {
206                 onCloseComplete();
207             }
208         });
209         if (mSwipeDetector.isIdleState()) {
210             mOpenCloseAnimator
211                     .setDuration(defaultDuration)
212                     .setInterpolator(Interpolators.ACCEL);
213         } else {
214             mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
215         }
216         mOpenCloseAnimator.start();
217     }
218 
onCloseComplete()219     protected void onCloseComplete() {
220         mIsOpen = false;
221         getPopupContainer().removeView(this);
222         if (mColorScrim != null) {
223             getPopupContainer().removeView(mColorScrim);
224         }
225         mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed);
226     }
227 
getPopupContainer()228     protected BaseDragLayer getPopupContainer() {
229         return mActivityContext.getDragLayer();
230     }
231 
createColorScrim(Context context, int bgColor)232     protected View createColorScrim(Context context, int bgColor) {
233         View view = new View(context);
234         view.forceHasOverlappingRendering(false);
235         view.setBackgroundColor(bgColor);
236 
237         BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT);
238         lp.ignoreInsets = true;
239         view.setLayoutParams(lp);
240 
241         return view;
242     }
243 
244     /**
245      * Interface to report that the {@link AbstractSlideInView} has closed.
246      */
247     public interface OnCloseListener {
248 
249         /**
250          * Called when {@link AbstractSlideInView} closes.
251          */
onSlideInViewClosed()252         void onSlideInViewClosed();
253     }
254 }
255