Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bbb00da5 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add overscroll support for QS."

parents 945356ac 6b3b4f49
Loading
Loading
Loading
Loading
+100 −3
Original line number Diff line number Diff line
@@ -14,8 +14,11 @@

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;
@@ -23,6 +26,8 @@ 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.
@@ -35,6 +40,9 @@ 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);
@@ -49,15 +57,19 @@ public class QSScrollLayout extends NestedScrollView {
            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 super.onInterceptTouchEvent(ev);
        }
        return false;
        mSwipeDetector.onTouchEvent(ev);
        return super.onInterceptTouchEvent(ev) || mOverScrollHelper.isInOverScroll();
    }

    @Override
@@ -65,7 +77,15 @@ public class QSScrollLayout extends NestedScrollView {
        if (canScrollVertically(1) || canScrollVertically(-1)) {
            return super.onTouchEvent(ev);
        }
        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) {
@@ -98,4 +118,81 @@ public class QSScrollLayout extends NestedScrollView {
            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());
        }
    }
}
+57 −0
Original line number Diff line number Diff line
/*
 * 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.touch;

/**
 * Utility methods for overscroll damping and related effect.
 *
 * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/OverScroll.java
 */
public class OverScroll {

    private static final float OVERSCROLL_DAMP_FACTOR = 0.07f;

    /**
     * This curve determines how the effect of scrolling over the limits of the page diminishes
     * as the user pulls further and further from the bounds
     *
     * @param f The percentage of how much the user has overscrolled.
     * @return A transformed percentage based on the influence curve.
     */
    private static float overScrollInfluenceCurve(float f) {
        f -= 1.0f;
        return f * f * f + 1.0f;
    }

    /**
     * @param amount The original amount overscrolled.
     * @param max The maximum amount that the View can overscroll.
     * @return The dampened overscroll amount.
     */
    public static int dampedScroll(float amount, int max) {
        if (Float.compare(amount, 0) == 0) return 0;

        float f = amount / max;
        f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));

        // Clamp this factor, f, to -1 < f < 1
        if (Math.abs(f) >= 1) {
            f /= Math.abs(f);
        }

        return Math.round(OVERSCROLL_DAMP_FACTOR * f * max);
    }
}
+356 −0
Original line number Diff line number Diff line
/*
 * 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.touch;

import static android.view.MotionEvent.INVALID_POINTER_ID;

import android.content.Context;
import android.graphics.PointF;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

/**
 * One dimensional scroll/drag/swipe gesture detector.
 *
 * Definition of swipe is different from android system in that this detector handles
 * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
 * swipe action happens
 *
 * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/SwipeDetector.java
 */
public class SwipeDetector {

    private static final boolean DBG = false;
    private static final String TAG = "SwipeDetector";

    private int mScrollConditions;
    public static final int DIRECTION_POSITIVE = 1 << 0;
    public static final int DIRECTION_NEGATIVE = 1 << 1;
    public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;

    private static final float ANIMATION_DURATION = 1200;

    protected int mActivePointerId = INVALID_POINTER_ID;

    /**
     * The minimum release velocity in pixels per millisecond that triggers fling..
     */
    public static final float RELEASE_VELOCITY_PX_MS = 1.0f;

