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 import android.view.animation.AnimationUtils;
28 import android.view.animation.Interpolator;
29 import com.android.systemui.ExpandHelper;
30 import com.android.systemui.Gefingerpoken;
31 import com.android.systemui.R;
32 
33 /**
34  * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
35  * the notification where the drag started.
36  */
37 public class DragDownHelper implements Gefingerpoken {
38 
39     private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
40     private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
41 
42     private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
43 
44     private int mMinDragDistance;
45     private ExpandHelper.Callback mCallback;
46     private float mInitialTouchX;
47     private float mInitialTouchY;
48     private boolean mDraggingDown;
49     private float mTouchSlop;
50     private DragDownCallback mDragDownCallback;
51     private View mHost;
52     private final int[] mTemp2 = new int[2];
53     private boolean mDraggedFarEnough;
54     private ExpandableView mStartingChild;
55     private Interpolator mInterpolator;
56     private float mLastHeight;
57 
DragDownHelper(Context context, View host, ExpandHelper.Callback callback, DragDownCallback dragDownCallback)58     public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
59             DragDownCallback dragDownCallback) {
60         mMinDragDistance = context.getResources().getDimensionPixelSize(
61                 R.dimen.keyguard_drag_down_min_distance);
62         mInterpolator =
63                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
64         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
65         mCallback = callback;
66         mDragDownCallback = dragDownCallback;
67         mHost = host;
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                     mDraggingDown = true;
88                     captureStartingChild(mInitialTouchX, mInitialTouchY);
89                     mInitialTouchY = y;
90                     mInitialTouchX = x;
91                     mDragDownCallback.onTouchSlopExceeded();
92                     return true;
93                 }
94                 break;
95         }
96         return false;
97     }
98 
99     @Override
onTouchEvent(MotionEvent event)100     public boolean onTouchEvent(MotionEvent event) {
101         if (!mDraggingDown) {
102             return false;
103         }
104         final float x = event.getX();
105         final float y = event.getY();
106 
107         switch (event.getActionMasked()) {
108             case MotionEvent.ACTION_MOVE:
109                 mLastHeight = y - mInitialTouchY;
110                 captureStartingChild(mInitialTouchX, mInitialTouchY);
111                 if (mStartingChild != null) {
112                     handleExpansion(mLastHeight, mStartingChild);
113                 } else {
114                     mDragDownCallback.setEmptyDragAmount(mLastHeight);
115                 }
116                 if (mLastHeight > mMinDragDistance) {
117                     if (!mDraggedFarEnough) {
118                         mDraggedFarEnough = true;
119                         mDragDownCallback.onThresholdReached();
120                     }
121                 } else {
122                     if (mDraggedFarEnough) {
123                         mDraggedFarEnough = false;
124                         mDragDownCallback.onDragDownReset();
125                     }
126                 }
127                 return true;
128             case MotionEvent.ACTION_UP:
129                 if (mDraggedFarEnough && mDragDownCallback.onDraggedDown(mStartingChild,
130                         (int) (y - mInitialTouchY))) {
131                     if (mStartingChild == null) {
132                         mDragDownCallback.setEmptyDragAmount(0f);
133                     }
134                     mDraggingDown = false;
135                 } else {
136                     stopDragging();
137                     return false;
138                 }
139                 break;
140             case MotionEvent.ACTION_CANCEL:
141                 stopDragging();
142                 return false;
143         }
144         return false;
145     }
146 
captureStartingChild(float x, float y)147     private void captureStartingChild(float x, float y) {
148         if (mStartingChild == null) {
149             mStartingChild = findView(x, y);
150             if (mStartingChild != null) {
151                 mCallback.setUserLockedChild(mStartingChild, true);
152             }
153         }
154     }
155 
handleExpansion(float heightDelta, ExpandableView child)156     private void handleExpansion(float heightDelta, ExpandableView child) {
157         if (heightDelta < 0) {
158             heightDelta = 0;
159         }
160         boolean expandable = child.isContentExpandable();
161         float rubberbandFactor = expandable
162                 ? RUBBERBAND_FACTOR_EXPANDABLE
163                 : RUBBERBAND_FACTOR_STATIC;
164         float rubberband = heightDelta * rubberbandFactor;
165         if (expandable && (rubberband + child.getMinHeight()) > child.getMaxContentHeight()) {
166             float overshoot = (rubberband + child.getMinHeight()) - child.getMaxContentHeight();
167             overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
168             rubberband -= overshoot;
169         }
170         child.setContentHeight((int) (child.getMinHeight() + rubberband));
171     }
172 
cancelExpansion(final ExpandableView child)173     private void cancelExpansion(final ExpandableView child) {
174         if (child.getContentHeight() == child.getMinHeight()) {
175             return;
176         }
177         ObjectAnimator anim = ObjectAnimator.ofInt(child, "contentHeight",
178                 child.getContentHeight(), child.getMinHeight());
179         anim.setInterpolator(mInterpolator);
180         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
181         anim.addListener(new AnimatorListenerAdapter() {
182             @Override
183             public void onAnimationEnd(Animator animation) {
184                 mCallback.setUserLockedChild(child, false);
185             }
186         });
187         anim.start();
188     }
189 
cancelExpansion()190     private void cancelExpansion() {
191         ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
192         anim.setInterpolator(mInterpolator);
193         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
194         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
195             @Override
196             public void onAnimationUpdate(ValueAnimator animation) {
197                 mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
198             }
199         });
200         anim.start();
201     }
202 
stopDragging()203     private void stopDragging() {
204         if (mStartingChild != null) {
205             cancelExpansion(mStartingChild);
206         } else {
207             cancelExpansion();
208         }
209         mDraggingDown = false;
210         mDragDownCallback.onDragDownReset();
211     }
212 
findView(float x, float y)213     private ExpandableView findView(float x, float y) {
214         mHost.getLocationOnScreen(mTemp2);
215         x += mTemp2[0];
216         y += mTemp2[1];
217         return mCallback.getChildAtRawPosition(x, y);
218     }
219 
220     public interface DragDownCallback {
221 
222         /**
223          * @return true if the interaction is accepted, false if it should be cancelled
224          */
onDraggedDown(View startingChild, int dragLengthY)225         boolean onDraggedDown(View startingChild, int dragLengthY);
onDragDownReset()226         void onDragDownReset();
onThresholdReached()227         void onThresholdReached();
onTouchSlopExceeded()228         void onTouchSlopExceeded();
setEmptyDragAmount(float amount)229         void setEmptyDragAmount(float amount);
230     }
231 }
232