DragDownHelper.java revision 48bc36af053885daf091bcca6d99411e0438ba83
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;
32
33/**
34 * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
35 * the notification where the drag started.
36 */
37public class DragDownHelper implements Gefingerpoken {
38
39    private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f;
40    private static final float RUBBERBAND_FACTOR_STATIC = 0.15f;
41
42    private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375;
43
44    private int mMinDragDistance;
45    private ExpandHelper.Callback mCallback;
46    private float mInitialTouchX;
47    private float mInitialTouchY;
48    private boolean mDraggingDown;
49    private float mTouchSlop;
50    private DragDownCallback mDragDownCallback;
51    private View mHost;
52    private final int[] mTemp2 = new int[2];
53    private boolean mDraggedFarEnough;
54    private ExpandableView mStartingChild;
55    private Interpolator mInterpolator;
56    private float mLastHeight;
57
58    public DragDownHelper(Context context, View host, ExpandHelper.Callback callback,
59            DragDownCallback dragDownCallback) {
60        mMinDragDistance = context.getResources().getDimensionPixelSize(
61                R.dimen.keyguard_drag_down_min_distance);
62        mInterpolator =
63                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
64        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
65        mCallback = callback;
66        mDragDownCallback = dragDownCallback;
67        mHost = host;
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                    mDraggingDown = true;
88                    captureStartingChild(mInitialTouchX, mInitialTouchY);
89                    mInitialTouchY = y;
90                    mInitialTouchX = x;
91                    mDragDownCallback.onTouchSlopExceeded();
92                    return true;
93                }
94                break;
95        }
96        return false;
97    }
98
99    @Override
100    public boolean onTouchEvent(MotionEvent event) {
101        if (!mDraggingDown) {
102            return false;
103        }
104        final float x = event.getX();
105        final float y = event.getY();
106
107        switch (event.getActionMasked()) {
108            case MotionEvent.ACTION_MOVE:
109                mLastHeight = y - mInitialTouchY;
110                captureStartingChild(mInitialTouchX, mInitialTouchY);
111                if (mStartingChild != null) {
112                    handleExpansion(mLastHeight, mStartingChild);
113                } else {
114                    mDragDownCallback.setEmptyDragAmount(mLastHeight);
115                }
116                if (mLastHeight > mMinDragDistance) {
117                    if (!mDraggedFarEnough) {
118                        mDraggedFarEnough = true;
119                        mDragDownCallback.onThresholdReached();
120                    }
121                } else {
122                    if (mDraggedFarEnough) {
123                        mDraggedFarEnough = false;
124                        mDragDownCallback.onDragDownReset();
125                    }
126                }
127                return true;
128            case MotionEvent.ACTION_UP:
129                if (mDraggedFarEnough && mDragDownCallback.onDraggedDown(mStartingChild)) {
130                    if (mStartingChild != null) {
131                        mCallback.setUserLockedChild(mStartingChild, false);
132                    } else {
133                        mDragDownCallback.setEmptyDragAmount(0f);
134                    }
135                    mDraggingDown = false;
136                } else {
137                    stopDragging();
138                    return false;
139                }
140                break;
141            case MotionEvent.ACTION_CANCEL:
142                stopDragging();
143                return false;
144        }
145        return false;
146    }
147
148    private void captureStartingChild(float x, float y) {
149        if (mStartingChild == null) {
150            mStartingChild = findView(x, y);
151            if (mStartingChild != null) {
152                mCallback.setUserLockedChild(mStartingChild, true);
153            }
154        }
155    }
156
157    private void handleExpansion(float heightDelta, ExpandableView child) {
158        if (heightDelta < 0) {
159            heightDelta = 0;
160        }
161        boolean expandable = child.isContentExpandable();
162        float rubberbandFactor = expandable
163                ? RUBBERBAND_FACTOR_EXPANDABLE
164                : RUBBERBAND_FACTOR_STATIC;
165        float rubberband = heightDelta * rubberbandFactor;
166        if (expandable && (rubberband + child.getMinHeight()) > child.getMaxHeight()) {
167            float overshoot = (rubberband + child.getMinHeight()) - child.getMaxHeight();
168            overshoot *= (1 - RUBBERBAND_FACTOR_STATIC);
169            rubberband -= overshoot;
170        }
171        child.setActualHeight((int) (child.getMinHeight() + rubberband));
172    }
173
174    private void cancelExpansion(final ExpandableView child) {
175        if (child.getActualHeight() == child.getMinHeight()) {
176            return;
177        }
178        ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight",
179                child.getActualHeight(), child.getMinHeight());
180        anim.setInterpolator(mInterpolator);
181        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
182        anim.addListener(new AnimatorListenerAdapter() {
183            @Override
184            public void onAnimationEnd(Animator animation) {
185                mCallback.setUserLockedChild(child, false);
186            }
187        });
188        anim.start();
189    }
190
191    private void cancelExpansion() {
192        ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0);
193        anim.setInterpolator(mInterpolator);
194        anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS);
195        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
196            @Override
197            public void onAnimationUpdate(ValueAnimator animation) {
198                mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue());
199            }
200        });
201        anim.start();
202    }
203
204    private void stopDragging() {
205        if (mStartingChild != null) {
206            cancelExpansion(mStartingChild);
207        } else {
208            cancelExpansion();
209        }
210        mDraggingDown = false;
211        mDragDownCallback.onDragDownReset();
212    }
213
214    private ExpandableView findView(float x, float y) {
215        mHost.getLocationOnScreen(mTemp2);
216        x += mTemp2[0];
217        y += mTemp2[1];
218        return mCallback.getChildAtRawPosition(x, y);
219    }
220
221    public interface DragDownCallback {
222
223        /**
224         * @return true if the interaction is accepted, false if it should be cancelled
225         */
226        boolean onDraggedDown(View startingChild);
227        void onDragDownReset();
228        void onThresholdReached();
229        void onTouchSlopExceeded();
230        void setEmptyDragAmount(float amount);
231    }
232}
233