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