Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2014 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 package com.android.systemui.statusbar;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.ValueAnimator;
     23 import android.content.Context;
     24 import android.view.MotionEvent;
     25 import android.view.View;
     26 import android.view.ViewConfiguration;
     27 
     28 import com.android.systemui.ExpandHelper;
     29 import com.android.systemui.Gefingerpoken;
     30 import com.android.systemui.Interpolators;
     31 import com.android.systemui.R;
     32 import com.android.systemui.classifier.FalsingManager;
     33 
     34 /**
     35  * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
     36  * the notification where the drag started.
     37  */
     38 public class DragDownHelper implements Gefingerpoken {
     39 
     40     private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
     41     private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
     42 
     43     private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
     44 
     45     private int mMinDragDistance;
     46     private ExpandHelper.Callback mCallback;
     47     private float mInitialTouchX;
     48     private float mInitialTouchY;
     49     private boolean mDraggingDown;
     50     private float mTouchSlop;
     51     private DragDownCallback mDragDownCallback;
     52     private View mHost;
     53     private final int[] mTemp2 = new int[2];
     54     private boolean mDraggedFarEnough;
     55     private ExpandableView mStartingChild;
     56     private float mLastHeight;
     57     private FalsingManager mFalsingManager;
     58 
     59     public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
     60             DragDownCallback dragDownCallback) {
     61         mMinDragDistance = context.getResources().getDimensionPixelSize(
     62                 R.dimen.keyguard_drag_down_min_distance);
     63         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
     64         mCallback = callback;
     65         mDragDownCallback = dragDownCallback;
     66         mHost = host;
     67         mFalsingManager = FalsingManager.getInstance(context);
     68     }
     69 
     70     @Override
     71     public boolean onInterceptTouchEvent(MotionEvent event) {
     72         final float x = event.getX();
     73         final float y = event.getY();
     74 
     75         switch (event.getActionMasked()) {
     76             case MotionEvent.ACTION_DOWN:
     77                 mDraggedFarEnough = false;
     78                 mDraggingDown = false;
     79                 mStartingChild = null;
     80                 mInitialTouchY = y;
     81                 mInitialTouchX = x;
     82                 break;
     83 
     84             case MotionEvent.ACTION_MOVE:
     85                 final float h = y - mInitialTouchY;
     86                 if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
     87                     mFalsingManager.onNotificatonStartDraggingDown();
     88                     mDraggingDown = true;
     89                     captureStartingChild(mInitialTouchX, mInitialTouchY);
     90                     mInitialTouchY = y;
     91                     mInitialTouchX = x;
     92                     mDragDownCallback.onTouchSlopExceeded();
     93                     return true;
     94                 }
     95                 break;
     96         }
     97         return false;
     98     }
     99 
    100     @Override
    101     public boolean onTouchEvent(MotionEvent event) {
    102         if (!mDraggingDown) {
    103             return false;
    104         }
    105         final float x = event.getX();
    106         final float y = event.getY();
    107 
    108         switch (event.getActionMasked()) {
    109             case MotionEvent.ACTION_MOVE:
    110                 mLastHeight = y - mInitialTouchY;
    111                 captureStartingChild(mInitialTouchX, mInitialTouchY);
    112                 if (mStartingChild != null) {
    113                     handleExpansion(mLastHeight, mStartingChild);
    114                 } else {
    115                     mDragDownCallback.setEmptyDragAmount(mLastHeight);
    116                 }
    117                 if (mLastHeight > mMinDragDistance) {
    118                     if (!mDraggedFarEnough) {
    119                         mDraggedFarEnough = true;
    120                         mDragDownCallback.onCrossedThreshold(true);
    121                     }
    122                 } else {
    123                     if (mDraggedFarEnough) {
    124                         mDraggedFarEnough = false;
    125                         mDragDownCallback.onCrossedThreshold(false);
    126                     }
    127                 }
    128                 return true;
    129             case MotionEvent.ACTION_UP:
    130                 if (!isFalseTouch() && mDragDownCallback.onDraggedDown(mStartingChild,
    131                         (int) (y - mInitialTouchY))) {
    132                     if (mStartingChild == null) {
    133                         mDragDownCallback.setEmptyDragAmount(0f);
    134                     } else {
    135                         mCallback.setUserLockedChild(mStartingChild, false);
    136                         mStartingChild = null;
    137                     }
    138                     mDraggingDown = false;
    139                 } else {
    140                     stopDragging();
    141                     return false;
    142                 }
    143                 break;
    144             case MotionEvent.ACTION_CANCEL:
    145                 stopDragging();
    146                 return false;
    147         }
    148         return false;
    149     }
    150 
    151     private boolean isFalseTouch() {
    152         return mFalsingManager.isFalseTouch() || !mDraggedFarEnough;
    153     }
    154 
    155     private void captureStartingChild(float x, float y) {
    156         if (mStartingChild == null) {
    157             mStartingChild = findView(x, y);
    158             if (mStartingChild != null) {
    159                 mCallback.setUserLockedChild(mStartingChild, true);
    160             }
    161         }
    162     }
    163 
    164     private void handleExpansion(float heightDelta, ExpandableView child) {
    165         if (heightDelta < 0) {
    166             heightDelta = 0;
    167         }
    168         boolean expandable = child.isContentExpandable();
    169         float rubberbandFactor = expandable
    170                 ? RUBBERBAND_FACTOR_EXPANDABLE
    171                 : RUBBERBAND_FACTOR_STATIC;
    172         float rubberband = heightDelta * rubberbandFactor;
    173         if (expandable
    174                 && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) {
    175             float overshoot =
    176                     (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight();
    177             overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
    178             rubberband -= overshoot;
    179         }
    180         child.setActualHeight((int) (child.getCollapsedHeight() + rubberband));
    181     }
    182 
    183     private void cancelExpansion(final ExpandableView child) {
    184         if (child.getActualHeight() == child.getCollapsedHeight()) {
    185             mCallback.setUserLockedChild(child, false);
    186             return;
    187         }
    188         ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
    189                 child.getActualHeight(), child.getCollapsedHeight());
    190         anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    191         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
    192         anim.addListener(new AnimatorListenerAdapter() {
    193             @Override
    194             public void onAnimationEnd(Animator animation) {
    195                 mCallback.setUserLockedChild(child, false);
    196             }
    197         });
    198         anim.start();
    199     }
    200 
    201     private void cancelExpansion() {
    202         ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
    203         anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    204         anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
    205         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    206             @Override
    207             public void onAnimationUpdate(ValueAnimator animation) {
    208                 mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
    209             }
    210         });
    211         anim.start();
    212     }
    213 
    214     private void stopDragging() {
    215         mFalsingManager.onNotificatonStopDraggingDown();
    216         if (mStartingChild != null) {
    217             cancelExpansion(mStartingChild);
    218             mStartingChild = null;
    219         } else {
    220             cancelExpansion();
    221         }
    222         mDraggingDown = false;
    223         mDragDownCallback.onDragDownReset();
    224     }
    225 
    226     private ExpandableView findView(float x, float y) {
    227         mHost.getLocationOnScreen(mTemp2);
    228         x += mTemp2[0];
    229         y += mTemp2[1];
    230         return mCallback.getChildAtRawPosition(x, y);
    231     }
    232 
    233     public boolean isDraggingDown() {
    234         return mDraggingDown;
    235     }
    236 
    237     public interface DragDownCallback {
    238 
    239         /**
    240          * @return true if the interaction is accepted, false if it should be cancelled
    241          */
    242         boolean onDraggedDown(View startingChild, int dragLengthY);
    243         void onDragDownReset();
    244 
    245         /**
    246          * The user has dragged either above or below the threshold
    247          * @param above whether he dragged above it
    248          */
    249         void onCrossedThreshold(boolean above);
    250         void onTouchSlopExceeded();
    251         void setEmptyDragAmount(float amount);
    252     }
    253 }
    254