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