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

Commit 05a3bbde authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Adding swipe gestures in overview screen

> When on home time, swiping up goes to all_apps, and swiping down goes to normal
> When on a recents tile, swiping up the tile dismisses it, swiping down launches it
> When on a recents tile, swiping up on the hotseat opens allApps.

Change-Id: I59f8c02f5c5d9cb88c0585a083fbc33d33b1c806
parent 9f082604
Loading
Loading
Loading
Loading
+323 −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.launcher3.uioverrides;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.touch.SwipeDetector;
import com.android.launcher3.util.TouchController;
import com.android.quickstep.RecentsView;
import com.android.quickstep.TaskView;

import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;

/**
 * Touch controller for swipe interaction in Overview state
 */
public class OverviewSwipeController extends AnimatorListenerAdapter
        implements TouchController, SwipeDetector.Listener {

    private static final String TAG = "OverviewSwipeController";

    private static final float ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS = 0.1f;
    private static final int SINGLE_FRAME_MS = 16;

    // Progress after which the transition is assumed to be a success in case user does not fling
    private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;

    private final Launcher mLauncher;
    private final SwipeDetector mDetector;
    private final RecentsView mRecentsView;
    private final int[] mTempCords = new int[2];

    private AnimatorPlaybackController mCurrentAnimation;
    private boolean mCurrentAnimationIsGoingUp;

    private boolean mNoIntercept;
    private boolean mSwipeDownEnabled;

    private float mDisplacementShift;
    private float mProgressMultiplier;
    private float mEndDisplacement;

    private TaskView mTaskBeingDragged;

    public OverviewSwipeController(Launcher launcher) {
        mLauncher = launcher;
        mRecentsView = launcher.getOverviewPanel();
        mDetector = new SwipeDetector(launcher, this, SwipeDetector.VERTICAL);
    }

    private boolean canInterceptTouch() {
        if (mCurrentAnimation != null) {
            // If we are already animating from a previous state, we can intercept.
            return true;
        }
        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
            return false;
        }
        return mLauncher.isInState(OVERVIEW);
    }

    private boolean isEventOverHotseat(MotionEvent ev) {
        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
            return ev.getY() >
                    mLauncher.getDragLayer().getHeight() * OVERVIEW.getVerticalProgress(mLauncher);
        } else {
            return mLauncher.getDragLayer().isEventOverHotseat(ev);
        }
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
            Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
            mDetector.finishedScrolling();
            mCurrentAnimation = null;
        }
    }

    @Override
    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            mNoIntercept = !canInterceptTouch();
            if (mNoIntercept) {
                return false;
            }

            // Now figure out which direction scroll events the controller will start
            // calling the callbacks.
            final int directionsToDetectScroll;
            boolean ignoreSlopWhenSettling = false;

            if (mCurrentAnimation != null) {
                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
                ignoreSlopWhenSettling = true;
            } else {
                mTaskBeingDragged = null;
                mSwipeDownEnabled = true;

                int currentPage = mRecentsView.getCurrentPage();
                if (currentPage == 0) {
                    // User is on home tile
                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
                } else {
                    View view = mRecentsView.getChildAt(currentPage);
                    if (mLauncher.getDragLayer().isEventOverView(view, ev) &&
                            view instanceof TaskView) {
                        // The tile can be dragged down to open the task.
                        mTaskBeingDragged = (TaskView) view;
                        directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
                    } else if (isEventOverHotseat(ev)) {
                        // The hotseat is being dragged
                        directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
                        mSwipeDownEnabled = false;
                    } else {
                        mNoIntercept = true;
                        return false;
                    }
                }
            }

            mDetector.setDetectableScrollConditions(
                    directionsToDetectScroll, ignoreSlopWhenSettling);
        }

        if (mNoIntercept) {
            return false;
        }

        onControllerTouchEvent(ev);
        return mDetector.isDraggingOrSettling();
    }

    @Override
    public boolean onControllerTouchEvent(MotionEvent ev) {
        return mDetector.onTouchEvent(ev);
    }

    private void reinitAnimationController(boolean goingUp) {
        if (!goingUp && !mSwipeDownEnabled) {
            goingUp = true;
        }
        if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
            // No need to init
            return;
        }
        if (mCurrentAnimation != null) {
            mCurrentAnimation.setPlayFraction(0);
        }
        mCurrentAnimationIsGoingUp = goingUp;
        float range = mLauncher.getAllAppsController().getShiftRange();
        long maxDuration = (long) (2 * range);
        DragLayer dl = mLauncher.getDragLayer();

        if (mTaskBeingDragged == null) {
            // User is either going to all apps or home
            mCurrentAnimation = mLauncher.getStateManager()
                    .createAnimationToNewWorkspace(goingUp ? ALL_APPS : NORMAL, maxDuration);
            if (goingUp) {
                mEndDisplacement = -range;
            } else {
                View ws = mLauncher.getWorkspace();
                mTempCords[1] = ws.getHeight() - ws.getPaddingBottom();
                dl.getDescendantCoordRelativeToSelf(ws, mTempCords);

                float distance = mTempCords[1];
                if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
                    mTempCords[1] = 0;
                    dl.getDescendantCoordRelativeToSelf(mLauncher.getHotseat(), mTempCords);
                    distance = mTempCords[1] - distance;
                } else {
                    distance = dl.getHeight() - distance;
                }

                mEndDisplacement = distance;
            }
        } else {
            if (goingUp) {
                AnimatorSet anim = new AnimatorSet();
                ObjectAnimator translate = ObjectAnimator.ofFloat(
                        mTaskBeingDragged, View.TRANSLATION_Y, -mTaskBeingDragged.getBottom());
                translate.setInterpolator(LINEAR);
                translate.setDuration(maxDuration);
                anim.play(translate);

                ObjectAnimator alpha = ObjectAnimator.ofFloat(mTaskBeingDragged, View.ALPHA, 0);
                alpha.setInterpolator(DEACCEL_1_5);
                alpha.setDuration(maxDuration);
                anim.play(alpha);
                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
                mEndDisplacement = -mTaskBeingDragged.getBottom();
            } else {
                AnimatorSet anim = new AnimatorSet();
                // TODO: Setup a zoom animation
                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);

                mTempCords[1] = mTaskBeingDragged.getHeight();
                dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
                mEndDisplacement = dl.getHeight() - mTempCords[1];
            }
        }

        mCurrentAnimation.getTarget().addListener(this);
        mCurrentAnimation.dispatchOnStart();
        mProgressMultiplier = 1 / mEndDisplacement;
    }

    @Override
    public void onDragStart(boolean start) {
        if (mCurrentAnimation == null) {
            reinitAnimationController(mDetector.wasInitialTouchPositive());
            mDisplacementShift = 0;
        } else {
            mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
            mCurrentAnimation.pause();
        }
    }

    @Override
    public boolean onDrag(float displacement, float velocity) {
        float totalDisplacement = displacement + mDisplacementShift;
        boolean isGoingUp =
                totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0;
        if (isGoingUp != mCurrentAnimationIsGoingUp) {
            reinitAnimationController(isGoingUp);
        }
        mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier);
        return true;
    }

    @Override
    public void onDragEnd(float velocity, boolean fling) {
        final boolean goingToEnd;

        if (fling) {
            boolean goingUp = velocity < 0;
            if (!goingUp && !mSwipeDownEnabled) {
                goingToEnd = false;
            } else if (goingUp != mCurrentAnimationIsGoingUp) {
                // In case the fling is in opposite direction, make sure if is close enough
                // from the start position
                if (mCurrentAnimation.getProgressFraction()
                        >= ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS) {
                    // Not allowed
                    goingToEnd = false;
                } else {
                    reinitAnimationController(goingUp);
                    goingToEnd = true;
                }
            } else {
                goingToEnd = true;
            }
        } else {
            goingToEnd = mCurrentAnimation.getProgressFraction() > SUCCESS_TRANSITION_PROGRESS;
        }

        float progress = mCurrentAnimation.getProgressFraction();
        long animationDuration = SwipeDetector.calculateDuration(
                velocity, goingToEnd ? (1 - progress) : progress);

        float nextFrameProgress = Utilities.boundToRange(
                progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f);


        mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd));

        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
        anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
        anim.setDuration(animationDuration);
        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
        anim.start();
    }

    private void onCurrentAnimationEnd(boolean wasSuccess) {
        // TODO: Might be a good time to log something.
        if (mTaskBeingDragged == null) {
            LauncherState state = wasSuccess ?
                    (mCurrentAnimationIsGoingUp ? ALL_APPS : NORMAL) : OVERVIEW;
            mLauncher.getStateManager().goToState(state, false);
        } else if (wasSuccess) {
            if (mCurrentAnimationIsGoingUp) {
                mRecentsView.onTaskDismissed(mTaskBeingDragged);
            } else {
                mTaskBeingDragged.launchTask(false);
            }
        }
        mDetector.finishedScrolling();
        mTaskBeingDragged = null;
        mCurrentAnimation = null;
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -41,11 +41,11 @@ public class UiFactory {
            return new TouchController[]{
                    new EdgeSwipeController(launcher),
                    new TwoStepSwipeController(launcher),
                    new OverviewSwipeUpController(launcher)};
                    new OverviewSwipeController(launcher)};
        } else {
            return new TouchController[]{
                    new TwoStepSwipeController(launcher),
                    new OverviewSwipeUpController(launcher)};
                    new OverviewSwipeController(launcher)};
        }
    }

