DragDownHelper.java revision 177fd4324ab89dc138b3d4c7472623ae9dc8c9d8
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
17package com.android.systemui.statusbar;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.view.MotionEvent;
25import android.view.View;
26import android.view.ViewConfiguration;
27import android.view.animation.AnimationUtils;
28import android.view.animation.Interpolator;
29import com.android.systemui.ExpandHelper;
30import com.android.systemui.Gefingerpoken;
31import com.android.systemui.R;
32import 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 */
38public 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 Interpolator mInterpolator;
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        mInterpolator =
65                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
66        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
67        mCallback = callback;
68        mDragDownCallback = dragDownCallback;
69        mHost = host;
70        mFalsingManager = FalsingManager.getInstance(context);
71    }
72
73    @Override
74    public boolean onInterceptTouchEvent(MotionEvent event) {
75        final float x = event.getX();
76        final float y = event.getY();
77
78        switch (event.getActionMasked()) {
79            case MotionEvent.ACTION_DOWN:
80                mDraggedFarEnough = false;
81                mDraggingDown = false;
82                mStartingChild = null;
83                mInitialTouchY = y;
84                mInitialTouchX = x;
85                break;
86
87            case MotionEvent.ACTION_MOVE:
88                final float h = y - mInitialTouchY;
89                if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
90                    mFalsingManager.onNotificatonStartDraggingDown();
91                    mDraggingDown = true;
92                    captureStartingChild(mInitialTouchX, mInitialTouchY);
93                    mInitialTouchY = y;
94                    mInitialTouchX = x;
95                    mDragDownCallback.onTouchSlopExceeded();
96                    return true;
97                }
98                break;
99        }
100        return false;
101    }
102
103    @Override
104    public boolean onTouchEvent(MotionEvent event) {
105        if (!mDraggingDown) {
106            return false;
107        }
108        final float x = event.getX();
109        final float y = event.getY();
110
111        switch (event.getActionMasked()) {
112            case MotionEvent.ACTION_MOVE:
113                mLastHeight = y - mInitialTouchY;
114                captureStartingChild(mInitialTouchX, mInitialTouchY);
115                if (mStartingChild != null) {
116                    handleExpansion(mLastHeight, mStartingChild);
117                } else {
118                    mDragDownCallback.setEmptyDragAmount(mLastHeight);
119                }
120                if (mLastHeight > mMinDragDistance) {
121                    if (!mDraggedFarEnough) {
122                        mDraggedFarEnough = true;
123                        mDragDownCallback.onCrossedThreshold(true);
124                    }
125                } else {
126                    if (mDraggedFarEnough) {
127                        mDraggedFarEnough = false;
128                        mDragDownCallback.onCrossedThreshold(false);
129                    }
130                }
131                return true;
132            case MotionEvent.ACTION_UP:
133                if (!isFalseTouch() && mDragDownCallback.onDraggedDown(mStartingChild,
134                        (int) (y - mInitialTouchY))) {
135                    if (mStartingChild == null) {
136                        mDragDownCallback.setEmptyDragAmount(0f);
137                    } else {
138                        mCallback.setUserLockedChild(mStartingChild, false);
139                    }
140                    mDraggingDown = false;
141                } else {
142                    stopDragging();
143                    return false;
144                }
145                break;
146            case MotionEvent.ACTION_CANCEL:
147                stopDragging();
148                return false;
149        }
150        return false;
151    }
152
153    private boolean isFalseTouch() {
154        return mFalsingManager.isFalseTouch() || !mDraggedFarEnough;
155    }
156
157    private void captureStartingChild(float x, float y) {
158        if (mStartingChild == null) {
159            mStartingChild = findView(x, y);
160            if (mStartingChild != null) {
161                mCallback.setUserLockedChild(mStartingChild, true);
162            }
163        }
164    }
165
166    private void handleExpansion(float heightDelta, ExpandableView child) {
167        if (heightDelta < 0) {
168            heightDelta = 0;
169        }
170        boolean expandable = child.isContentExpandable();
171        float rubberbandFactor = expandable
172                ? RUBBERBAND_FACTOR_EXPANDABLE
173                : RUBBERBAND_FACTOR_STATIC;
174        float rubberband = heightDelta * rubberbandFactor;
175        if (expandable && (rubberband + child.getMinHeight()) > child.getMaxContentHeight()) {
176            float overshoot = (rubberband + child.getMinHeight()) - child.getMaxContentHeight();
177            overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
178            rubberband -= overshoot;
179        }
180        child.setActualHeight((int) (child.getMinHeight() + rubberband));
181    }
182
183    private void cancelExpansion(final ExpandableView child) {
184        if (child.getActualHeight() == child.getMinHeight()) {
185            mCallback.setUserLockedChild(child, false);
186            return;
187        }
188        ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
189                child.getActualHeight(), child.getMinHeight());
190        anim.setInterpolator(mInterpolator);
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(mInterpolator);
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        } else {
219            cancelExpansion();
220        }
221        mDraggingDown = false;
222        mDragDownCallback.onDragDownReset();
223    }
224
225    private ExpandableView findView(float x, float y) {
226        mHost.getLocationOnScreen(mTemp2);
227        x += mTemp2[0];
228        y += mTemp2[1];
229        return mCallback.getChildAtRawPosition(x, y);
230    }
231
232    public interface DragDownCallback {
233
234        /**
235         * @return true if the interaction is accepted, false if it should be cancelled
236         */
237        boolean onDraggedDown(View startingChild, int dragLengthY);
238        void onDragDownReset();
239
240        /**
241         * The user has dragged either above or below the threshold
242         * @param above whether he dragged above it
243         */
244        void onCrossedThreshold(boolean above);
245        void onTouchSlopExceeded();
246        void setEmptyDragAmount(float amount);
247    }
248}
249