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

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

Merge "Adding the touch gestures for the pull down notification shade." into qt-dev

parents 0cab8dc0 b0241580
Loading
Loading
Loading
Loading
+268 −22
Original line number Diff line number Diff line
@@ -16,11 +16,15 @@

package com.android.systemui.statusbar.car;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.car.drivingstate.CarDrivingStateEvent;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.GestureDetector;
@@ -55,6 +59,7 @@ import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.car.CarQSFragment;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.car.hvac.HvacController;
import com.android.systemui.statusbar.car.hvac.TemperatureView;
@@ -74,6 +79,15 @@ import java.util.Map;
public class CarStatusBar extends StatusBar implements
        CarBatteryController.BatteryViewHandler {
    private static final String TAG = "CarStatusBar";
    // used to calculate how fast to open or close the window
    private static final float DEFAULT_FLING_VELOCITY = 0;
    // max time a fling animation takes
    private static final float FLING_ANIMATION_MAX_TIME = 0.5f;
    // acceleration rate for the fling animation
    private static final float FLING_SPEED_UP_FACTOR = 0.6f;

    private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
    private float mClosingVelocity = DEFAULT_FLING_VELOCITY;

    private TaskStackListenerImpl mTaskStackListener;

@@ -100,6 +114,7 @@ public class CarStatusBar extends StatusBar implements
    private boolean mDeviceIsProvisioned = true;
    private HvacController mHvacController;
    private DrivingStateHelper mDrivingStateHelper;
    private static FlingAnimationUtils sFlingAnimationUtils;
    private SwitchToGuestTimer mSwitchToGuestTimer;

    // The container for the notifications.
@@ -113,6 +128,27 @@ public class CarStatusBar extends StatusBar implements
    // it's open.
    private View.OnTouchListener mNavBarNotificationTouchListener;

    // Percentage from top of the screen after which the notification shade will open. This value
    // will be used while opening the notification shade.
    private int mSettleOpenPercentage;
    // Percentage from top of the screen below which the notification shade will close. This
    // value will be used while closing the notification shade.
    private int mSettleClosePercentage;
    // Percentage of notification shade open from top of the screen.
    private int mPercentageFromBottom;
    // If notification shade is animation to close or to open.
    private boolean mIsNotificationAnimating;

    // Tracks when the notification shade is being scrolled. This refers to the glass pane being
    // scrolled not the recycler view.
    private boolean mIsTracking;
    private float mFirstTouchDownOnGlassPane;

    // If the notification card inside the recycler view is being swiped.
    private boolean mIsNotificationCardSwiping;
    // If notification shade is being swiped vertically to close.
    private boolean mIsSwipingVerticallyToClose;

    @Override
    public void start() {
        // get the provisioned state before calling the parent class since it's that flow that
@@ -125,6 +161,12 @@ public class CarStatusBar extends StatusBar implements
        mActivityManagerWrapper.registerTaskStackListener(mTaskStackListener);

        mNotificationPanel.setScrollingEnabled(true);
        mSettleOpenPercentage = mContext.getResources().getInteger(
                R.integer.notification_settle_open_percentage);
        mSettleClosePercentage = mContext.getResources().getInteger(
                R.integer.notification_settle_close_percentage);
        sFlingAnimationUtils = new FlingAnimationUtils(mContext,
                FLING_ANIMATION_MAX_TIME, FLING_SPEED_UP_FACTOR);

        createBatteryController();
        mCarBatteryController.startListening();
@@ -313,14 +355,46 @@ public class CarStatusBar extends StatusBar implements
                    }
                });
        mNavBarNotificationTouchListener =
                (v, event) -> navBarCloseNotificationGestureDetector.onTouchEvent(event);
                (v, event) -> {
                    boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
                    if (consumed) {
                        return true;
                    }
                    if (event.getActionMasked() == MotionEvent.ACTION_UP
                            && mNotificationView.getVisibility() == View.VISIBLE) {
                        if (mSettleClosePercentage < mPercentageFromBottom) {
                            animateNotificationPanel(
                                    DEFAULT_FLING_VELOCITY, false);
                        } else {
                            animateNotificationPanel(DEFAULT_FLING_VELOCITY,
                                    true);
                        }
                    }
                    return true;
                };

        // The following are the ui elements that the user would call the status bar.
        // This will set the status bar so it they can make call backs.
        CarNavigationBarView topBar = mStatusBarWindow.findViewById(R.id.car_top_bar);
        topBar.setStatusBar(this);
        topBar.setStatusBarWindowTouchListener((v1, event1) ->
                openGestureDetector.onTouchEvent(event1));
        topBar.setStatusBarWindowTouchListener((v1, event1) -> {

                    boolean consumed = openGestureDetector.onTouchEvent(event1);
                    if (consumed) {
                        return true;
                    }
                    if (event1.getActionMasked() == MotionEvent.ACTION_UP
                            && mNotificationView.getVisibility() == View.VISIBLE) {
                        if (mSettleOpenPercentage > mPercentageFromBottom) {
                            animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
                        } else {
                            animateNotificationPanel(
                                    DEFAULT_FLING_VELOCITY, false);
                        }
                    }
                    return true;
                }
        );

        NotificationClickHandlerFactory clickHandlerFactory = new NotificationClickHandlerFactory(
                mBarService,
@@ -347,7 +421,10 @@ public class CarStatusBar extends StatusBar implements
                mNotificationListAtBottomAtTimeOfTouch = false;
            }
            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                mFirstTouchDownOnGlassPane = event.getRawX();
                mNotificationListAtBottomAtTimeOfTouch = mNotificationListAtBottom;
                // Reset the tracker when there is a touch down on the glass pane.
                mIsTracking = false;
                // Pass the down event to gesture detector so that it knows where the touch event
                // started.
                closeGestureDetector.onTouchEvent(event);
