blob: 001cbbac560661357cdb3d71c93a9bafbf568891 [file] [log] [blame]
Jason Monk231b0522018-01-04 10:49:55 -05001/*
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
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050017import android.animation.ObjectAnimator;
Jason Monk231b0522018-01-04 10:49:55 -050018import android.content.Context;
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050019import android.graphics.Canvas;
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050020import android.util.Property;
Jason Monk231b0522018-01-04 10:49:55 -050021import android.view.MotionEvent;
22import android.view.View;
23import android.view.ViewConfiguration;
24import android.view.ViewParent;
25import android.widget.LinearLayout;
26
Gus Prevasab336792018-11-14 13:52:20 -050027import androidx.core.widget.NestedScrollView;
28
Amin Shaikhc225e322018-01-31 18:08:34 -050029import com.android.systemui.R;
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050030import com.android.systemui.qs.touch.OverScroll;
31import com.android.systemui.qs.touch.SwipeDetector;
Amin Shaikhc225e322018-01-31 18:08:34 -050032
Jason Monk231b0522018-01-04 10:49:55 -050033/**
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 */
40public class QSScrollLayout extends NestedScrollView {
41 private final int mTouchSlop;
Amin Shaikhc225e322018-01-31 18:08:34 -050042 private final int mFooterHeight;
Jason Monk231b0522018-01-04 10:49:55 -050043 private int mLastMotionY;
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050044 private final SwipeDetector mSwipeDetector;
45 private final OverScrollHelper mOverScrollHelper;
46 private float mContentTranslationY;
Jason Monk231b0522018-01-04 10:49:55 -050047
48 public QSScrollLayout(Context context, View... children) {
49 super(context);
50 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
Amin Shaikhc225e322018-01-31 18:08:34 -050051 mFooterHeight = getResources().getDimensionPixelSize(R.dimen.qs_footer_height);
Jason Monk231b0522018-01-04 10:49:55 -050052 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);
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050061 setOverScrollMode(OVER_SCROLL_NEVER);
62 mOverScrollHelper = new OverScrollHelper();
63 mSwipeDetector = new SwipeDetector(context, mOverScrollHelper, SwipeDetector.VERTICAL);
64 mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true);
Jason Monk231b0522018-01-04 10:49:55 -050065 }
66
Amin Shaikh489c5072018-02-02 13:38:03 -050067 @Override
68 public boolean onInterceptTouchEvent(MotionEvent ev) {
Amin Shaikhba1442a2018-02-08 13:17:48 -050069 if (!canScrollVertically(1) && !canScrollVertically(-1)) {
70 return false;
Amin Shaikh489c5072018-02-02 13:38:03 -050071 }
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050072 mSwipeDetector.onTouchEvent(ev);
73 return super.onInterceptTouchEvent(ev) || mOverScrollHelper.isInOverScroll();
Amin Shaikh489c5072018-02-02 13:38:03 -050074 }
75
76 @Override
77 public boolean onTouchEvent(MotionEvent ev) {
Amin Shaikhba1442a2018-02-08 13:17:48 -050078 if (!canScrollVertically(1) && !canScrollVertically(-1)) {
79 return false;
Amin Shaikh489c5072018-02-02 13:38:03 -050080 }
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050081 mSwipeDetector.onTouchEvent(ev);
82 return super.onTouchEvent(ev);
83 }
84
85 @Override
86 protected void dispatchDraw(Canvas canvas) {
87 canvas.translate(0, mContentTranslationY);
88 super.dispatchDraw(canvas);
89 canvas.translate(0, -mContentTranslationY);
Amin Shaikh489c5072018-02-02 13:38:03 -050090 }
91
Jason Monk231b0522018-01-04 10:49:55 -050092 public boolean shouldIntercept(MotionEvent ev) {
Amin Shaikhc225e322018-01-31 18:08:34 -050093 if (ev.getY() > (getBottom() - mFooterHeight)) {
94 // Do not intercept touches that are below the divider between QS and the footer.
Jason Monk231b0522018-01-04 10:49:55 -050095 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
116 private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
117 final ViewParent parent = getParent();
118 if (parent != null) {
119 parent.requestDisallowInterceptTouchEvent(disallowIntercept);
120 }
121 }
Amin Shaikh6b3b4f42018-02-06 16:35:01 -0500122
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
148 public void onDragStart(boolean start) {}
149
150 @Override
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
177 public void onDragEnd(float velocity, boolean fling) {
178 reset();
179 }
180
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
191 public boolean isInOverScroll() {
192 return mIsInOverScroll;
193 }
194
195 private float getDampedOverScroll(float y) {
196 return OverScroll.dampedScroll(y, getHeight());
197 }
198 }
Jason Monk231b0522018-01-04 10:49:55 -0500199}