/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.android.systemui.qs; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; import android.util.Property; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import android.widget.LinearLayout; import androidx.core.widget.NestedScrollView; import com.android.systemui.R; import com.android.systemui.qs.touch.OverScroll; import com.android.systemui.qs.touch.SwipeDetector; /** * Quick setting scroll view containing the brightness slider and the QS tiles. * *

Call {@link #shouldIntercept(MotionEvent)} from parent views' * {@link #onInterceptTouchEvent(MotionEvent)} method to determine whether this view should * consume the touch event. */ public class QSScrollLayout extends NestedScrollView { private final int mTouchSlop; private final int mFooterHeight; private int mLastMotionY; private final SwipeDetector mSwipeDetector; private final OverScrollHelper mOverScrollHelper; private float mContentTranslationY; public QSScrollLayout(Context context, View... children) { super(context); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); mFooterHeight = getResources().getDimensionPixelSize(R.dimen.qs_footer_height); LinearLayout linearLayout = new LinearLayout(mContext); linearLayout.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); linearLayout.setOrientation(LinearLayout.VERTICAL); for (View view : children) { linearLayout.addView(view); } addView(linearLayout); setOverScrollMode(OVER_SCROLL_NEVER); mOverScrollHelper = new OverScrollHelper(); mSwipeDetector = new SwipeDetector(context, mOverScrollHelper, SwipeDetector.VERTICAL); mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!canScrollVertically(1) && !canScrollVertically(-1)) { return false; } mSwipeDetector.onTouchEvent(ev); return super.onInterceptTouchEvent(ev) || mOverScrollHelper.isInOverScroll(); } @Override public boolean onTouchEvent(MotionEvent ev) { if (!canScrollVertically(1) && !canScrollVertically(-1)) { return false; } mSwipeDetector.onTouchEvent(ev); return super.onTouchEvent(ev); } @Override protected void dispatchDraw(Canvas canvas) { canvas.translate(0, mContentTranslationY); super.dispatchDraw(canvas); canvas.translate(0, -mContentTranslationY); } public boolean shouldIntercept(MotionEvent ev) { if (ev.getY() > (getBottom() - mFooterHeight)) { // Do not intercept touches that are below the divider between QS and the footer. return false; } if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { mLastMotionY = (int) ev.getY(); } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) { // Do not allow NotificationPanelView to intercept touch events when this // view can be scrolled down. if (mLastMotionY >= 0 && Math.abs(ev.getY() - mLastMotionY) > mTouchSlop && canScrollVertically(1)) { requestParentDisallowInterceptTouchEvent(true); mLastMotionY = (int) ev.getY(); return true; } } else if (ev.getActionMasked() == MotionEvent.ACTION_CANCEL || ev.getActionMasked() == MotionEvent.ACTION_UP) { mLastMotionY = -1; requestParentDisallowInterceptTouchEvent(false); } return false; } private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(disallowIntercept); } } private void setContentTranslationY(float contentTranslationY) { mContentTranslationY = contentTranslationY; invalidate(); } private static final Property CONTENT_TRANS_Y = new Property(Float.class, "qsScrollLayoutContentTransY") { @Override public Float get(QSScrollLayout qsScrollLayout) { return qsScrollLayout.mContentTranslationY; } @Override public void set(QSScrollLayout qsScrollLayout, Float y) { qsScrollLayout.setContentTranslationY(y); } }; private class OverScrollHelper implements SwipeDetector.Listener { private boolean mIsInOverScroll; // We use this value to calculate the actual amount the user has overscrolled. private float mFirstDisplacement = 0; @Override public void onDragStart(boolean start) {} @Override public boolean onDrag(float displacement, float velocity) { // Only overscroll if the user is scrolling down when they're already at the bottom // or scrolling up when they're already at the top. boolean wasInOverScroll = mIsInOverScroll; mIsInOverScroll = (!canScrollVertically(1) && displacement < 0) || (!canScrollVertically(-1) && displacement > 0); if (wasInOverScroll && !mIsInOverScroll) { // Exit overscroll. This can happen when the user is in overscroll and then // scrolls the opposite way. Note that this causes the reset translation animation // to run while the user is dragging, which feels a bit unnatural. reset(); } else if (mIsInOverScroll) { if (Float.compare(mFirstDisplacement, 0) == 0) { // Because users can scroll before entering overscroll, we need to // subtract the amount where the user was not in overscroll. mFirstDisplacement = displacement; } float overscrollY = displacement - mFirstDisplacement; setContentTranslationY(getDampedOverScroll(overscrollY)); } return mIsInOverScroll; } @Override public void onDragEnd(float velocity, boolean fling) { reset(); } private void reset() { if (Float.compare(mContentTranslationY, 0) != 0) { ObjectAnimator.ofFloat(QSScrollLayout.this, CONTENT_TRANS_Y, 0) .setDuration(100) .start(); } mIsInOverScroll = false; mFirstDisplacement = 0; } public boolean isInOverScroll() { return mIsInOverScroll; } private float getDampedOverScroll(float y) { return OverScroll.dampedScroll(y, getHeight()); } } }