@@ -364,23 +441,56 @@ public class CarStatusBar extends StatusBar implements
                    return;
                }
                mNotificationListAtBottom = false;
                mIsSwipingVerticallyToClose = false;
                mNotificationListAtBottomAtTimeOfTouch = false;
            }
        });
        mNotificationList.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
                        > SWIPE_MAX_OFF_PATH;
                if (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom) {
                    // We need to save the state here as if notification card is swiping we will
                    // change the mNotificationListAtBottomAtTimeOfTouch. This is to protect
                    // closing the notification shade while the notification card is being swiped.
                    mIsSwipingVerticallyToClose = true;
                }

                // If the card is swiping we should not allow the notification shade to close.
                // Hence setting mNotificationListAtBottomAtTimeOfTouch to false will stop that
                // for us. We are also checking for mIsTracking because while swiping the
                // notification shade to close if the user goes a bit horizontal while swiping
                // upwards then also this should close.
                if (mIsNotificationCardSwiping && !mIsTracking) {
                    mNotificationListAtBottomAtTimeOfTouch = false;
                }

                boolean handled = false;
                if (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom) {
                    handled = closeGestureDetector.onTouchEvent(event);
                }
                boolean isTracking = mIsTracking;
                Rect rect = mNotificationView.getClipBounds();
                float clippedHeight = rect.bottom;
                if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP
                        && mIsSwipingVerticallyToClose) {
                    if (mSettleClosePercentage < mPercentageFromBottom && isTracking) {
                        animateNotificationPanel(DEFAULT_FLING_VELOCITY, false);
                    } else if (clippedHeight != mNotificationView.getHeight() && isTracking) {
                        // this can be caused when user is at the end of the list and trying to
                        // fling to top of the list by scrolling down.
                        animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
                    }
                }

                // Updating the mNotificationListAtBottomAtTimeOfTouch state has to be done after
                // the event has been passed to the closeGestureDetector above, such that the
                // closeGestureDetector sees the up event before the state has changed.
                if (event.getActionMasked() == MotionEvent.ACTION_UP) {
                    mNotificationListAtBottomAtTimeOfTouch = false;
                }
                return handled;
                return handled || isTracking;
            }
        });

