1/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package com.android.systemui.qs;
16
17import android.animation.ObjectAnimator;
18import android.content.Context;
19import android.graphics.Canvas;
20import android.support.v4.widget.NestedScrollView;
21import android.util.Property;
22import android.view.MotionEvent;
23import android.view.View;
24import android.view.ViewConfiguration;
25import android.view.ViewParent;
26import android.widget.LinearLayout;
27
28import com.android.systemui.R;
29import com.android.systemui.qs.touch.OverScroll;
30import com.android.systemui.qs.touch.SwipeDetector;
31
32/**
33 * Quick setting scroll view containing the brightness slider and the QS tiles.
34 *
35 * <p>Call {@link #shouldIntercept(MotionEvent)} from parent views'
36 * {@link #onInterceptTouchEvent(MotionEvent)} method to determine whether this view should
37 * consume the touch event.
38 */
39public class QSScrollLayout extends NestedScrollView {
40    private final int mTouchSlop;
41    private final int mFooterHeight;
42    private int mLastMotionY;
43    private final SwipeDetector mSwipeDetector;
44    private final OverScrollHelper mOverScrollHelper;
45    private float mContentTranslationY;
46
47    public QSScrollLayout(Context context, View... children) {
48        super(context);
49        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
50        mFooterHeight = getResources().getDimensionPixelSize(R.dimen.qs_footer_height);
51        LinearLayout linearLayout = new LinearLayout(mContext);
52        linearLayout.setLayoutParams(new LinearLayout.LayoutParams(
53            LinearLayout.LayoutParams.MATCH_PARENT,
54            LinearLayout.LayoutParams.WRAP_CONTENT));
55        linearLayout.setOrientation(LinearLayout.VERTICAL);
56        for (View view : children) {
57            linearLayout.addView(view);
58        }
59        addView(linearLayout);
60        setOverScrollMode(OVER_SCROLL_NEVER);
61        mOverScrollHelper = new OverScrollHelper();
62        mSwipeDetector = new SwipeDetector(context, mOverScrollHelper, SwipeDetector.VERTICAL);
63        mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true);
64    }
65
66    @Override
67    public boolean onInterceptTouchEvent(MotionEvent ev) {
68        if (!canScrollVertically(1) && !canScrollVertically(-1)) {
69            return false;
70        }
71        mSwipeDetector.onTouchEvent(ev);
72        return super.onInterceptTouchEvent(ev) || mOverScrollHelper.isInOverScroll();
73    }
74
75    @Override
76    public boolean onTouchEvent(MotionEvent ev) {
77        if (!canScrollVertically(1) && !canScrollVertically(-1)) {
78            return false;
79        }
80        mSwipeDetector.onTouchEvent(ev);
81        return super.onTouchEvent(ev);
82    }
83
84    @Override
85    protected void dispatchDraw(Canvas canvas) {
86        canvas.translate(0, mContentTranslationY);
87        super.dispatchDraw(canvas);
88        canvas.translate(0, -mContentTranslationY);
89    }
90
91    public boolean shouldIntercept(MotionEvent ev) {
92        if (ev.getY() > (getBottom() - mFooterHeight)) {
93            // Do not intercept touches that are below the divider between QS and the footer.
94            return false;
95        }
96        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
97            mLastMotionY = (int) ev.getY();
98        } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
99            // Do not allow NotificationPanelView to intercept touch events when this
100            // view can be scrolled down.
101            if (mLastMotionY >= 0 && Math.abs(ev.getY() - mLastMotionY) > mTouchSlop
102                    && canScrollVertically(1)) {
103                requestParentDisallowInterceptTouchEvent(true);
104                mLastMotionY = (int) ev.getY();
105                return true;
106            }
107        } else if (ev.getActionMasked() == MotionEvent.ACTION_CANCEL
108            || ev.getActionMasked() == MotionEvent.ACTION_UP) {
109            mLastMotionY = -1;
110            requestParentDisallowInterceptTouchEvent(false);
111        }
112        return false;
113    }
114
115    private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
116        final ViewParent parent = getParent();
117        if (parent != null) {
118            parent.requestDisallowInterceptTouchEvent(disallowIntercept);
119        }
120    }
121
122    private void setContentTranslationY(float contentTranslationY) {
123        mContentTranslationY = contentTranslationY;
124        invalidate();
125    }
126
127    private static final Property<QSScrollLayout, Float> CONTENT_TRANS_Y =
128            new Property<QSScrollLayout, Float>(Float.class, "qsScrollLayoutContentTransY") {
129                @Override
130                public Float get(QSScrollLayout qsScrollLayout) {
131                    return qsScrollLayout.mContentTranslationY;
132                }
133
134                @Override
135                public void set(QSScrollLayout qsScrollLayout, Float y) {
136                    qsScrollLayout.setContentTranslationY(y);
137                }
138            };
139
140    private class OverScrollHelper implements SwipeDetector.Listener {
141        private boolean mIsInOverScroll;
142
143        // We use this value to calculate the actual amount the user has overscrolled.
144        private float mFirstDisplacement = 0;
145
146        @Override
147        public void onDragStart(boolean start) {}
148
149        @Override
150        public boolean onDrag(float displacement, float velocity) {
151            // Only overscroll if the user is scrolling down when they're already at the bottom
152            // or scrolling up when they're already at the top.
153            boolean wasInOverScroll = mIsInOverScroll;
154            mIsInOverScroll = (!canScrollVertically(1) && displacement < 0) ||
155                    (!canScrollVertically(-1) && displacement > 0);
156
157            if (wasInOverScroll && !mIsInOverScroll) {
158                // Exit overscroll. This can happen when the user is in overscroll and then
159                // scrolls the opposite way. Note that this causes the reset translation animation
160                // to run while the user is dragging, which feels a bit unnatural.
161                reset();
162            } else if (mIsInOverScroll) {
163                if (Float.compare(mFirstDisplacement, 0) == 0) {
164                    // Because users can scroll before entering overscroll, we need to
165                    // subtract the amount where the user was not in overscroll.
166                    mFirstDisplacement = displacement;
167                }
168                float overscrollY = displacement - mFirstDisplacement;
169                setContentTranslationY(getDampedOverScroll(overscrollY));
170            }
171
172            return mIsInOverScroll;
173        }
174
175        @Override
176        public void onDragEnd(float velocity, boolean fling) {
177            reset();
178        }
179
180        private void reset() {
181            if (Float.compare(mContentTranslationY, 0) != 0) {
182                ObjectAnimator.ofFloat(QSScrollLayout.this, CONTENT_TRANS_Y, 0)
183                        .setDuration(100)
184                        .start();
185            }
186            mIsInOverScroll = false;
187            mFirstDisplacement = 0;
188        }
189
190        public boolean isInOverScroll() {
191            return mIsInOverScroll;
192        }
193
194        private float getDampedOverScroll(float y) {
195            return OverScroll.dampedScroll(y, getHeight());
196        }
197    }
198}
199