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