@@ -401,7 +511,9 @@ public class CarStatusBar extends StatusBar implements
        mNotificationList.scrollToPosition(0);
        mStatusBarWindowController.setPanelVisible(true);
        mNotificationView.setVisibility(View.VISIBLE);
        // let the status bar know that the panel is open

        animateNotificationPanel(mOpeningVelocity, false);

        setPanelExpanded(true);
    }

@@ -415,12 +527,66 @@ public class CarStatusBar extends StatusBar implements
        mStatusBarWindowController.setStatusBarFocusable(false);
        mStatusBarWindow.cancelExpandHelper();
        mStatusBarView.collapsePanel(true /* animate */, delayed, speedUpFactor);

        animateNotificationPanel(mClosingVelocity, true);

        if (!mIsTracking) {
            mStatusBarWindowController.setPanelVisible(false);
            mNotificationView.setVisibility(View.INVISIBLE);
        // let the status bar know that the panel is cloased
        }

        setPanelExpanded(false);
    }

    /**
     * Animates the notification shade from one position to other. This is used to either open or
     * close the notification shade completely with a velocity. Id the animation is to close the
     * notification shade this method also makes the view invisible after animation ends.
     */
    private void animateNotificationPanel(float velocity, boolean isClosing) {
        Rect rect = mNotificationView.getClipBounds();
        if (rect == null) {
            return;
        }
        float from = rect.bottom;
        float to = 0;
        if (!isClosing) {
            to = mNotificationView.getHeight();
        }
        if (mIsNotificationAnimating) {
            return;
        }
        mIsNotificationAnimating = true;
        mIsTracking = true;
        ValueAnimator animator = ValueAnimator.ofFloat(from, to);
        animator.addUpdateListener(
                animation -> {
                    float animatedValue = (Float) animation.getAnimatedValue();
                    setNotificationViewClipBounds((int) animatedValue);
                });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                mIsNotificationAnimating = false;
                mIsTracking = false;
                mOpeningVelocity = DEFAULT_FLING_VELOCITY;
                mClosingVelocity = DEFAULT_FLING_VELOCITY;
                if (isClosing) {
                    mStatusBarWindowController.setPanelVisible(false);
                    mNotificationView.setVisibility(View.INVISIBLE);
                    mNotificationView.setClipBounds(null);
                    // let the status bar know that the panel is closed
                    setPanelExpanded(false);
                } else {
                    // let the status bar know that the panel is open
                    setPanelExpanded(true);
                }
            }
        });
        sFlingAnimationUtils.apply(animator, from, to, Math.abs(velocity));
        animator.start();
    }

    @Override
    protected QS createDefaultQSFragment() {
@@ -576,7 +742,7 @@ public class CarStatusBar extends StatusBar implements

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        //When executing dump() funciton simultaneously, we need to serialize them
        //When executing dump() function simultaneously, we need to serialize them
        //to get mStackScroller's position correctly.
        synchronized (mQueueLock) {
            pw.println("  mStackScroller: " + viewInfo(mStackScroller));
@@ -760,38 +926,69 @@ public class CarStatusBar extends StatusBar implements
    }

    /** Returns true if the current user makes it through the setup wizard, false otherwise. */
    public boolean getIsUserSetup(){
    private boolean getIsUserSetup() {
        return mUserSetup;
    }

    private void setNotificationViewClipBounds(int height) {
        Rect clipBounds = new Rect();
        clipBounds.set(0, 0, mNotificationView.getWidth(), height);
        mNotificationView.setClipBounds(clipBounds);
    }

    private void calculatePercentageFromBottom(float height) {
        if (mNotificationView.getHeight() > 0) {
            mPercentageFromBottom = (int) Math.abs(
                    height / mNotificationView.getHeight() * 100);
        }
    }

    // TODO: add settle down/up logic
    private static final int SWIPE_UP_MIN_DISTANCE = 75;
    private static final int SWIPE_DOWN_MIN_DISTANCE = 25;
    private static final int SWIPE_MAX_OFF_PATH = 75;
    private static final int SWIPE_THRESHOLD_VELOCITY = 200;

    // Only responsible for open hooks. Since once the panel opens it covers all elements
    // there is no need to merge with close.
    private abstract class OpenNotificationGestureListener extends
            GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
                float distanceY) {

            if (mNotificationView.getVisibility() == View.INVISIBLE) {
                // when the on-scroll is called for the first time to open.
                mNotificationList.scrollToPosition(0);
            }
            mStatusBarWindowController.setPanelVisible(true);
            mNotificationView.setVisibility(View.VISIBLE);

            // clips the view for the notification shade when the user scrolls to open.
            setNotificationViewClipBounds((int) event2.getRawY());

            // Initially the scroll starts with height being zero. This checks protects from divide
            // by zero error.
            calculatePercentageFromBottom(event2.getRawY());

            mIsTracking = true;
            return true;
        }


        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2,
                float velocityX, float velocityY) {
            if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
                    || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
                // swipe was not vertical or was not fast enough
                return false;
            }
            boolean isDown = velocityY > 0;
            float distanceDelta = Math.abs(event1.getY() - event2.getY());
            if (isDown && distanceDelta > SWIPE_DOWN_MIN_DISTANCE) {
            if (velocityY > SWIPE_THRESHOLD_VELOCITY) {
                mOpeningVelocity = velocityY;
                openNotification();
                return true;
            }
            animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);

            return false;
        }

        protected abstract void openNotification();
    }

