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