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