@@ -799,36 +996,85 @@ public class CarStatusBar extends StatusBar implements
    private abstract class CloseNotificationGestureListener extends
            GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
                float distanceY) {
            if (!mNotificationListAtBottomAtTimeOfTouch) {
                return false;
            }
            float actualNotificationHeight =
                    mNotificationView.getHeight() - (event1.getRawY() - event2.getRawY());
            if (actualNotificationHeight > mNotificationView.getHeight()) {
                actualNotificationHeight = mNotificationView.getHeight();
            }
            if (mNotificationView.getHeight() > 0) {
                mPercentageFromBottom = (int) Math.abs(
                        actualNotificationHeight / mNotificationView.getHeight() * 100);
                boolean isUp = distanceY > 0;

                // This check is to figure out if onScroll was called while swiping the card at
                // bottom of the list. At that time we should not allow notification shade to
                // close. We are also checking for the upwards swipe gesture here because it is
                // possible if a user is closing the notification shade and while swiping starts
                // to open again but does not fling. At that time we should allow the
                // notification shade to close fully or else it would stuck in between.
                if (Math.abs(mNotificationView.getHeight() - actualNotificationHeight)
                        > SWIPE_DOWN_MIN_DISTANCE && isUp) {
                    setNotificationViewClipBounds((int) actualNotificationHeight);
                    mIsTracking = true;
                } else if (!isUp) {
                    setNotificationViewClipBounds((int) actualNotificationHeight);
                }
            }
            // if we return true the the items in RV won't be scrollable.
            return false;
        }


        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2,
                float velocityX, float velocityY) {

            if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
                    || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
                // swipe was not vertical or was not fast enough
                return false;
            }
            boolean isUp = velocityY < 0;
            float distanceDelta = Math.abs(event1.getY() - event2.getY());
            if (isUp && distanceDelta > SWIPE_UP_MIN_DISTANCE) {
            if (isUp) {
                close();
                return true;
            } else {
                // we should close the shade
                animateNotificationPanel(velocityY, false);
            }
            return false;
        }

        protected abstract void close();
    }

    // to be installed on the nav bars
    // To be installed on the nav bars.
    private abstract class NavBarCloseNotificationGestureListener extends
            CloseNotificationGestureListener {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            mClosingVelocity = DEFAULT_FLING_VELOCITY;
            close();
            return super.onSingleTapUp(e);
        }

        @Override
        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
                float distanceY) {
            calculatePercentageFromBottom(event2.getRawY());
            setNotificationViewClipBounds((int) event2.getRawY());
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            mClosingVelocity = DEFAULT_FLING_VELOCITY;
            close();
            super.onLongPress(e);
        }