+4 −129
Original line number Diff line number Diff line
@@ -16,29 +16,17 @@

package com.android.quickstep;

import static com.android.quickstep.RecentsView.SCROLL_TYPE_TASK;
import static com.android.quickstep.RecentsView.SCROLL_TYPE_WORKSPACE;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.app.ActivityOptions;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.touch.SwipeDetector;
import com.android.quickstep.RecentsView.PageCallbacks;
import com.android.quickstep.RecentsView.ScrollState;
import com.android.systemui.shared.recents.model.Task;
@@ -52,11 +40,13 @@ import com.android.systemui.shared.system.ActivityManagerWrapper;
import java.util.ArrayList;
import java.util.List;

import static com.android.quickstep.RecentsView.SCROLL_TYPE_TASK;
import static com.android.quickstep.RecentsView.SCROLL_TYPE_WORKSPACE;

/**
 * A task in the Recents view.
 */
public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetector.Listener,
        PageCallbacks {
public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks {

    /** Designates how "curvy" the carousel is from 0 to 1, where 0 is a straight line. */
    private static final float CURVE_FACTOR = 0.25f;
@@ -70,30 +60,8 @@ public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetecto
     */
    private static final float MAX_PAGE_SCRIM_ALPHA = 0.8f;

    private static final int SWIPE_DIRECTIONS = SwipeDetector.DIRECTION_POSITIVE;

    /**
     * The task will appear fully dismissed when the distance swiped
     * reaches this percentage of the card height.
     */
    private static final float SWIPE_DISTANCE_HEIGHT_PERCENTAGE = 0.38f;

    private static final long SCALE_ICON_DURATION = 120;

    private static final Property<TaskView, Float> PROPERTY_SWIPE_PROGRESS =
            new Property<TaskView, Float>(Float.class, "swipe_progress") {

                @Override
                public Float get(TaskView taskView) {
                    return taskView.mSwipeProgress;
                }

                @Override
                public void set(TaskView taskView, Float progress) {
                    taskView.setSwipeProgress(progress);
                }
            };

    private static final Property<TaskView, Float> SCALE_ICON_PROPERTY =
            new Property<TaskView, Float>(Float.TYPE, "scale_icon") {
                @Override
@@ -110,11 +78,6 @@ public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetecto
    private Task mTask;
    private TaskThumbnailView mSnapshotView;
    private ImageView mIconView;
    private SwipeDetector mSwipeDetector;
    private float mSwipeDistance;
    private float mSwipeProgress;
    private Interpolator mAlphaInterpolator;
    private Interpolator mSwipeAnimInterpolator;
    private float mIconScale = 1f;

    public TaskView(Context context) {
@@ -130,11 +93,6 @@ public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetecto
        setOnClickListener((view) -> {
            launchTask(true /* animate */);
        });

        mSwipeDetector = new SwipeDetector(getContext(), this, SwipeDetector.VERTICAL);
        mSwipeDetector.setDetectableScrollConditions(SWIPE_DIRECTIONS, false);
        mAlphaInterpolator = Interpolators.ACCEL_1_5;
        mSwipeAnimInterpolator = Interpolators.SCROLL_CUBIC;
    }

    @Override
@@ -144,15 +102,6 @@ public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetecto
        mIconView = findViewById(R.id.icon);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        View p = (View) getParent();
        mSwipeDistance = (getMeasuredHeight() - p.getPaddingTop() - p.getPaddingBottom())
                * SWIPE_DISTANCE_HEIGHT_PERCENTAGE;
    }

    /**
     * Updates this task view to the given {@param task}.
     */
@@ -223,80 +172,6 @@ public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetecto
        // Do nothing
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        mSwipeDetector.onTouchEvent(ev);
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mSwipeDetector.onTouchEvent(event);
        return mSwipeDetector.isDraggingOrSettling() || super.onTouchEvent(event);
    }

    // Swipe detector methods

    @Override
    public void onDragStart(boolean start) {
        getParent().requestDisallowInterceptTouchEvent(true);
    }

    @Override
    public boolean onDrag(float displacement, float velocity) {
        setSwipeProgress(Utilities.boundToRange(displacement / mSwipeDistance,
                allowsSwipeUp() ? -1 : 0, allowsSwipeDown() ? 1 : 0));
        return true;
    }

    /**
     * Indicates the page is being removed.
     * @param progress Ranges from -1 (fading upwards) to 1 (fading downwards).
     */
    private void setSwipeProgress(float progress) {
        mSwipeProgress = progress;
        float translationY = mSwipeProgress * mSwipeDistance;
        float alpha = 1f - mAlphaInterpolator.getInterpolation(Math.abs(mSwipeProgress));
        // Only change children to avoid changing our properties while dragging.
        mIconView.setTranslationY(translationY);
        mSnapshotView.setTranslationY(translationY);
        mIconView.setAlpha(alpha);
        mSnapshotView.setAlpha(alpha);
    }

    private boolean allowsSwipeUp() {
        return (SWIPE_DIRECTIONS & SwipeDetector.DIRECTION_POSITIVE) != 0;
    }

    private boolean allowsSwipeDown() {
        return (SWIPE_DIRECTIONS & SwipeDetector.DIRECTION_NEGATIVE) != 0;
    }

    @Override
    public void onDragEnd(float velocity, boolean fling) {
        boolean movingAwayFromCenter = velocity < 0 == mSwipeProgress < 0;
        boolean flingAway = fling && movingAwayFromCenter
                && (allowsSwipeUp() && velocity < 0 || allowsSwipeDown() && velocity > 0);
        final boolean shouldRemove = flingAway || (!fling && Math.abs(mSwipeProgress) > 0.5f);
        float fromProgress = mSwipeProgress;
        float toProgress = !shouldRemove ? 0f : mSwipeProgress < 0 ? -1f : 1f;
        ValueAnimator swipeAnimator = ObjectAnimator.ofFloat(this, PROPERTY_SWIPE_PROGRESS,
                fromProgress, toProgress);
        swipeAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (shouldRemove) {
                    ((RecentsView) getParent()).onTaskDismissed(TaskView.this);
                }
                mSwipeDetector.finishedScrolling();
            }
        });
        swipeAnimator.setDuration(SwipeDetector.calculateDuration(velocity,
                Math.abs(toProgress - fromProgress)));
        swipeAnimator.setInterpolator(mSwipeAnimInterpolator);
        swipeAnimator.start();
    }

    public void animateIconToScale(float scale) {
        ObjectAnimator.ofFloat(this, SCALE_ICON_PROPERTY, scale)
                .setDuration(SCALE_ICON_DURATION).start();
+6 −0
Original line number Diff line number Diff line
@@ -33,6 +33,12 @@ import java.util.List;
 */
public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {

    /**
     * Creates an animation controller for the provided animation.
     * The actual duration does not matter as the animation is manually controlled. It just
     * needs to be larger than the total number of pixels so that we don't have jittering due
     * to float (animation-fraction * total duration) to int conversion.
     */
    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {

        /**
+10 −0
Original line number Diff line number Diff line
@@ -286,6 +286,16 @@ public class SwipeDetector {
        }
    }

    /**
     * Returns if the start drag was towards the positive direction or negative.
     *
     * @see #setDetectableScrollConditions(int, boolean)
     * @see #DIRECTION_BOTH
     */
    public boolean wasInitialTouchPositive() {
        return mSubtractDisplacement < 0;
    }

    private boolean reportDragging() {
        if (mDisplacement != mLastDisplacement) {
            if (DBG) {