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