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