• 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 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.FalsingManagerFactory;
33 import com.android.systemui.plugins.FalsingManager;
34 import com.android.systemui.statusbar.notification.row.ExpandableView;
35 
36 /**
37  * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
38  * the notification where the drag started.
39  */
40 public class DragDownHelper implements Gefingerpoken {
41 
42     private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
43     private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
44 
45     private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
46 
47     private int mMinDragDistance;
48     private ExpandHelper.Callback mCallback;
49     private float mInitialTouchX;
50     private float mInitialTouchY;
51     private boolean mDraggingDown;
52     private float mTouchSlop;
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)61     public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
62             DragDownCallback dragDownCallback) {
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 = FalsingManagerFactory.getInstance(context);
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 true;
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                 mCallback.setUserLockedChild(mStartingChild, true);
166             }
167         }
168     }
169 
handleExpansion(float heightDelta, ExpandableView child)170     private void handleExpansion(float heightDelta, ExpandableView child) {
171         if (heightDelta < 0) {
172             heightDelta = 0;
173         }
174         boolean expandable = child.isContentExpandable();
175         float rubberbandFactor = expandable
176                 ? RUBBERBAND_FACTOR_EXPANDABLE
177                 : RUBBERBAND_FACTOR_STATIC;
178         float rubberband = heightDelta * rubberbandFactor;
179         if (expandable
180                 && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) {
181             float overshoot =
182                     (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight();
183             overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
184             rubberband -= overshoot;
185         }
186         child.setActualHeight((int) (child.getCollapsedHeight() + rubberband));
187     }
188 
cancelExpansion(final ExpandableView child)189     private void cancelExpansion(final ExpandableView child) {
190         if (child.getActualHeight() == child.getCollapsedHeight()) {
191             mCallback.setUserLockedChild(child, false);
192             return;
193         }
194         ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
195                 child.getActualHeight(), child.getCollapsedHeight());
196         anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
197         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
198         anim.addListener(new AnimatorListenerAdapter() {
199             @Override
200             public void onAnimationEnd(Animator animation) {
201                 mCallback.setUserLockedChild(child, false);
202             }
203         });
204         anim.start();
205     }
206 
cancelExpansion()207     private void cancelExpansion() {
208         ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
209         anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
210         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
211         anim.addUpdateListener(animation -> {
212             mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
213         });
214         anim.start();
215     }
216 
stopDragging()217     private void stopDragging() {
218         mFalsingManager.onNotificatonStopDraggingDown();
219         if (mStartingChild != null) {
220             cancelExpansion(mStartingChild);
221             mStartingChild = null;
222         } else {
223             cancelExpansion();
224         }
225         mDraggingDown = false;
226         mDragDownCallback.onDragDownReset();
227     }
228 
findView(float x, float y)229     private ExpandableView findView(float x, float y) {
230         mHost.getLocationOnScreen(mTemp2);
231         x += mTemp2[0];
232         y += mTemp2[1];
233         return mCallback.getChildAtRawPosition(x, y);
234     }
235 
isDraggingDown()236     public boolean isDraggingDown() {
237         return mDraggingDown;
238     }
239 
240     public interface DragDownCallback {
241 
242         /**
243          * @return true if the interaction is accepted, false if it should be cancelled
244          */
onDraggedDown(View startingChild, int dragLengthY)245         boolean onDraggedDown(View startingChild, int dragLengthY);
onDragDownReset()246         void onDragDownReset();
247 
248         /**
249          * The user has dragged either above or below the threshold
250          * @param above whether he dragged above it
251          */
onCrossedThreshold(boolean above)252         void onCrossedThreshold(boolean above);
onTouchSlopExceeded()253         void onTouchSlopExceeded();
setEmptyDragAmount(float amount)254         void setEmptyDragAmount(float amount);
isFalsingCheckNeeded()255         boolean isFalsingCheckNeeded();
256     }
257 }
258