1 /*
2  * Copyright (C) 2014 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 
17 package com.android.systemui.statusbar;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.view.ViewConfiguration;
27 
28 import com.android.systemui.ExpandHelper;
29 import com.android.systemui.Gefingerpoken;
30 import com.android.systemui.Interpolators;
31 import com.android.systemui.R;
32 import com.android.systemui.classifier.FalsingManager;
33 import com.android.systemui.statusbar.phone.StatusBar;
34 
35 /**
36  * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
37  * the notification where the drag started.
38  */
39 public class DragDownHelper implements Gefingerpoken {
40 
41     private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
42     private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
43 
44     private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
45 
46     private int mMinDragDistance;
47     private ExpandHelper.Callback mCallback;
48     private float mInitialTouchX;
49     private float mInitialTouchY;
50     private boolean mDraggingDown;
51     private float mTouchSlop;
52     private DragDownCallback mDragDownCallback;
53     private View mHost;
54     private final int[] mTemp2 = new int[2];
55     private boolean mDraggedFarEnough;
56     private ExpandableView mStartingChild;
57     private float mLastHeight;
58     private FalsingManager mFalsingManager;
59 
DragDownHelper(Context context, View host, ExpandHelper.Callback callback, DragDownCallback dragDownCallback)60     public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
61             DragDownCallback dragDownCallback) {
62         mMinDragDistance = context.getResources().getDimensionPixelSize(
63                 R.dimen.keyguard_drag_down_min_distance);
64         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
65         mCallback = callback;
66         mDragDownCallback = dragDownCallback;
67         mHost = host;
68         mFalsingManager = FalsingManager.getInstance(context);
69     }
70 
71     @Override
onInterceptTouchEvent(MotionEvent event)72     public boolean onInterceptTouchEvent(MotionEvent event) {
73         final float x = event.getX();
74         final float y = event.getY();
75 
76         switch (event.getActionMasked()) {
77             case MotionEvent.ACTION_DOWN:
78                 mDraggedFarEnough = false;
79                 mDraggingDown = false;
80                 mStartingChild = null;
81                 mInitialTouchY = y;
82                 mInitialTouchX = x;
83                 break;
84 
85             case MotionEvent.ACTION_MOVE:
86                 final float h = y - mInitialTouchY;
87                 if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
88                     mFalsingManager.onNotificatonStartDraggingDown();
89                     mDraggingDown = true;
90                     captureStartingChild(mInitialTouchX, mInitialTouchY);
91                     mInitialTouchY = y;
92                     mInitialTouchX = x;
93                     mDragDownCallback.onTouchSlopExceeded();
94                     return true;
95                 }
96                 break;
97         }
98         return false;
99     }
100 
101     @Override
onTouchEvent(MotionEvent event)102     public boolean onTouchEvent(MotionEvent event) {
103         if (!mDraggingDown) {
104             return false;
105         }
106         final float x = event.getX();
107         final float y = event.getY();
108 
109         switch (event.getActionMasked()) {
110             case MotionEvent.ACTION_MOVE:
111                 mLastHeight = y - mInitialTouchY;
112                 captureStartingChild(mInitialTouchX, mInitialTouchY);
113                 if (mStartingChild != null) {
114                     handleExpansion(mLastHeight, mStartingChild);
115                 } else {
116                     mDragDownCallback.setEmptyDragAmount(mLastHeight);
117                 }
118                 if (mLastHeight > mMinDragDistance) {
119                     if (!mDraggedFarEnough) {
120                         mDraggedFarEnough = true;
121                         mDragDownCallback.onCrossedThreshold(true);
122                     }
123                 } else {
124                     if (mDraggedFarEnough) {
125                         mDraggedFarEnough = false;
126                         mDragDownCallback.onCrossedThreshold(false);
127                     }
128                 }
129                 return true;
130             case MotionEvent.ACTION_UP:
131                 if (!isFalseTouch() && mDragDownCallback.onDraggedDown(mStartingChild,
132                         (int) (y - mInitialTouchY))) {
133                     if (mStartingChild == null) {
134                         mDragDownCallback.setEmptyDragAmount(0f);
135                     } else {
136                         mCallback.setUserLockedChild(mStartingChild, false);
137                         mStartingChild = null;
138                     }
139                     mDraggingDown = false;
140                 } else {
141                     stopDragging();
142                     return false;
143                 }
144                 break;
145             case MotionEvent.ACTION_CANCEL:
146                 stopDragging();
147                 return false;
148         }
149         return false;
150     }
151 
isFalseTouch()152     private boolean isFalseTouch() {
153         if (!mDragDownCallback.isFalsingCheckNeeded()) {
154             return false;
155         }
156         return mFalsingManager.isFalseTouch() || !mDraggedFarEnough;
157     }
158 
captureStartingChild(float x, float y)159     private void captureStartingChild(float x, float y) {
160         if (mStartingChild == null) {
161             mStartingChild = findView(x, y);
162             if (mStartingChild != null) {
163                 mCallback.setUserLockedChild(mStartingChild, true);
164             }
165         }
166     }
167 
handleExpansion(float heightDelta, ExpandableView child)168     private void handleExpansion(float heightDelta, ExpandableView child) {
169         if (heightDelta < 0) {
170             heightDelta = 0;
171         }
172         boolean expandable = child.isContentExpandable();
173         float rubberbandFactor = expandable
174                 ? RUBBERBAND_FACTOR_EXPANDABLE
175                 : RUBBERBAND_FACTOR_STATIC;
176         float rubberband = heightDelta * rubberbandFactor;
177         if (expandable
178                 && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) {
179             float overshoot =
180                     (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight();
181             overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
182             rubberband -= overshoot;
183         }
184         child.setActualHeight((int) (child.getCollapsedHeight() + rubberband));
185     }
186 
cancelExpansion(final ExpandableView child)187     private void cancelExpansion(final ExpandableView child) {
188         if (child.getActualHeight() == child.getCollapsedHeight()) {
189             mCallback.setUserLockedChild(child, false);
190             return;
191         }
192         ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
193                 child.getActualHeight(), child.getCollapsedHeight());
194         anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
195         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
196         anim.addListener(new AnimatorListenerAdapter() {
197             @Override
198             public void onAnimationEnd(Animator animation) {
199                 mCallback.setUserLockedChild(child, false);
200             }
201         });
202         anim.start();
203     }
204 
cancelExpansion()205     private void cancelExpansion() {
206         ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
207         anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
208         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
209         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
210             @Override
211             public void onAnimationUpdate(ValueAnimator animation) {
212                 mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
213             }
214         });
215         anim.start();
216     }
217 
stopDragging()218     private void stopDragging() {
219         mFalsingManager.onNotificatonStopDraggingDown();
220         if (mStartingChild != null) {
221             cancelExpansion(mStartingChild);
222             mStartingChild = null;
223         } else {
224             cancelExpansion();
225         }
226         mDraggingDown = false;
227         mDragDownCallback.onDragDownReset();
228     }
229 
findView(float x, float y)230     private ExpandableView findView(float x, float y) {
231         mHost.getLocationOnScreen(mTemp2);
232         x += mTemp2[0];
233         y += mTemp2[1];
234         return mCallback.getChildAtRawPosition(x, y);
235     }
236 
isDraggingDown()237     public boolean isDraggingDown() {
238         return mDraggingDown;
239     }
240 
241     public interface DragDownCallback {
242 
243         /**
244          * @return true if the interaction is accepted, false if it should be cancelled
245          */
onDraggedDown(View startingChild, int dragLengthY)246         boolean onDraggedDown(View startingChild, int dragLengthY);
onDragDownReset()247         void onDragDownReset();
248 
249         /**
250          * The user has dragged either above or below the threshold
251          * @param above whether he dragged above it
252          */
onCrossedThreshold(boolean above)253         void onCrossedThreshold(boolean above);
onTouchSlopExceeded()254         void onTouchSlopExceeded();
setEmptyDragAmount(float amount)255         void setEmptyDragAmount(float amount);
isFalsingCheckNeeded()256         boolean isFalsingCheckNeeded();
257     }
258 }
259