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