blob: 7b1509dcd17399750bb0e960ad21a34e28c57fbd [file] [log] [blame]
/*
* 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.support.v4.widget.NestedScrollView;
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 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());
}
}
}