blob: 7b1509dcd17399750bb0e960ad21a34e28c57fbd [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;
Jason Monk231b0522018-01-04 10:49:55 -050020import android.support.v4.widget.NestedScrollView;
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050021import android.util.Property;
Jason Monk231b0522018-01-04 10:49:55 -050022import android.view.MotionEvent;
23import android.view.View;
24import android.view.ViewConfiguration;
25import android.view.ViewParent;
26import android.widget.LinearLayout;
27
Amin Shaikhc225e322018-01-31 18:08:34 -050028import com.android.systemui.R;
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050029import com.android.systemui.qs.touch.OverScroll;
30import com.android.systemui.qs.touch.SwipeDetector;
Amin Shaikhc225e322018-01-31 18:08:34 -050031
Jason Monk231b0522018-01-04 10:49:55 -050032/**
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;
Amin Shaikhc225e322018-01-31 18:08:34 -050041 private final int mFooterHeight;
Jason Monk231b0522018-01-04 10:49:55 -050042 private int mLastMotionY;
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050043 private final SwipeDetector mSwipeDetector;
44 private final OverScrollHelper mOverScrollHelper;
45 private float mContentTranslationY;
Jason Monk231b0522018-01-04 10:49:55 -050046
47 public QSScrollLayout(Context context, View... children) {
48 super(context);
49 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
Amin Shaikhc225e322018-01-31 18:08:34 -050050 mFooterHeight = getResources().getDimensionPixelSize(R.dimen.qs_footer_height);
Jason Monk231b0522018-01-04 10:49:55 -050051 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);
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050060 setOverScrollMode(OVER_SCROLL_NEVER);
61 mOverScrollHelper = new OverScrollHelper();
62 mSwipeDetector = new SwipeDetector(context, mOverScrollHelper, SwipeDetector.VERTICAL);
63 mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true);
Jason Monk231b0522018-01-04 10:49:55 -050064 }
65
Amin Shaikh489c5072018-02-02 13:38:03 -050066 @Override
67 public boolean onInterceptTouchEvent(MotionEvent ev) {
Amin Shaikhba1442a2018-02-08 13:17:48 -050068 if (!canScrollVertically(1) && !canScrollVertically(-1)) {
69 return false;
Amin Shaikh489c5072018-02-02 13:38:03 -050070 }
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050071 mSwipeDetector.onTouchEvent(ev);
72 return super.onInterceptTouchEvent(ev) || mOverScrollHelper.isInOverScroll();
Amin Shaikh489c5072018-02-02 13:38:03 -050073 }
74
75 @Override
76 public boolean onTouchEvent(MotionEvent ev) {
Amin Shaikhba1442a2018-02-08 13:17:48 -050077 if (!canScrollVertically(1) && !canScrollVertically(-1)) {
78 return false;
Amin Shaikh489c5072018-02-02 13:38:03 -050079 }
Amin Shaikh6b3b4f42018-02-06 16:35:01 -050080 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);
Amin Shaikh489c5072018-02-02 13:38:03 -050089 }
90
Jason Monk231b0522018-01-04 10:49:55 -050091 public boolean shouldIntercept(MotionEvent ev) {
Amin Shaikhc225e322018-01-31 18:08:34 -050092 if (ev.getY() > (getBottom() - mFooterHeight)) {
93 // Do not intercept touches that are below the divider between QS and the footer.
Jason Monk231b0522018-01-04 10:49:55 -050094 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 }
Amin Shaikh6b3b4f42018-02-06 16:35:01 -0500121
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 }
Jason Monk231b0522018-01-04 10:49:55 -0500198}