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