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