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