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