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