1 /*
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 
18 package com.android.systemui;
19 
20 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_EXPAND;
21 
22 import android.content.Context;
23 import android.util.FloatProperty;
24 import android.util.Log;
25 import android.view.Gravity;
26 import android.view.HapticFeedbackConstants;
27 import android.view.MotionEvent;
28 import android.view.ScaleGestureDetector;
29 import android.view.ScaleGestureDetector.OnScaleGestureListener;
30 import android.view.VelocityTracker;
31 import android.view.View;
32 import android.view.ViewConfiguration;
33 
34 import androidx.annotation.NonNull;
35 import androidx.core.animation.Animator;
36 import androidx.core.animation.AnimatorListenerAdapter;
37 import androidx.core.animation.ObjectAnimator;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.internal.jank.InteractionJankMonitor;
41 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
42 import com.android.systemui.statusbar.notification.row.ExpandableView;
43 import com.android.systemui.statusbar.policy.ScrollAdapter;
44 import com.android.wm.shell.animation.FlingAnimationUtils;
45 
46 public class ExpandHelper implements Gefingerpoken {
47     public interface Callback {
getChildAtRawPosition(float x, float y)48         ExpandableView getChildAtRawPosition(float x, float y);
getChildAtPosition(float x, float y)49         ExpandableView getChildAtPosition(float x, float y);
canChildBeExpanded(View v)50         boolean canChildBeExpanded(View v);
setUserExpandedChild(View v, boolean userExpanded)51         void setUserExpandedChild(View v, boolean userExpanded);
setUserLockedChild(View v, boolean userLocked)52         void setUserLockedChild(View v, boolean userLocked);
expansionStateChanged(boolean isExpanding)53         void expansionStateChanged(boolean isExpanding);
getMaxExpandHeight(ExpandableView view)54         int getMaxExpandHeight(ExpandableView view);
setExpansionCancelled(View view)55         void setExpansionCancelled(View view);
56     }
57 
58     private static final String TAG = "ExpandHelper";
59     protected static final boolean DEBUG = false;
60     protected static final boolean DEBUG_SCALE = false;
61     private static final float EXPAND_DURATION = 0.3f;
62 
63     // Set to false to disable focus-based gestures (spread-finger vertical pull).
64     private static final boolean USE_DRAG = true;
65     // Set to false to disable scale-based gestures (both horizontal and vertical).
66     private static final boolean USE_SPAN = true;
67     // Both gestures types may be active at the same time.
68     // At least one gesture type should be active.
69     // A variant of the screwdriver gesture will emerge from either gesture type.
70 
71     // amount of overstretch for maximum brightness expressed in U
72     // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
73     private static final float STRETCH_INTERVAL = 2f;
74 
75     private static final FloatProperty<ViewScaler> VIEW_SCALER_HEIGHT_PROPERTY =
76             new FloatProperty<ViewScaler>("ViewScalerHeight") {
77         @Override
78         public void setValue(ViewScaler object, float value) {
79             object.setHeight(value);
80         }
81 
82         @Override
83         public Float get(ViewScaler object) {
84             return object.getHeight();
85         }
86     };
87 
88     @SuppressWarnings("unused")
89     private Context mContext;
90 
91     private boolean mExpanding;
92     private static final int NONE    = 0;
93     private static final int BLINDS  = 1<<0;
94     private static final int PULL    = 1<<1;
95     private static final int STRETCH = 1<<2;
96     private int mExpansionStyle = NONE;
97     private boolean mWatchingForPull;
98     private boolean mHasPopped;
99     private View mEventSource;
100     private float mOldHeight;
101     private float mNaturalHeight;
102     private float mInitialTouchFocusY;
103     private float mInitialTouchX;
104     private float mInitialTouchY;
105     private float mInitialTouchSpan;
106     private float mLastFocusY;
107     private float mLastSpanY;
108     private final int mTouchSlop;
109     private final float mSlopMultiplier;
110     private float mLastMotionY;
111     private float mPullGestureMinXSpan;
112     private Callback mCallback;
113     private ScaleGestureDetector mSGD;
114     private ViewScaler mScaler;
115     private ObjectAnimator mScaleAnimation;
116     private boolean mEnabled = true;
117     private ExpandableView mResizedView;
118     private float mCurrentHeight;
119 
120     private int mSmallSize;
121     private int mLargeSize;
122     private float mMaximumStretch;
123     private boolean mOnlyMovements;
124 
125     private int mGravity;
126 
127     private ScrollAdapter mScrollAdapter;
128     private FlingAnimationUtils mFlingAnimationUtils;
129     private VelocityTracker mVelocityTracker;
130 
131     private OnScaleGestureListener mScaleGestureListener
132             = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
133         @Override
134         public boolean onScaleBegin(ScaleGestureDetector detector) {
135             if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()");
136 
137             if (!mOnlyMovements) {
138                 startExpanding(mResizedView, STRETCH);
139             }
140             return mExpanding;
141         }
142 
143         @Override
144         public boolean onScale(ScaleGestureDetector detector) {
145             if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView);
146             return true;
147         }
148 
149         @Override
150         public void onScaleEnd(ScaleGestureDetector detector) {
151         }
152     };
153 
154     @VisibleForTesting
getScaleAnimation()155     ObjectAnimator getScaleAnimation() {
156         return mScaleAnimation;
157     }
158 
159     private class ViewScaler {
160         ExpandableView mView;
161 
ViewScaler()162         public ViewScaler() {}
setView(ExpandableView v)163         public void setView(ExpandableView v) {
164             mView = v;
165         }
166 
setHeight(float h)167         public void setHeight(float h) {
168             if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
169             mView.setActualHeight((int) h);
170             mCurrentHeight = h;
171         }
getHeight()172         public float getHeight() {
173             return mView.getActualHeight();
174         }
getNaturalHeight()175         public int getNaturalHeight() {
176             return mCallback.getMaxExpandHeight(mView);
177         }
178     }
179 
180     /**
181      * Handle expansion gestures to expand and contract children of the callback.
182      *
183      * @param context application context
184      * @param callback the container that holds the items to be manipulated
185      * @param small the smallest allowable size for the manipulated items.
186      * @param large the largest allowable size for the manipulated items.
187      */
ExpandHelper(Context context, Callback callback, int small, int large)188     public ExpandHelper(Context context, Callback callback, int small, int large) {
189         mSmallSize = small;
190         mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
191         mLargeSize = large;
192         mContext = context;
193         mCallback = callback;
194         mScaler = new ViewScaler();
195         mGravity = Gravity.TOP;
196         mScaleAnimation = ObjectAnimator.ofFloat(mScaler, VIEW_SCALER_HEIGHT_PROPERTY, 0f);
197         mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
198 
199         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
200         mTouchSlop = configuration.getScaledTouchSlop();
201         mSlopMultiplier = configuration.getAmbiguousGestureMultiplier();
202 
203         mSGD = new ScaleGestureDetector(context, mScaleGestureListener);
204         mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(),
205                 EXPAND_DURATION);
206     }
207 
208     @VisibleForTesting
updateExpansion()209     void updateExpansion() {
210         if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()");
211         // are we scaling or dragging?
212         float span = mSGD.getCurrentSpan() - mInitialTouchSpan;
213         span *= USE_SPAN ? 1f : 0f;
214         float drag = mSGD.getFocusY() - mInitialTouchFocusY;
215         drag *= USE_DRAG ? 1f : 0f;
216         drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
217         float pull = Math.abs(drag) + Math.abs(span) + 1f;
218         float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
219         float target = hand + mOldHeight;
220         float newHeight = clamp(target);
221         mScaler.setHeight(newHeight);
222         mLastFocusY = mSGD.getFocusY();
223         mLastSpanY = mSGD.getCurrentSpan();
224     }
225 
clamp(float target)226     private float clamp(float target) {
227         float out = target;
228         out = out < mSmallSize ? mSmallSize : out;
229         out = out > mNaturalHeight ? mNaturalHeight : out;
230         return out;
231     }
232 
findView(float x, float y)233     private ExpandableView findView(float x, float y) {
234         ExpandableView v;
235         if (mEventSource != null) {
236             int[] location = new int[2];
237             mEventSource.getLocationOnScreen(location);
238             x += location[0];
239             y += location[1];
240             v = mCallback.getChildAtRawPosition(x, y);
241         } else {
242             v = mCallback.getChildAtPosition(x, y);
243         }
244         return v;
245     }
246 
isInside(View v, float x, float y)247     private boolean isInside(View v, float x, float y) {
248         if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")");
249 
250         if (v == null) {
251             if (DEBUG) Log.d(TAG, "isinside null subject");
252             return false;
253         }
254         if (mEventSource != null) {
255             int[] location = new int[2];
256             mEventSource.getLocationOnScreen(location);
257             x += location[0];
258             y += location[1];
259             if (DEBUG) Log.d(TAG, "  to global (" + x + ", " + y + ")");
260         }
261         int[] location = new int[2];
262         v.getLocationOnScreen(location);
263         x -= location[0];
264         y -= location[1];
265         if (DEBUG) Log.d(TAG, "  to local (" + x + ", " + y + ")");
266         if (DEBUG) Log.d(TAG, "  inside (" + v.getWidth() + ", " + v.getHeight() + ")");
267         boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
268         return inside;
269     }
270 
setEventSource(View eventSource)271     public void setEventSource(View eventSource) {
272         mEventSource = eventSource;
273     }
274 
setGravity(int gravity)275     public void setGravity(int gravity) {
276         mGravity = gravity;
277     }
278 
setScrollAdapter(ScrollAdapter adapter)279     public void setScrollAdapter(ScrollAdapter adapter) {
280         mScrollAdapter = adapter;
281     }
282 
getTouchSlop(MotionEvent event)283     private float getTouchSlop(MotionEvent event) {
284         // Adjust the touch slop if another gesture may be being performed.
285         return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
286                 ? mTouchSlop * mSlopMultiplier
287                 : mTouchSlop;
288     }
289 
290     @Override
onInterceptTouchEvent(MotionEvent ev)291     public boolean onInterceptTouchEvent(MotionEvent ev) {
292         if (!isEnabled()) {
293             return false;
294         }
295         trackVelocity(ev);
296         final int action = ev.getAction();
297         if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
298                          " expanding=" + mExpanding +
299                          (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
300                          (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
301                          (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
302         // check for a spread-finger vertical pull gesture
303         mSGD.onTouchEvent(ev);
304         final int x = (int) mSGD.getFocusX();
305         final int y = (int) mSGD.getFocusY();
306 
307         mInitialTouchFocusY = y;
308         mInitialTouchSpan = mSGD.getCurrentSpan();
309         mLastFocusY = mInitialTouchFocusY;
310         mLastSpanY = mInitialTouchSpan;
311         if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan);
312 
313         if (mExpanding) {
314             mLastMotionY = ev.getRawY();
315             maybeRecycleVelocityTracker(ev);
316             return true;
317         } else {
318             if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
319                 // we've begun Venetian blinds style expansion
320                 return true;
321             }
322             switch (action & MotionEvent.ACTION_MASK) {
323             case MotionEvent.ACTION_MOVE: {
324                 final float xspan = mSGD.getCurrentSpanX();
325                 if (xspan > mPullGestureMinXSpan &&
326                         xspan > mSGD.getCurrentSpanY() && !mExpanding) {
327                     // detect a vertical pulling gesture with fingers somewhat separated
328                     if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
329                     startExpanding(mResizedView, PULL);
330                     mWatchingForPull = false;
331                 }
332                 if (mWatchingForPull) {
333                     final float yDiff = ev.getRawY() - mInitialTouchY;
334                     final float xDiff = ev.getRawX() - mInitialTouchX;
335                     if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) {
336                         if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
337                         mWatchingForPull = false;
338                         if (mResizedView != null && !isFullyExpanded(mResizedView)) {
339                             if (startExpanding(mResizedView, BLINDS)) {
340                                 mLastMotionY = ev.getRawY();
341                                 mInitialTouchY = ev.getRawY();
342                                 mHasPopped = false;
343                             }
344                         }
345                     }
346                 }
347                 break;
348             }
349 
350             case MotionEvent.ACTION_DOWN:
351                 mWatchingForPull = mScrollAdapter != null &&
352                         isInside(mScrollAdapter.getHostView(), x, y)
353                         && mScrollAdapter.isScrolledToTop();
354                 mResizedView = findView(x, y);
355                 if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) {
356                     mResizedView = null;
357                     mWatchingForPull = false;
358                 }
359                 mInitialTouchY = ev.getRawY();
360                 mInitialTouchX = ev.getRawX();
361                 break;
362 
363             case MotionEvent.ACTION_CANCEL:
364             case MotionEvent.ACTION_UP:
365                 if (DEBUG) Log.d(TAG, "up/cancel");
366                 finishExpanding(ev.getActionMasked() == MotionEvent.ACTION_CANCEL /* forceAbort */,
367                         getCurrentVelocity());
368                 clearView();
369                 break;
370             }
371             mLastMotionY = ev.getRawY();
372             maybeRecycleVelocityTracker(ev);
373             return mExpanding;
374         }
375     }
376 
trackVelocity(MotionEvent event)377     private void trackVelocity(MotionEvent event) {
378         int action = event.getActionMasked();
379         switch(action) {
380             case MotionEvent.ACTION_DOWN:
381                 if (mVelocityTracker == null) {
382                     mVelocityTracker = VelocityTracker.obtain();
383                 } else {
384                     mVelocityTracker.clear();
385                 }
386                 mVelocityTracker.addMovement(event);
387                 break;
388             case MotionEvent.ACTION_MOVE:
389                 if (mVelocityTracker == null) {
390                     mVelocityTracker = VelocityTracker.obtain();
391                 }
392                 mVelocityTracker.addMovement(event);
393                 break;
394             default:
395                 break;
396         }
397     }
398 
maybeRecycleVelocityTracker(MotionEvent event)399     private void maybeRecycleVelocityTracker(MotionEvent event) {
400         if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL
401                 || event.getActionMasked() == MotionEvent.ACTION_UP)) {
402             mVelocityTracker.recycle();
403             mVelocityTracker = null;
404         }
405     }
406 
getCurrentVelocity()407     private float getCurrentVelocity() {
408         if (mVelocityTracker != null) {
409             mVelocityTracker.computeCurrentVelocity(1000);
410             return mVelocityTracker.getYVelocity();
411         } else {
412             return 0f;
413         }
414     }
415 
setEnabled(boolean enable)416     public void setEnabled(boolean enable) {
417         mEnabled = enable;
418     }
419 
isEnabled()420     private boolean isEnabled() {
421         return mEnabled;
422     }
423 
isFullyExpanded(ExpandableView underFocus)424     private boolean isFullyExpanded(ExpandableView underFocus) {
425         return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight()
426                 && (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded());
427     }
428 
429     @Override
onTouchEvent(MotionEvent ev)430     public boolean onTouchEvent(MotionEvent ev) {
431         if (!isEnabled() && !mExpanding) {
432             // In case we're expanding we still want to finish the current motion.
433             return false;
434         }
435         trackVelocity(ev);
436         final int action = ev.getActionMasked();
437         if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
438                 " expanding=" + mExpanding +
439                 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
440                 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
441                 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
442 
443         mSGD.onTouchEvent(ev);
444         final int x = (int) mSGD.getFocusX();
445         final int y = (int) mSGD.getFocusY();
446 
447         if (mOnlyMovements) {
448             mLastMotionY = ev.getRawY();
449             return false;
450         }
451         switch (action) {
452             case MotionEvent.ACTION_DOWN:
453                 mWatchingForPull = mScrollAdapter != null &&
454                         isInside(mScrollAdapter.getHostView(), x, y);
455                 mResizedView = findView(x, y);
456                 mInitialTouchX = ev.getRawX();
457                 mInitialTouchY = ev.getRawY();
458                 break;
459             case MotionEvent.ACTION_MOVE: {
460                 if (mWatchingForPull) {
461                     final float yDiff = ev.getRawY() - mInitialTouchY;
462                     final float xDiff = ev.getRawX() - mInitialTouchX;
463                     if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) {
464                         if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
465                         mWatchingForPull = false;
466                         if (mResizedView != null && !isFullyExpanded(mResizedView)) {
467                             if (startExpanding(mResizedView, BLINDS)) {
468                                 mInitialTouchY = ev.getRawY();
469                                 mLastMotionY = ev.getRawY();
470                                 mHasPopped = false;
471                             }
472                         }
473                     }
474                 }
475                 if (mExpanding && 0 != (mExpansionStyle & BLINDS)) {
476                     final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight;
477                     final float newHeight = clamp(rawHeight);
478                     boolean isFinished = false;
479                     boolean expanded = false;
480                     if (rawHeight > mNaturalHeight) {
481                         isFinished = true;
482                         expanded = true;
483                     }
484                     if (rawHeight < mSmallSize) {
485                         isFinished = true;
486                         expanded = false;
487                     }
488 
489                     if (!mHasPopped) {
490                         if (mEventSource != null) {
491                             mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
492                         }
493                         mHasPopped = true;
494                     }
495 
496                     mScaler.setHeight(newHeight);
497                     mLastMotionY = ev.getRawY();
498                     if (isFinished) {
499                         mCallback.expansionStateChanged(false);
500                     } else {
501                         mCallback.expansionStateChanged(true);
502                     }
503                     return true;
504                 }
505 
506                 if (mExpanding) {
507 
508                     // Gestural expansion is running
509                     updateExpansion();
510                     mLastMotionY = ev.getRawY();
511                     return true;
512                 }
513 
514                 break;
515             }
516 
517             case MotionEvent.ACTION_POINTER_UP:
518             case MotionEvent.ACTION_POINTER_DOWN:
519                 if (DEBUG) Log.d(TAG, "pointer change");
520                 mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
521                 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
522                 break;
523 
524             case MotionEvent.ACTION_UP:
525             case MotionEvent.ACTION_CANCEL:
526                 if (DEBUG) Log.d(TAG, "up/cancel");
527                 finishExpanding(!isEnabled() || ev.getActionMasked() == MotionEvent.ACTION_CANCEL,
528                         getCurrentVelocity());
529                 clearView();
530                 break;
531         }
532         mLastMotionY = ev.getRawY();
533         maybeRecycleVelocityTracker(ev);
534         return mResizedView != null;
535     }
536 
537     /**
538      * @return True if the view is expandable, false otherwise.
539      */
540     @VisibleForTesting
startExpanding(ExpandableView v, int expandType)541     boolean startExpanding(ExpandableView v, int expandType) {
542         if (!(v instanceof ExpandableNotificationRow)) {
543             return false;
544         }
545         mExpansionStyle = expandType;
546         if (mExpanding && v == mResizedView) {
547             return true;
548         }
549         mExpanding = true;
550         mCallback.expansionStateChanged(true);
551         if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
552         mCallback.setUserLockedChild(v, true);
553         mScaler.setView(v);
554         mOldHeight = mScaler.getHeight();
555         mCurrentHeight = mOldHeight;
556         boolean canBeExpanded = mCallback.canChildBeExpanded(v);
557         if (canBeExpanded) {
558             if (DEBUG) Log.d(TAG, "working on an expandable child");
559             mNaturalHeight = mScaler.getNaturalHeight();
560             mSmallSize = v.getCollapsedHeight();
561         } else {
562             if (DEBUG) Log.d(TAG, "working on a non-expandable child");
563             mNaturalHeight = mOldHeight;
564         }
565         if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
566                     " mNaturalHeight: " + mNaturalHeight);
567         InteractionJankMonitor.getInstance().begin(v, CUJ_NOTIFICATION_SHADE_ROW_EXPAND);
568         return true;
569     }
570 
571     /**
572      * Finish the current expand motion
573      * @param forceAbort whether the expansion should be forcefully aborted and returned to the old
574      *                   state
575      * @param velocity the velocity this was expanded/ collapsed with
576      */
577     @VisibleForTesting
finishExpanding(boolean forceAbort, float velocity)578     void finishExpanding(boolean forceAbort, float velocity) {
579         finishExpanding(forceAbort, velocity, true /* allowAnimation */);
580     }
581 
582     /**
583      * Finish the current expand motion
584      * @param forceAbort whether the expansion should be forcefully aborted and returned to the old
585      *                   state
586      * @param velocity the velocity this was expanded/ collapsed with
587      */
finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation)588     private void finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation) {
589         if (!mExpanding) return;
590 
591         if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView);
592 
593         float currentHeight = mScaler.getHeight();
594         final boolean wasClosed = (mOldHeight == mSmallSize);
595         boolean nowExpanded;
596         if (!forceAbort) {
597             if (wasClosed) {
598                 nowExpanded = currentHeight > mOldHeight && velocity >= 0;
599             } else {
600                 nowExpanded = currentHeight >= mOldHeight || velocity > 0;
601             }
602             nowExpanded |= mNaturalHeight == mSmallSize;
603         } else {
604             nowExpanded = !wasClosed;
605         }
606         if (mScaleAnimation.isRunning()) {
607             mScaleAnimation.cancel();
608         }
609         mCallback.expansionStateChanged(false);
610         int naturalHeight = mScaler.getNaturalHeight();
611         float targetHeight = nowExpanded ? naturalHeight : mSmallSize;
612         if (targetHeight != currentHeight && mEnabled && allowAnimation) {
613             mScaleAnimation.setFloatValues(targetHeight);
614             mScaleAnimation.setupStartValues();
615             final View scaledView = mResizedView;
616             final boolean expand = nowExpanded;
617             mScaleAnimation.addListener(new AnimatorListenerAdapter() {
618                 public boolean mCancelled;
619 
620                 @Override
621                 public void onAnimationEnd(@NonNull Animator animation) {
622                     if (!mCancelled) {
623                         mCallback.setUserExpandedChild(scaledView, expand);
624                         if (!mExpanding) {
625                             mScaler.setView(null);
626                         }
627                     } else {
628                         mCallback.setExpansionCancelled(scaledView);
629                     }
630                     mCallback.setUserLockedChild(scaledView, false);
631                     mScaleAnimation.removeListener(this);
632                     if (wasClosed) {
633                         InteractionJankMonitor.getInstance().end(CUJ_NOTIFICATION_SHADE_ROW_EXPAND);
634                     }
635                 }
636 
637                 @Override
638                 public void onAnimationCancel(@NonNull Animator animation) {
639                     mCancelled = true;
640                 }
641             });
642             velocity = nowExpanded == velocity >= 0 ? velocity : 0;
643             mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity);
644             mScaleAnimation.start();
645         } else {
646             if (targetHeight != currentHeight) {
647                 mScaler.setHeight(targetHeight);
648             }
649             mCallback.setUserExpandedChild(mResizedView, nowExpanded);
650             mCallback.setUserLockedChild(mResizedView, false);
651             mScaler.setView(null);
652             if (wasClosed) {
653                 InteractionJankMonitor.getInstance().end(CUJ_NOTIFICATION_SHADE_ROW_EXPAND);
654             }
655         }
656 
657         mExpanding = false;
658         mExpansionStyle = NONE;
659 
660         if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed);
661         if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight);
662         if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize);
663         if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight);
664         if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView);
665     }
666 
clearView()667     private void clearView() {
668         mResizedView = null;
669     }
670 
671     /**
672      * Use this to abort any pending expansions in progress and force that there will be no
673      * animations.
674      */
cancelImmediately()675     public void cancelImmediately() {
676         cancel(false /* allowAnimation */);
677     }
678 
679     /**
680      * Use this to abort any pending expansions in progress.
681      */
cancel()682     public void cancel() {
683         cancel(true /* allowAnimation */);
684     }
685 
cancel(boolean allowAnimation)686     private void cancel(boolean allowAnimation) {
687         finishExpanding(true /* forceAbort */, 0f /* velocity */, allowAnimation);
688         clearView();
689 
690         // reset the gesture detector
691         mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
692     }
693 
694     /**
695      * Change the expansion mode to only observe movements and don't perform any resizing.
696      * This is needed when the expanding is finished and the scroller kicks in,
697      * performing an overscroll motion. We only want to shrink it again when we are not
698      * overscrolled.
699      *
700      * @param onlyMovements Should only movements be observed?
701      */
onlyObserveMovements(boolean onlyMovements)702     public void onlyObserveMovements(boolean onlyMovements) {
703         mOnlyMovements = onlyMovements;
704     }
705 }
706 
707