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;
27
28import com.android.systemui.ExpandHelper;
29import com.android.systemui.Gefingerpoken;
30import com.android.systemui.Interpolators;
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 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                    }
137                    mDraggingDown = false;
138                } else {
139                    stopDragging();
140                    return false;
141                }
142                break;
143            case MotionEvent.ACTION_CANCEL:
144                stopDragging();
145                return false;
146        }
147        return false;
148    }
149
150    private boolean isFalseTouch() {
151        return mFalsingManager.isFalseTouch() || !mDraggedFarEnough;
152    }
153
154    private void captureStartingChild(float x, float y) {
155        if (mStartingChild == null) {
156            mStartingChild = findView(x, y);
157            if (mStartingChild != null) {
158                mCallback.setUserLockedChild(mStartingChild, true);
159            }
160        }
161    }
162
163    private void handleExpansion(float heightDelta, ExpandableView child) {
164        if (heightDelta < 0) {
165            heightDelta = 0;
166        }
167        boolean expandable = child.isContentExpandable();
168        float rubberbandFactor = expandable
169                ? RUBBERBAND_FACTOR_EXPANDABLE
170                : RUBBERBAND_FACTOR_STATIC;
171        float rubberband = heightDelta * rubberbandFactor;
172        if (expandable
173                && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) {
174            float overshoot =
175                    (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight();
176            overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
177            rubberband -= overshoot;
178        }
179        child.setActualHeight((int) (child.getCollapsedHeight() + rubberband));
180    }
181
182    private void cancelExpansion(final ExpandableView child) {
183        if (child.getActualHeight() == child.getCollapsedHeight()) {
184            mCallback.setUserLockedChild(child, false);
185            return;
186        }
187        ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
188                child.getActualHeight(), child.getCollapsedHeight());
189        anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
190        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
191        anim.addListener(new AnimatorListenerAdapter() {
192            @Override
193            public void onAnimationEnd(Animator animation) {
194                mCallback.setUserLockedChild(child, false);
195            }
196        });
197        anim.start();
198    }
199
200    private void cancelExpansion() {
201        ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
202        anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
203        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
204        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
205            @Override
206            public void onAnimationUpdate(ValueAnimator animation) {
207                mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
208            }
209        });
210        anim.start();
211    }
212
213    private void stopDragging() {
214        mFalsingManager.onNotificatonStopDraggingDown();
215        if (mStartingChild != null) {
216            cancelExpansion(mStartingChild);
217        } else {
218            cancelExpansion();
219        }
220        mDraggingDown = false;
221        mDragDownCallback.onDragDownReset();
222    }
223
224    private ExpandableView findView(float x, float y) {
225        mHost.getLocationOnScreen(mTemp2);
226        x += mTemp2[0];
227        y += mTemp2[1];
228        return mCallback.getChildAtRawPosition(x, y);
229    }
230
231    public interface DragDownCallback {
232
233        /**
234         * @return true if the interaction is accepted, false if it should be cancelled
235         */
236        boolean onDraggedDown(View startingChild, int dragLengthY);
237        void onDragDownReset();
238
239        /**
240         * The user has dragged either above or below the threshold
241         * @param above whether he dragged above it
242         */
243        void onCrossedThreshold(boolean above);
244        void onTouchSlopExceeded();
245        void setEmptyDragAmount(float amount);
246    }
247}
248