Home | History | Annotate | Download | only in qs
      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 
     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
     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
     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
     86     protected void dispatchDraw(Canvas canvas) {
     87         canvas.translate(0, mContentTranslationY);
     88         super.dispatchDraw(canvas);
     89         canvas.translate(0, -mContentTranslationY);
     90     }
     91 
     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 
    116     private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
    117         final ViewParent parent = getParent();
    118         if (parent != null) {
    119             parent.requestDisallowInterceptTouchEvent(disallowIntercept);
    120         }
    121     }
    122 
    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     }
    199 }
    200