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 
15 package com.android.systemui.qs;
16 
17 import android.animation.ObjectAnimator;
18 import android.content.Context;
19 import android.graphics.Canvas;
20 import android.support.v4.widget.NestedScrollView;
21 import android.util.Property;
22 import android.view.MotionEvent;
23 import android.view.View;
24 import android.view.ViewConfiguration;
25 import android.view.ViewParent;
26 import android.widget.LinearLayout;
27 
28 import com.android.systemui.R;
29 import com.android.systemui.qs.touch.OverScroll;
30 import 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  */
39 public 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 
QSScrollLayout(Context context, View... children)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
onInterceptTouchEvent(MotionEvent ev)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
onTouchEvent(MotionEvent ev)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
dispatchDraw(Canvas canvas)85     protected void dispatchDraw(Canvas canvas) {
86         canvas.translate(0, mContentTranslationY);
87         super.dispatchDraw(canvas);
88         canvas.translate(0, -mContentTranslationY);
89     }
90 
shouldIntercept(MotionEvent ev)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 
requestParentDisallowInterceptTouchEvent(boolean disallowIntercept)115     private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
116         final ViewParent parent = getParent();
117         if (parent != null) {
118             parent.requestDisallowInterceptTouchEvent(disallowIntercept);
119         }
120     }
121 
setContentTranslationY(float contentTranslationY)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
onDragStart(boolean start)147         public void onDragStart(boolean start) {}
148 
149         @Override
onDrag(float displacement, float velocity)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
onDragEnd(float velocity, boolean fling)176         public void onDragEnd(float velocity, boolean fling) {
177             reset();
178         }
179 
reset()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 
isInOverScroll()190         public boolean isInOverScroll() {
191             return mIsInOverScroll;
192         }
193 
getDampedOverScroll(float y)194         private float getDampedOverScroll(float y) {
195             return OverScroll.dampedScroll(y, getHeight());
196         }
197     }
198 }
199