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