    /**
     * The time constant used to calculate dampening in the low-pass filter of scroll velocity.
     * Cutoff frequency is set at 10 Hz.
     */
    public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10);

    /* Scroll state, this is set to true during dragging and animation. */
    private ScrollState mState = ScrollState.IDLE;

    enum ScrollState {
        IDLE,
        DRAGGING,      // onDragStart, onDrag
        SETTLING       // onDragEnd
    }

    public static abstract class Direction {

        abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint);

        /**
         * Distance in pixels a touch can wander before we think the user is scrolling.
         */
        abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);
    }

    public static final Direction VERTICAL = new Direction() {

        @Override
        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
            return ev.getY(pointerIndex) - refPoint.y;
        }

        @Override
        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
            return Math.abs(ev.getX(pointerIndex) - downPos.x);
        }
    };

    public static final Direction HORIZONTAL = new Direction() {

        @Override
        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
            return ev.getX(pointerIndex) - refPoint.x;
        }

        @Override
        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
            return Math.abs(ev.getY(pointerIndex) - downPos.y);
        }
    };

    //------------------- ScrollState transition diagram -----------------------------------
    //
    // IDLE ->      (mDisplacement > mTouchSlop) -> DRAGGING
    // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
    // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
    // SETTLING -> (View settled) -> IDLE

    private void setState(ScrollState newState) {
        if (DBG) {
            Log.d(TAG, "setState:" + mState + "->" + newState);
        }
        // onDragStart and onDragEnd is reported ONLY on state transition
        if (newState == ScrollState.DRAGGING) {
            initializeDragging();
            if (mState == ScrollState.IDLE) {
                reportDragStart(false /* recatch */);
            } else if (mState == ScrollState.SETTLING) {
                reportDragStart(true /* recatch */);
            }
        }
        if (newState == ScrollState.SETTLING) {
            reportDragEnd();
        }

        mState = newState;
    }

    public boolean isDraggingOrSettling() {
        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
    }

    /**
     * There's no touch and there's no animation.
     */
    public boolean isIdleState() {
        return mState == ScrollState.IDLE;
    }

    public boolean isSettlingState() {
        return mState == ScrollState.SETTLING;
    }

    public boolean isDraggingState() {
        return mState == ScrollState.DRAGGING;
    }

    private final PointF mDownPos = new PointF();
    private final PointF mLastPos = new PointF();
    private final Direction mDir;

    private final float mTouchSlop;

    /* Client of this gesture detector can register a callback. */
    private final Listener mListener;

    private long mCurrentMillis;

    private float mVelocity;
    private float mLastDisplacement;
    private float mDisplacement;

    private float mSubtractDisplacement;
    private boolean mIgnoreSlopWhenSettling;

    public interface Listener {
        void onDragStart(boolean start);

        boolean onDrag(float displacement, float velocity);

        void onDragEnd(float velocity, boolean fling);
    }

    public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
        this(ViewConfiguration.get(context).getScaledTouchSlop(), l, dir);
    }

    @VisibleForTesting
    protected SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir) {
        mTouchSlop = touchSlope;
        mListener = l;
        mDir = dir;
    }

    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
        mScrollConditions = scrollDirectionFlags;
        mIgnoreSlopWhenSettling = ignoreSlop;
    }

    private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
        // reject cases where the angle or slop condition is not met.
        if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
                > Math.abs(mDisplacement)) {
            return false;
        }

        // Check if the client is interested in scroll in current direction.
        if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDisplacement > 0) ||
                ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDisplacement < 0)) {
            return true;
        }
        return false;
    }

    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = ev.getPointerId(0);
                mDownPos.set(ev.getX(), ev.getY());
                mLastPos.set(mDownPos);
                mLastDisplacement = 0;
                mDisplacement = 0;
                mVelocity = 0;

                if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
                    setState(ScrollState.DRAGGING);
                }
                break;
            //case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
                int ptrIdx = ev.getActionIndex();
                int ptrId = ev.getPointerId(ptrIdx);
                if (ptrId == mActivePointerId) {
                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
                    mDownPos.set(
                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
                    mActivePointerId = ev.getPointerId(newPointerIdx);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex == INVALID_POINTER_ID) {
                    break;
                }
                mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos);
                computeVelocity(mDir.getDisplacement(ev, pointerIndex, mLastPos),
                        ev.getEventTime());

                // handle state and listener calls.
                if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
                    setState(ScrollState.DRAGGING);
                }
                if (mState == ScrollState.DRAGGING) {
                    reportDragging();
                }
                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // These are synthetic events and there is no need to update internal values.
                if (mState == ScrollState.DRAGGING) {
                    setState(ScrollState.SETTLING);
                }
                break;
            default:
                break;
        }
        return true;
    }

    public void finishedScrolling() {
        setState(ScrollState.IDLE);
    }

    private boolean reportDragStart(boolean recatch) {
        mListener.onDragStart(!recatch);
        if (DBG) {
            Log.d(TAG, "onDragStart recatch:" + recatch);
        }
        return true;
    }

    private void initializeDragging() {
        if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
            mSubtractDisplacement = 0;
        }
        if (mDisplacement > 0) {
            mSubtractDisplacement = mTouchSlop;
        } else {
            mSubtractDisplacement = -mTouchSlop;
        }
    }

    private boolean reportDragging() {
        if (mDisplacement != mLastDisplacement) {
            if (DBG) {
                Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f",
                        mDisplacement, mVelocity));
            }

            mLastDisplacement = mDisplacement;
            return mListener.onDrag(mDisplacement - mSubtractDisplacement, mVelocity);
        }
        return true;
    }

    private void reportDragEnd() {
        if (DBG) {
            Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
                    mDisplacement, mVelocity));
        }
        mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);

    }

    /**
     * Computes the damped velocity.
     */
    public float computeVelocity(float delta, long currentMillis) {
        long previousMillis = mCurrentMillis;
        mCurrentMillis = currentMillis;

        float deltaTimeMillis = mCurrentMillis - previousMillis;
        float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0;
        if (Math.abs(mVelocity) < 0.001f) {
            mVelocity = velocity;
        } else {
            float alpha = computeDampeningFactor(deltaTimeMillis);
            mVelocity = interpolate(mVelocity, velocity, alpha);
        }
        return mVelocity;
    }

    /**
     * Returns a time-dependent dampening factor using delta time.
     */
    private static float computeDampeningFactor(float deltaTime) {
        return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime);
    }

    /**
     * Returns the linear interpolation between two values
     */
    private static float interpolate(float from, float to, float alpha) {
        return (1.0f - alpha) * from + alpha * to;
    }

    public static long calculateDuration(float velocity, float progressNeeded) {
        // TODO: make these values constants after tuning.
        float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
        float travelDistance = Math.max(0.2f, progressNeeded);
        long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
        if (DBG) {
            Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
        }
        return duration;
    }
}