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