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