/*
|
* 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.
|
*
|
* <p>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<QSScrollLayout, Float> CONTENT_TRANS_Y =
|
new Property<QSScrollLayout, Float>(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());
|
}
|
}
|
}
|