blob: 59af458e2402557bd0faf88cc15e02c4be593308 [file] [log] [blame]
Daniel Sandler6a858c32012-03-12 14:38:58 -04001/*
2 * Copyright (C) 2012 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
18package com.android.systemui;
19
Romain Guy8900e632012-05-25 12:08:39 -070020import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
Daniel Sandler6a858c32012-03-12 14:38:58 -040022import android.animation.ObjectAnimator;
23import android.content.Context;
John Spurlockcd686b52013-06-05 10:13:46 -040024import android.util.Log;
Chris Wren9b2cd152012-06-11 10:39:36 -040025import android.view.Gravity;
Jorim Jaggi9ff15102015-06-10 15:58:08 -070026import android.view.HapticFeedbackConstants;
Daniel Sandler6a858c32012-03-12 14:38:58 -040027import android.view.MotionEvent;
28import android.view.ScaleGestureDetector;
Daniel Sandlerac47ff72012-10-23 10:41:44 -040029import android.view.ScaleGestureDetector.OnScaleGestureListener;
Jorim Jaggi28c0b7142014-07-26 12:36:35 +020030import android.view.VelocityTracker;
Daniel Sandler6a858c32012-03-12 14:38:58 -040031import android.view.View;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -040032import android.view.ViewConfiguration;
Daniel Sandler6a858c32012-03-12 14:38:58 -040033
Selim Cinek3acdc8e72017-04-06 17:38:27 -070034import com.android.internal.annotations.VisibleForTesting;
Gus Prevasab336792018-11-14 13:52:20 -050035import com.android.systemui.statusbar.FlingAnimationUtils;
Rohan Shah20790b82018-07-02 17:21:04 -070036import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
37import com.android.systemui.statusbar.notification.row.ExpandableView;
Selim Cinekb6d85eb2014-03-28 20:21:01 +010038import com.android.systemui.statusbar.policy.ScrollAdapter;
39
Jorim Jaggi4222d9a2014-04-23 16:13:15 +020040public class ExpandHelper implements Gefingerpoken {
Daniel Sandler6a858c32012-03-12 14:38:58 -040041 public interface Callback {
Jorim Jaggi4222d9a2014-04-23 16:13:15 +020042 ExpandableView getChildAtRawPosition(float x, float y);
43 ExpandableView getChildAtPosition(float x, float y);
Chris Wren80a76272012-04-18 10:52:18 -040044 boolean canChildBeExpanded(View v);
Chris Wren51c75102013-07-16 20:49:17 -040045 void setUserExpandedChild(View v, boolean userExpanded);
46 void setUserLockedChild(View v, boolean userLocked);
Selim Cinek1408eb52014-06-02 14:45:38 +020047 void expansionStateChanged(boolean isExpanding);
Selim Cinek1b2a05e2016-04-28 14:20:39 -070048 int getMaxExpandHeight(ExpandableView view);
Mady Mellorb0a82462016-04-30 17:31:02 -070049 void setExpansionCancelled(View view);
Daniel Sandler6a858c32012-03-12 14:38:58 -040050 }
51
52 private static final String TAG = "ExpandHelper";
Chris Wrene46647d2012-06-20 16:18:10 -040053 protected static final boolean DEBUG = false;
Chris Wren3c148f12012-06-19 13:10:25 -040054 protected static final boolean DEBUG_SCALE = false;
Jorim Jaggi28c0b7142014-07-26 12:36:35 +020055 private static final float EXPAND_DURATION = 0.3f;
Chris Wrenba925e82012-04-20 16:46:43 -040056
Daniel Sandler4377d142012-09-11 15:18:47 -040057 // Set to false to disable focus-based gestures (spread-finger vertical pull).
Chris Wren89139d72012-05-07 17:00:06 -040058 private static final boolean USE_DRAG = true;
59 // Set to false to disable scale-based gestures (both horizontal and vertical).
60 private static final boolean USE_SPAN = true;
61 // Both gestures types may be active at the same time.
62 // At least one gesture type should be active.
63 // A variant of the screwdriver gesture will emerge from either gesture type.
Daniel Sandler6a858c32012-03-12 14:38:58 -040064
Chris Wren80a76272012-04-18 10:52:18 -040065 // amount of overstretch for maximum brightness expressed in U
66 // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
67 private static final float STRETCH_INTERVAL = 2f;
68
Daniel Sandler6a858c32012-03-12 14:38:58 -040069 @SuppressWarnings("unused")
70 private Context mContext;
71
Daniel Sandler4377d142012-09-11 15:18:47 -040072 private boolean mExpanding;
73 private static final int NONE = 0;
74 private static final int BLINDS = 1<<0;
75 private static final int PULL = 1<<1;
76 private static final int STRETCH = 1<<2;
77 private int mExpansionStyle = NONE;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -040078 private boolean mWatchingForPull;
79 private boolean mHasPopped;
Chris Wren5de6e942012-05-16 14:22:21 -040080 private View mEventSource;
Daniel Sandler6a858c32012-03-12 14:38:58 -040081 private float mOldHeight;
82 private float mNaturalHeight;
Chris Wren89139d72012-05-07 17:00:06 -040083 private float mInitialTouchFocusY;
Selim Cinek821d6a72016-03-11 17:24:19 -080084 private float mInitialTouchX;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -040085 private float mInitialTouchY;
Daniel Sandler6a858c32012-03-12 14:38:58 -040086 private float mInitialTouchSpan;
Chris Wrencea52072012-10-10 21:02:56 -040087 private float mLastFocusY;
88 private float mLastSpanY;
Philip Quinn47169132020-03-23 19:04:55 -070089 private final int mTouchSlop;
90 private final float mSlopMultiplier;
Selim Cinek1408eb52014-06-02 14:45:38 +020091 private float mLastMotionY;
Daniel Sandler4377d142012-09-11 15:18:47 -040092 private float mPullGestureMinXSpan;
Daniel Sandler6a858c32012-03-12 14:38:58 -040093 private Callback mCallback;
Daniel Sandler4377d142012-09-11 15:18:47 -040094 private ScaleGestureDetector mSGD;
Daniel Sandler6a858c32012-03-12 14:38:58 -040095 private ViewScaler mScaler;
Chris Wrenba925e82012-04-20 16:46:43 -040096 private ObjectAnimator mScaleAnimation;
Selim Cinek1408eb52014-06-02 14:45:38 +020097 private boolean mEnabled = true;
98 private ExpandableView mResizedView;
99 private float mCurrentHeight;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400100
101 private int mSmallSize;
102 private int mLargeSize;
Chris Wren80a76272012-04-18 10:52:18 -0400103 private float mMaximumStretch;
Selim Cinek1408eb52014-06-02 14:45:38 +0200104 private boolean mOnlyMovements;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400105
Chris Wren9b2cd152012-06-11 10:39:36 -0400106 private int mGravity;
107
Selim Cinekfab078b2014-03-27 22:45:58 +0100108 private ScrollAdapter mScrollAdapter;
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200109 private FlingAnimationUtils mFlingAnimationUtils;
110 private VelocityTracker mVelocityTracker;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400111
John Spurlock209bede2013-07-17 12:23:27 -0400112 private OnScaleGestureListener mScaleGestureListener
Daniel Sandlerac47ff72012-10-23 10:41:44 -0400113 = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
114 @Override
115 public boolean onScaleBegin(ScaleGestureDetector detector) {
John Spurlockcd686b52013-06-05 10:13:46 -0400116 if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()");
Daniel Sandlerac47ff72012-10-23 10:41:44 -0400117
Jorim Jaggi5e8e1c62015-09-04 11:51:26 -0700118 if (!mOnlyMovements) {
119 startExpanding(mResizedView, STRETCH);
120 }
Daniel Sandlerac47ff72012-10-23 10:41:44 -0400121 return mExpanding;
122 }
123
124 @Override
125 public boolean onScale(ScaleGestureDetector detector) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200126 if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView);
Daniel Sandlerac47ff72012-10-23 10:41:44 -0400127 return true;
128 }
129
130 @Override
131 public void onScaleEnd(ScaleGestureDetector detector) {
132 }
133 };
134
Selim Cinek3acdc8e72017-04-06 17:38:27 -0700135 @VisibleForTesting
136 ObjectAnimator getScaleAnimation() {
137 return mScaleAnimation;
138 }
139
Daniel Sandler6a858c32012-03-12 14:38:58 -0400140 private class ViewScaler {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200141 ExpandableView mView;
Chris Wren9b2cd152012-06-11 10:39:36 -0400142
Daniel Sandler6a858c32012-03-12 14:38:58 -0400143 public ViewScaler() {}
Jorim Jaggibe565df2014-04-28 17:51:23 +0200144 public void setView(ExpandableView v) {
Daniel Sandler6a858c32012-03-12 14:38:58 -0400145 mView = v;
146 }
147 public void setHeight(float h) {
John Spurlockcd686b52013-06-05 10:13:46 -0400148 if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
Selim Cinekeef84282015-10-30 16:28:00 -0700149 mView.setActualHeight((int) h);
Selim Cinek1408eb52014-06-02 14:45:38 +0200150 mCurrentHeight = h;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400151 }
152 public float getHeight() {
Selim Cinekeef84282015-10-30 16:28:00 -0700153 return mView.getActualHeight();
Daniel Sandler6a858c32012-03-12 14:38:58 -0400154 }
Selim Cinek388df6d2015-10-22 13:25:11 -0700155 public int getNaturalHeight() {
Selim Cinek1b2a05e2016-04-28 14:20:39 -0700156 return mCallback.getMaxExpandHeight(mView);
Daniel Sandler6a858c32012-03-12 14:38:58 -0400157 }
158 }
159
Chris Wren9b2cd152012-06-11 10:39:36 -0400160 /**
161 * Handle expansion gestures to expand and contract children of the callback.
162 *
163 * @param context application context
164 * @param callback the container that holds the items to be manipulated
165 * @param small the smallest allowable size for the manuipulated items.
166 * @param large the largest allowable size for the manuipulated items.
Chris Wren9b2cd152012-06-11 10:39:36 -0400167 */
Daniel Sandler6a858c32012-03-12 14:38:58 -0400168 public ExpandHelper(Context context, Callback callback, int small, int large) {
169 mSmallSize = small;
Chris Wren80a76272012-04-18 10:52:18 -0400170 mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400171 mLargeSize = large;
172 mContext = context;
173 mCallback = callback;
174 mScaler = new ViewScaler();
Chris Wren9b2cd152012-06-11 10:39:36 -0400175 mGravity = Gravity.TOP;
Chris Wrenba925e82012-04-20 16:46:43 -0400176 mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
Daniel Sandler4377d142012-09-11 15:18:47 -0400177 mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
Chris Wrenba925e82012-04-20 16:46:43 -0400178
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400179 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
180 mTouchSlop = configuration.getScaledTouchSlop();
Philip Quinn47169132020-03-23 19:04:55 -0700181 mSlopMultiplier = configuration.getAmbiguousGestureMultiplier();
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400182
Daniel Sandlerac47ff72012-10-23 10:41:44 -0400183 mSGD = new ScaleGestureDetector(context, mScaleGestureListener);
Dave Mankoff1373fdb2019-12-18 14:04:37 -0500184 mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(),
185 EXPAND_DURATION);
Daniel Sandler6a858c32012-03-12 14:38:58 -0400186 }
Chris Wren5de6e942012-05-16 14:22:21 -0400187
Selim Cinek3acdc8e72017-04-06 17:38:27 -0700188 @VisibleForTesting
189 void updateExpansion() {
John Spurlockcd686b52013-06-05 10:13:46 -0400190 if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()");
Daniel Sandler4377d142012-09-11 15:18:47 -0400191 // are we scaling or dragging?
Chris Wrencea52072012-10-10 21:02:56 -0400192 float span = mSGD.getCurrentSpan() - mInitialTouchSpan;
Daniel Sandler4377d142012-09-11 15:18:47 -0400193 span *= USE_SPAN ? 1f : 0f;
194 float drag = mSGD.getFocusY() - mInitialTouchFocusY;
195 drag *= USE_DRAG ? 1f : 0f;
196 drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
197 float pull = Math.abs(drag) + Math.abs(span) + 1f;
198 float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
199 float target = hand + mOldHeight;
200 float newHeight = clamp(target);
201 mScaler.setHeight(newHeight);
Chris Wrencea52072012-10-10 21:02:56 -0400202 mLastFocusY = mSGD.getFocusY();
203 mLastSpanY = mSGD.getCurrentSpan();
Daniel Sandler4377d142012-09-11 15:18:47 -0400204 }
205
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400206 private float clamp(float target) {
207 float out = target;
Selim Cinek388df6d2015-10-22 13:25:11 -0700208 out = out < mSmallSize ? mSmallSize : out;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400209 out = out > mNaturalHeight ? mNaturalHeight : out;
210 return out;
211 }
212
Jorim Jaggi4222d9a2014-04-23 16:13:15 +0200213 private ExpandableView findView(float x, float y) {
214 ExpandableView v;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400215 if (mEventSource != null) {
216 int[] location = new int[2];
217 mEventSource.getLocationOnScreen(location);
Daniel Sandler4377d142012-09-11 15:18:47 -0400218 x += location[0];
219 y += location[1];
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400220 v = mCallback.getChildAtRawPosition(x, y);
221 } else {
222 v = mCallback.getChildAtPosition(x, y);
223 }
224 return v;
225 }
226
227 private boolean isInside(View v, float x, float y) {
John Spurlockcd686b52013-06-05 10:13:46 -0400228 if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")");
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400229
230 if (v == null) {
John Spurlockcd686b52013-06-05 10:13:46 -0400231 if (DEBUG) Log.d(TAG, "isinside null subject");
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400232 return false;
233 }
234 if (mEventSource != null) {
235 int[] location = new int[2];
236 mEventSource.getLocationOnScreen(location);
Daniel Sandler4377d142012-09-11 15:18:47 -0400237 x += location[0];
238 y += location[1];
John Spurlockcd686b52013-06-05 10:13:46 -0400239 if (DEBUG) Log.d(TAG, " to global (" + x + ", " + y + ")");
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400240 }
241 int[] location = new int[2];
242 v.getLocationOnScreen(location);
Daniel Sandler4377d142012-09-11 15:18:47 -0400243 x -= location[0];
244 y -= location[1];
John Spurlockcd686b52013-06-05 10:13:46 -0400245 if (DEBUG) Log.d(TAG, " to local (" + x + ", " + y + ")");
246 if (DEBUG) Log.d(TAG, " inside (" + v.getWidth() + ", " + v.getHeight() + ")");
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400247 boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
248 return inside;
249 }
250
Chris Wren5de6e942012-05-16 14:22:21 -0400251 public void setEventSource(View eventSource) {
252 mEventSource = eventSource;
253 }
254
Chris Wren9b2cd152012-06-11 10:39:36 -0400255 public void setGravity(int gravity) {
256 mGravity = gravity;
257 }
258
Selim Cinekfab078b2014-03-27 22:45:58 +0100259 public void setScrollAdapter(ScrollAdapter adapter) {
260 mScrollAdapter = adapter;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400261 }
262
Philip Quinn47169132020-03-23 19:04:55 -0700263 private float getTouchSlop(MotionEvent event) {
264 // Adjust the touch slop if another gesture may be being performed.
265 return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
266 ? mTouchSlop * mSlopMultiplier
267 : mTouchSlop;
268 }
269
Daniel Sandler4377d142012-09-11 15:18:47 -0400270 @Override
Daniel Sandler6a858c32012-03-12 14:38:58 -0400271 public boolean onInterceptTouchEvent(MotionEvent ev) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200272 if (!isEnabled()) {
273 return false;
274 }
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200275 trackVelocity(ev);
Daniel Sandler4377d142012-09-11 15:18:47 -0400276 final int action = ev.getAction();
John Spurlockcd686b52013-06-05 10:13:46 -0400277 if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
Daniel Sandler4377d142012-09-11 15:18:47 -0400278 " expanding=" + mExpanding +
279 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
280 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
281 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
282 // check for a spread-finger vertical pull gesture
283 mSGD.onTouchEvent(ev);
284 final int x = (int) mSGD.getFocusX();
285 final int y = (int) mSGD.getFocusY();
Chris Wrencea52072012-10-10 21:02:56 -0400286
287 mInitialTouchFocusY = y;
288 mInitialTouchSpan = mSGD.getCurrentSpan();
289 mLastFocusY = mInitialTouchFocusY;
290 mLastSpanY = mInitialTouchSpan;
John Spurlockcd686b52013-06-05 10:13:46 -0400291 if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan);
Chris Wrencea52072012-10-10 21:02:56 -0400292
Daniel Sandler4377d142012-09-11 15:18:47 -0400293 if (mExpanding) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200294 mLastMotionY = ev.getRawY();
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200295 maybeRecycleVelocityTracker(ev);
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400296 return true;
297 } else {
Daniel Sandler4377d142012-09-11 15:18:47 -0400298 if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
299 // we've begun Venetian blinds style expansion
300 return true;
301 }
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400302 switch (action & MotionEvent.ACTION_MASK) {
303 case MotionEvent.ACTION_MOVE: {
Selim Cinek1408eb52014-06-02 14:45:38 +0200304 final float xspan = mSGD.getCurrentSpanX();
305 if (xspan > mPullGestureMinXSpan &&
306 xspan > mSGD.getCurrentSpanY() && !mExpanding) {
307 // detect a vertical pulling gesture with fingers somewhat separated
308 if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
309 startExpanding(mResizedView, PULL);
310 mWatchingForPull = false;
311 }
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400312 if (mWatchingForPull) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200313 final float yDiff = ev.getRawY() - mInitialTouchY;
Selim Cinek821d6a72016-03-11 17:24:19 -0800314 final float xDiff = ev.getRawX() - mInitialTouchX;
Philip Quinn47169132020-03-23 19:04:55 -0700315 if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) {
John Spurlockcd686b52013-06-05 10:13:46 -0400316 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
Selim Cinek1408eb52014-06-02 14:45:38 +0200317 mWatchingForPull = false;
318 if (mResizedView != null && !isFullyExpanded(mResizedView)) {
319 if (startExpanding(mResizedView, BLINDS)) {
320 mLastMotionY = ev.getRawY();
321 mInitialTouchY = ev.getRawY();
322 mHasPopped = false;
323 }
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400324 }
325 }
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400326 }
327 break;
328 }
329
330 case MotionEvent.ACTION_DOWN:
Chris Wrende3b1a22014-03-31 13:36:25 -0400331 mWatchingForPull = mScrollAdapter != null &&
Selim Cinek1408eb52014-06-02 14:45:38 +0200332 isInside(mScrollAdapter.getHostView(), x, y)
333 && mScrollAdapter.isScrolledToTop();
334 mResizedView = findView(x, y);
Selim Cineke53e6bb2015-04-13 16:14:26 -0700335 if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) {
Selim Cinekd2281152015-04-10 14:37:46 -0700336 mResizedView = null;
337 mWatchingForPull = false;
338 }
Selim Cinekfe090652016-04-18 15:57:57 -0700339 mInitialTouchY = ev.getRawY();
340 mInitialTouchX = ev.getRawX();
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400341 break;
342
343 case MotionEvent.ACTION_CANCEL:
344 case MotionEvent.ACTION_UP:
John Spurlockcd686b52013-06-05 10:13:46 -0400345 if (DEBUG) Log.d(TAG, "up/cancel");
Selim Cinekc25af402017-01-12 11:57:11 -0800346 finishExpanding(ev.getActionMasked() == MotionEvent.ACTION_CANCEL /* forceAbort */,
Selim Cinek740c1112016-12-22 16:39:54 +0100347 getCurrentVelocity());
Daniel Sandler4377d142012-09-11 15:18:47 -0400348 clearView();
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400349 break;
350 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200351 mLastMotionY = ev.getRawY();
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200352 maybeRecycleVelocityTracker(ev);
Daniel Sandler4377d142012-09-11 15:18:47 -0400353 return mExpanding;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400354 }
Daniel Sandler6a858c32012-03-12 14:38:58 -0400355 }
356
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200357 private void trackVelocity(MotionEvent event) {
358 int action = event.getActionMasked();
359 switch(action) {
360 case MotionEvent.ACTION_DOWN:
361 if (mVelocityTracker == null) {
362 mVelocityTracker = VelocityTracker.obtain();
363 } else {
364 mVelocityTracker.clear();
365 }
366 mVelocityTracker.addMovement(event);
367 break;
368 case MotionEvent.ACTION_MOVE:
Selim Cinek1b7f51e2014-10-28 15:20:34 +0100369 if (mVelocityTracker == null) {
370 mVelocityTracker = VelocityTracker.obtain();
371 }
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200372 mVelocityTracker.addMovement(event);
373 break;
374 default:
375 break;
376 }
377 }
378
379 private void maybeRecycleVelocityTracker(MotionEvent event) {
Jorim Jaggic5038962014-07-29 20:05:54 +0200380 if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL
381 || event.getActionMasked() == MotionEvent.ACTION_UP)) {
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200382 mVelocityTracker.recycle();
383 mVelocityTracker = null;
384 }
385 }
386
387 private float getCurrentVelocity() {
388 if (mVelocityTracker != null) {
389 mVelocityTracker.computeCurrentVelocity(1000);
390 return mVelocityTracker.getYVelocity();
391 } else {
392 return 0f;
393 }
394 }
395
Selim Cinek1408eb52014-06-02 14:45:38 +0200396 public void setEnabled(boolean enable) {
397 mEnabled = enable;
398 }
399
400 private boolean isEnabled() {
401 return mEnabled;
402 }
403
404 private boolean isFullyExpanded(ExpandableView underFocus) {
Selim Cinek388df6d2015-10-22 13:25:11 -0700405 return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight()
406 && (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded());
Selim Cinek1408eb52014-06-02 14:45:38 +0200407 }
408
Daniel Sandler4377d142012-09-11 15:18:47 -0400409 @Override
Daniel Sandler6a858c32012-03-12 14:38:58 -0400410 public boolean onTouchEvent(MotionEvent ev) {
Selim Cinek740c1112016-12-22 16:39:54 +0100411 if (!isEnabled() && !mExpanding) {
412 // In case we're expanding we still want to finish the current motion.
Selim Cinek1408eb52014-06-02 14:45:38 +0200413 return false;
414 }
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200415 trackVelocity(ev);
Chris Wrencea52072012-10-10 21:02:56 -0400416 final int action = ev.getActionMasked();
John Spurlockcd686b52013-06-05 10:13:46 -0400417 if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
Daniel Sandler4377d142012-09-11 15:18:47 -0400418 " expanding=" + mExpanding +
419 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
420 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
421 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
422
423 mSGD.onTouchEvent(ev);
Selim Cinek1408eb52014-06-02 14:45:38 +0200424 final int x = (int) mSGD.getFocusX();
425 final int y = (int) mSGD.getFocusY();
Daniel Sandler4377d142012-09-11 15:18:47 -0400426
Selim Cinek1408eb52014-06-02 14:45:38 +0200427 if (mOnlyMovements) {
428 mLastMotionY = ev.getRawY();
429 return false;
430 }
Daniel Sandler6a858c32012-03-12 14:38:58 -0400431 switch (action) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200432 case MotionEvent.ACTION_DOWN:
433 mWatchingForPull = mScrollAdapter != null &&
434 isInside(mScrollAdapter.getHostView(), x, y);
435 mResizedView = findView(x, y);
Selim Cinekfe090652016-04-18 15:57:57 -0700436 mInitialTouchX = ev.getRawX();
437 mInitialTouchY = ev.getRawY();
Selim Cinek1408eb52014-06-02 14:45:38 +0200438 break;
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400439 case MotionEvent.ACTION_MOVE: {
Selim Cinek1408eb52014-06-02 14:45:38 +0200440 if (mWatchingForPull) {
441 final float yDiff = ev.getRawY() - mInitialTouchY;
Selim Cinek821d6a72016-03-11 17:24:19 -0800442 final float xDiff = ev.getRawX() - mInitialTouchX;
Philip Quinn47169132020-03-23 19:04:55 -0700443 if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200444 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
445 mWatchingForPull = false;
446 if (mResizedView != null && !isFullyExpanded(mResizedView)) {
447 if (startExpanding(mResizedView, BLINDS)) {
448 mInitialTouchY = ev.getRawY();
449 mLastMotionY = ev.getRawY();
450 mHasPopped = false;
451 }
452 }
453 }
454 }
455 if (mExpanding && 0 != (mExpansionStyle & BLINDS)) {
456 final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight;
Chris Wren86d00fb2012-08-01 17:03:07 -0400457 final float newHeight = clamp(rawHeight);
Chris Wren86d00fb2012-08-01 17:03:07 -0400458 boolean isFinished = false;
Selim Cinek1408eb52014-06-02 14:45:38 +0200459 boolean expanded = false;
Chris Wren86d00fb2012-08-01 17:03:07 -0400460 if (rawHeight > mNaturalHeight) {
461 isFinished = true;
Selim Cinek1408eb52014-06-02 14:45:38 +0200462 expanded = true;
Chris Wren86d00fb2012-08-01 17:03:07 -0400463 }
464 if (rawHeight < mSmallSize) {
465 isFinished = true;
Selim Cinek1408eb52014-06-02 14:45:38 +0200466 expanded = false;
Chris Wren86d00fb2012-08-01 17:03:07 -0400467 }
468
Selim Cinek1408eb52014-06-02 14:45:38 +0200469 if (!mHasPopped) {
Jorim Jaggi9ff15102015-06-10 15:58:08 -0700470 if (mEventSource != null) {
471 mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
472 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200473 mHasPopped = true;
Chris Wren86d00fb2012-08-01 17:03:07 -0400474 }
475
Selim Cinek1408eb52014-06-02 14:45:38 +0200476 mScaler.setHeight(newHeight);
477 mLastMotionY = ev.getRawY();
478 if (isFinished) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200479 mCallback.expansionStateChanged(false);
Selim Cinek1408eb52014-06-02 14:45:38 +0200480 } else {
481 mCallback.expansionStateChanged(true);
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400482 }
483 return true;
484 }
Daniel Sandler4377d142012-09-11 15:18:47 -0400485
486 if (mExpanding) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200487
488 // Gestural expansion is running
Daniel Sandler4377d142012-09-11 15:18:47 -0400489 updateExpansion();
Selim Cinek1408eb52014-06-02 14:45:38 +0200490 mLastMotionY = ev.getRawY();
Daniel Sandler4377d142012-09-11 15:18:47 -0400491 return true;
492 }
493
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400494 break;
495 }
Chris Wrencea52072012-10-10 21:02:56 -0400496
497 case MotionEvent.ACTION_POINTER_UP:
498 case MotionEvent.ACTION_POINTER_DOWN:
John Spurlockcd686b52013-06-05 10:13:46 -0400499 if (DEBUG) Log.d(TAG, "pointer change");
Chris Wrencea52072012-10-10 21:02:56 -0400500 mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
501 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
502 break;
503
Daniel Sandler6a858c32012-03-12 14:38:58 -0400504 case MotionEvent.ACTION_UP:
505 case MotionEvent.ACTION_CANCEL:
John Spurlockcd686b52013-06-05 10:13:46 -0400506 if (DEBUG) Log.d(TAG, "up/cancel");
Selim Cinek740c1112016-12-22 16:39:54 +0100507 finishExpanding(!isEnabled() || ev.getActionMasked() == MotionEvent.ACTION_CANCEL,
508 getCurrentVelocity());
Chris Wren80a76272012-04-18 10:52:18 -0400509 clearView();
Daniel Sandler6a858c32012-03-12 14:38:58 -0400510 break;
511 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200512 mLastMotionY = ev.getRawY();
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200513 maybeRecycleVelocityTracker(ev);
Jorim Jaggi787a0af2014-06-04 18:57:14 +0200514 return mResizedView != null;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400515 }
Daniel Sandler4377d142012-09-11 15:18:47 -0400516
Jorim Jaggibe565df2014-04-28 17:51:23 +0200517 /**
518 * @return True if the view is expandable, false otherwise.
519 */
Selim Cinek3acdc8e72017-04-06 17:38:27 -0700520 @VisibleForTesting
521 boolean startExpanding(ExpandableView v, int expandType) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200522 if (!(v instanceof ExpandableNotificationRow)) {
523 return false;
524 }
Chris Wrencea52072012-10-10 21:02:56 -0400525 mExpansionStyle = expandType;
Selim Cinek1408eb52014-06-02 14:45:38 +0200526 if (mExpanding && v == mResizedView) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200527 return true;
Chris Wrencea52072012-10-10 21:02:56 -0400528 }
Daniel Sandler4377d142012-09-11 15:18:47 -0400529 mExpanding = true;
Selim Cinek1408eb52014-06-02 14:45:38 +0200530 mCallback.expansionStateChanged(true);
John Spurlockcd686b52013-06-05 10:13:46 -0400531 if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
Daniel Sandler4377d142012-09-11 15:18:47 -0400532 mCallback.setUserLockedChild(v, true);
Selim Cinek1408eb52014-06-02 14:45:38 +0200533 mScaler.setView(v);
Daniel Sandler4377d142012-09-11 15:18:47 -0400534 mOldHeight = mScaler.getHeight();
Selim Cinek1408eb52014-06-02 14:45:38 +0200535 mCurrentHeight = mOldHeight;
Selim Cinek388df6d2015-10-22 13:25:11 -0700536 boolean canBeExpanded = mCallback.canChildBeExpanded(v);
537 if (canBeExpanded) {
John Spurlockcd686b52013-06-05 10:13:46 -0400538 if (DEBUG) Log.d(TAG, "working on an expandable child");
Selim Cinek388df6d2015-10-22 13:25:11 -0700539 mNaturalHeight = mScaler.getNaturalHeight();
Selim Cinek567e8452016-03-24 10:54:56 -0700540 mSmallSize = v.getCollapsedHeight();
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400541 } else {
John Spurlockcd686b52013-06-05 10:13:46 -0400542 if (DEBUG) Log.d(TAG, "working on a non-expandable child");
Daniel Sandler4377d142012-09-11 15:18:47 -0400543 mNaturalHeight = mOldHeight;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400544 }
John Spurlockcd686b52013-06-05 10:13:46 -0400545 if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
Daniel Sandler4377d142012-09-11 15:18:47 -0400546 " mNaturalHeight: " + mNaturalHeight);
Jorim Jaggibe565df2014-04-28 17:51:23 +0200547 return true;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400548 }
549
Selim Cinek740c1112016-12-22 16:39:54 +0100550 /**
551 * Finish the current expand motion
552 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old
553 * state
554 * @param velocity the velocity this was expanded/ collapsed with
555 */
Selim Cinek3acdc8e72017-04-06 17:38:27 -0700556 @VisibleForTesting
557 void finishExpanding(boolean forceAbort, float velocity) {
Selim Cinek5b1591a2017-07-03 17:05:01 +0200558 finishExpanding(forceAbort, velocity, true /* allowAnimation */);
559 }
560
561 /**
562 * Finish the current expand motion
563 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old
564 * state
565 * @param velocity the velocity this was expanded/ collapsed with
566 */
567 private void finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation) {
Daniel Sandler4377d142012-09-11 15:18:47 -0400568 if (!mExpanding) return;
569
Selim Cinek1408eb52014-06-02 14:45:38 +0200570 if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView);
Chris Wrencea52072012-10-10 21:02:56 -0400571
Chris Wren3ddab0d2012-08-02 16:52:21 -0400572 float currentHeight = mScaler.getHeight();
Daniel Sandler6a858c32012-03-12 14:38:58 -0400573 final boolean wasClosed = (mOldHeight == mSmallSize);
Selim Cinek388df6d2015-10-22 13:25:11 -0700574 boolean nowExpanded;
Selim Cinek740c1112016-12-22 16:39:54 +0100575 if (!forceAbort) {
576 if (wasClosed) {
577 nowExpanded = currentHeight > mOldHeight && velocity >= 0;
578 } else {
579 nowExpanded = currentHeight >= mOldHeight || velocity > 0;
580 }
581 nowExpanded |= mNaturalHeight == mSmallSize;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400582 } else {
Selim Cinek740c1112016-12-22 16:39:54 +0100583 nowExpanded = !wasClosed;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400584 }
Chris Wrenba925e82012-04-20 16:46:43 -0400585 if (mScaleAnimation.isRunning()) {
586 mScaleAnimation.cancel();
587 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200588 mCallback.expansionStateChanged(false);
Selim Cinek740c1112016-12-22 16:39:54 +0100589 int naturalHeight = mScaler.getNaturalHeight();
Selim Cinek388df6d2015-10-22 13:25:11 -0700590 float targetHeight = nowExpanded ? naturalHeight : mSmallSize;
Selim Cinek5b1591a2017-07-03 17:05:01 +0200591 if (targetHeight != currentHeight && mEnabled && allowAnimation) {
Chris Wren3ddab0d2012-08-02 16:52:21 -0400592 mScaleAnimation.setFloatValues(targetHeight);
593 mScaleAnimation.setupStartValues();
Selim Cinek1408eb52014-06-02 14:45:38 +0200594 final View scaledView = mResizedView;
Selim Cinekd191a172016-03-11 18:25:05 -0800595 final boolean expand = nowExpanded;
Jorim Jaggibe565df2014-04-28 17:51:23 +0200596 mScaleAnimation.addListener(new AnimatorListenerAdapter() {
Selim Cinekd191a172016-03-11 18:25:05 -0800597 public boolean mCancelled;
598
Jorim Jaggibe565df2014-04-28 17:51:23 +0200599 @Override
600 public void onAnimationEnd(Animator animation) {
Selim Cinekd191a172016-03-11 18:25:05 -0800601 if (!mCancelled) {
602 mCallback.setUserExpandedChild(scaledView, expand);
Selim Cinek3acdc8e72017-04-06 17:38:27 -0700603 if (!mExpanding) {
604 mScaler.setView(null);
605 }
Mady Mellorb0a82462016-04-30 17:31:02 -0700606 } else {
607 mCallback.setExpansionCancelled(scaledView);
Selim Cinekd191a172016-03-11 18:25:05 -0800608 }
Jorim Jaggibe565df2014-04-28 17:51:23 +0200609 mCallback.setUserLockedChild(scaledView, false);
610 mScaleAnimation.removeListener(this);
611 }
Selim Cinekd191a172016-03-11 18:25:05 -0800612
613 @Override
614 public void onAnimationCancel(Animator animation) {
615 mCancelled = true;
616 }
Jorim Jaggibe565df2014-04-28 17:51:23 +0200617 });
Selim Cinekd191a172016-03-11 18:25:05 -0800618 velocity = nowExpanded == velocity >= 0 ? velocity : 0;
Jorim Jaggi28c0b7142014-07-26 12:36:35 +0200619 mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity);
Chris Wren3ddab0d2012-08-02 16:52:21 -0400620 mScaleAnimation.start();
Selim Cinek343e6e22014-04-11 21:23:30 +0200621 } else {
Selim Cinekc25af402017-01-12 11:57:11 -0800622 if (targetHeight != currentHeight) {
623 mScaler.setHeight(targetHeight);
624 }
Selim Cinekd191a172016-03-11 18:25:05 -0800625 mCallback.setUserExpandedChild(mResizedView, nowExpanded);
Selim Cinek1408eb52014-06-02 14:45:38 +0200626 mCallback.setUserLockedChild(mResizedView, false);
Selim Cinekf306d9b2017-02-21 11:45:13 -0800627 mScaler.setView(null);
Chris Wren3ddab0d2012-08-02 16:52:21 -0400628 }
Daniel Sandler4377d142012-09-11 15:18:47 -0400629
630 mExpanding = false;
631 mExpansionStyle = NONE;
632
John Spurlockcd686b52013-06-05 10:13:46 -0400633 if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed);
634 if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight);
635 if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize);
636 if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight);
Selim Cinek1408eb52014-06-02 14:45:38 +0200637 if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView);
Chris Wren80a76272012-04-18 10:52:18 -0400638 }
639
640 private void clearView() {
Selim Cinek1408eb52014-06-02 14:45:38 +0200641 mResizedView = null;
Daniel Sandler6a858c32012-03-12 14:38:58 -0400642 }
643
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400644 /**
Selim Cinek5b1591a2017-07-03 17:05:01 +0200645 * Use this to abort any pending expansions in progress and force that there will be no
646 * animations.
647 */
648 public void cancelImmediately() {
649 cancel(false /* allowAnimation */);
650 }
651
652 /**
Daniel Sandlerac47ff72012-10-23 10:41:44 -0400653 * Use this to abort any pending expansions in progress.
654 */
655 public void cancel() {
Selim Cinek5b1591a2017-07-03 17:05:01 +0200656 cancel(true /* allowAnimation */);
657 }
658
659 private void cancel(boolean allowAnimation) {
660 finishExpanding(true /* forceAbort */, 0f /* velocity */, allowAnimation);
Daniel Sandlerac47ff72012-10-23 10:41:44 -0400661 clearView();
662
663 // reset the gesture detector
664 mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
665 }
666
667 /**
Selim Cinek1408eb52014-06-02 14:45:38 +0200668 * Change the expansion mode to only observe movements and don't perform any resizing.
669 * This is needed when the expanding is finished and the scroller kicks in,
670 * performing an overscroll motion. We only want to shrink it again when we are not
671 * overscrolled.
672 *
673 * @param onlyMovements Should only movements be observed?
674 */
675 public void onlyObserveMovements(boolean onlyMovements) {
676 mOnlyMovements = onlyMovements;
677 }
Daniel Sandler6a858c32012-03-12 14:38:58 -0400678}
Chris Wrenb4e2c48b2012-06-15 16:51:54 -0400679