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

Commit 34064089 authored by Annie Lin's avatar Annie Lin
Browse files

Add snap algorithm and animation for AE interactive divider.

Demo: https://screencast.googleplex.com/cast/NTY5MzEyOTc1NDY3MzE1Mnw2ZjA1YTFhMy03Mw
Flag: None - subfeature of unreleased feature
Bug: 339705242
Test: atest DividerPresenterTest
Change-Id: Id65b4ccc45358b17161d6cfb7b10a4232bf51e6f
parent 0f12dc4c
Loading
Loading
Loading
Loading
+177 −46
Original line number Diff line number Diff line
@@ -33,7 +33,9 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;

import android.annotation.DimenRes;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityThread;
@@ -53,9 +55,11 @@ import android.view.Gravity;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.VelocityTracker;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.view.animation.PathInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.window.InputTransferToken;
@@ -97,6 +101,16 @@ class DividerPresenter implements View.OnTouchListener {
    @VisibleForTesting
    static final int DEFAULT_DIVIDER_WIDTH_DP = 24;

    @VisibleForTesting
    static final PathInterpolator FLING_ANIMATION_INTERPOLATOR =
            new PathInterpolator(0.4f, 0f, 0.2f, 1f);
    @VisibleForTesting
    static final int FLING_ANIMATION_DURATION = 250;
    @VisibleForTesting
    static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
    @VisibleForTesting
    static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;

    private final int mTaskId;

    @NonNull
@@ -108,6 +122,14 @@ class DividerPresenter implements View.OnTouchListener {
    @NonNull
    private final Executor mCallbackExecutor;

    /**
     * The VelocityTracker of the divider, used to track the dragging velocity. This field is
     * {@code null} until dragging starts.
     */
    @GuardedBy("mLock")
    @Nullable
    VelocityTracker mVelocityTracker;

    /**
     * The {@link Properties} of the divider. This field is {@code null} when no divider should be
     * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface
@@ -370,13 +392,11 @@ class DividerPresenter implements View.OnTouchListener {
                applicationContext.getResources().getDisplayMetrics());
    }

    private static int getDimensionDp(@DimenRes int resId) {
        final Context context = ActivityThread.currentActivityThread().getApplication();
        final int px = context.getResources().getDimensionPixelSize(resId);
        return (int) TypedValue.convertPixelsToDimension(
                COMPLEX_UNIT_DIP,
                px,
                context.getResources().getDisplayMetrics());
    private static float getDisplayDensity() {
        // TODO(b/329193115) support divider on secondary display
        final Context applicationContext =
                ActivityThread.currentActivityThread().getApplication();
        return applicationContext.getResources().getDisplayMetrics().density;
    }

    /**
@@ -487,26 +507,29 @@ class DividerPresenter implements View.OnTouchListener {
    @Override
    public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
        synchronized (mLock) {
            if (mProperties != null && mRenderer != null) {
                final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
                mDividerPosition = calculateDividerPosition(
                    event, taskBounds, mRenderer.mDividerWidthPx, mProperties.mDividerAttributes,
                    mProperties.mIsVerticalSplit, calculateMinPosition(), calculateMaxPosition());
                        event, taskBounds, mRenderer.mDividerWidthPx,
                        mProperties.mDividerAttributes, mProperties.mIsVerticalSplit,
                        calculateMinPosition(), calculateMaxPosition());
                mRenderer.setDividerPosition(mDividerPosition);
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                    onStartDragging();
                        onStartDragging(event);
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                    onFinishDragging();
                        onFinishDragging(event);
                        break;
                    case MotionEvent.ACTION_MOVE:
                    onDrag();
                        onDrag(event);
                        break;
                    default:
                        break;
                }
            }
        }

        // Returns true to prevent the default button click callback. The button pressed state is
        // set/unset when starting/finishing dragging.
@@ -514,7 +537,10 @@ class DividerPresenter implements View.OnTouchListener {
    }

    @GuardedBy("mLock")
    private void onStartDragging() {
    private void onStartDragging(@NonNull MotionEvent event) {
        mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(event);

        mRenderer.mIsDragging = true;
        mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging);
        mRenderer.updateSurface();
@@ -536,16 +562,81 @@ class DividerPresenter implements View.OnTouchListener {
    }

    @GuardedBy("mLock")
    private void onDrag() {
    private void onDrag(@NonNull MotionEvent event) {
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }
        mRenderer.updateSurface();
    }

    @GuardedBy("mLock")
    private void onFinishDragging() {
        mDividerPosition = adjustDividerPositionForSnapPoints(mDividerPosition);
        mRenderer.setDividerPosition(mDividerPosition);
    private void onFinishDragging(@NonNull MotionEvent event) {
        float velocity = 0.0f;
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
            mVelocityTracker.computeCurrentVelocity(1000 /* units */);
            velocity = mProperties.mIsVerticalSplit
                    ? mVelocityTracker.getXVelocity()
                    : mVelocityTracker.getYVelocity();
            mVelocityTracker.recycle();
        }

        final int prevDividerPosition = mDividerPosition;
        mDividerPosition = dividerPositionForSnapPoints(mDividerPosition, velocity);
        if (mDividerPosition != prevDividerPosition) {
            ValueAnimator animator = getFlingAnimator(prevDividerPosition, mDividerPosition);
            animator.start();
        } else {
            onDraggingEnd();
        }
    }

    @GuardedBy("mLock")
    @NonNull
    @VisibleForTesting
    ValueAnimator getFlingAnimator(int prevDividerPosition, int snappedDividerPosition) {
        final ValueAnimator animator =
                getValueAnimator(prevDividerPosition, snappedDividerPosition);
        animator.addUpdateListener(animation -> {
            synchronized (mLock) {
                updateDividerPosition((int) animation.getAnimatedValue());
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                synchronized (mLock) {
                    onDraggingEnd();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                synchronized (mLock) {
                    onDraggingEnd();
                }
            }
        });
        return animator;
    }

    @VisibleForTesting
    static ValueAnimator getValueAnimator(int prevDividerPosition, int snappedDividerPosition) {
        ValueAnimator animator = ValueAnimator
                .ofInt(prevDividerPosition, snappedDividerPosition)
                .setDuration(FLING_ANIMATION_DURATION);
        animator.setInterpolator(FLING_ANIMATION_INTERPOLATOR);
        return animator;
    }

    @GuardedBy("mLock")
    private void updateDividerPosition(int position) {
        mRenderer.setDividerPosition(position);
        mRenderer.updateSurface();
    }

    @GuardedBy("mLock")
    private void onDraggingEnd() {
        // Veil visibility change should be applied together with the surface boost transaction in
        // the wct.
        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
@@ -570,36 +661,76 @@ class DividerPresenter implements View.OnTouchListener {

    /**
     * Returns the divider position adjusted for the min max ratio and fullscreen expansion.
     *
     * If the dragging position is above the {@link DividerAttributes#getPrimaryMaxRatio()} or below
     * {@link DividerAttributes#getPrimaryMinRatio()} and
     * {@link DividerAttributes#isDraggingToFullscreenAllowed} is {@code true}, the system will
     * choose a snap algorithm to adjust the ending position to either fully expand one container or
     * move the divider back to the specified min/max ratio.
     *
     * TODO(b/327067596) implement snap algorithm
     *
     * The adjusted divider position is in the range of [minPosition, maxPosition] for a split, 0
     * for expanded right (bottom) container, or task width (height) minus the divider width for
     * expanded left (top) container.
     */
    @GuardedBy("mLock")
    private int adjustDividerPositionForSnapPoints(int dividerPosition) {
    private int dividerPositionForSnapPoints(int dividerPosition, float velocity) {
        final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
        final int minPosition = calculateMinPosition();
        final int maxPosition = calculateMaxPosition();
        final int fullyExpandedPosition = mProperties.mIsVerticalSplit
                ? taskBounds.right - mRenderer.mDividerWidthPx
                : taskBounds.bottom - mRenderer.mDividerWidthPx;

        if (isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)) {
            if (dividerPosition < minPosition) {
            final float displayDensity = getDisplayDensity();
            return dividerPositionWithDraggingToFullscreenAllowed(
                    dividerPosition,
                    minPosition,
                    maxPosition,
                    fullyExpandedPosition,
                    velocity,
                    displayDensity);
        }
        return Math.clamp(dividerPosition, minPosition, maxPosition);
    }

    /**
     * Returns the divider position given a set of position options. A snap algorithm is used to
     * adjust the ending position to either fully expand one container or move the divider back to
     * the specified min/max ratio depending on the dragging velocity.
     */
    @VisibleForTesting
    static int dividerPositionWithDraggingToFullscreenAllowed(int dividerPosition, int minPosition,
            int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity) {
        final float minDismissVelocityPxPerSecond =
                MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity;
        final float minFlingVelocityPxPerSecond =
                MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity;
        if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) {
            return 0;
        }
            if (dividerPosition > maxPosition) {
        if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) {
            return fullyExpandedPosition;
        }
        if (Math.abs(velocity) < minFlingVelocityPxPerSecond) {
            if (dividerPosition >= minPosition && dividerPosition <= maxPosition) {
                return dividerPosition;
            }
            int[] possiblePositions = {0, minPosition, maxPosition, fullyExpandedPosition};
            return snap(dividerPosition, possiblePositions);
        }
        return Math.clamp(dividerPosition, minPosition, maxPosition);
        if (velocity < 0) {
            return 0;
        } else {
            return fullyExpandedPosition;
        }
    }

    /** Calculates the snapped divider position based on the possible positions and distance. */
    private static int snap(int dividerPosition, int[] possiblePositions) {
        int snappedPosition = dividerPosition;
        float minDistance = Float.MAX_VALUE;
        for (int position : possiblePositions) {
            float distance = Math.abs(dividerPosition - position);
            if (distance < minDistance) {
                snappedPosition = position;
                minDistance = distance;
            }
        }
        return snappedPosition;
    }

    private static void setDecorSurfaceBoosted(
+104 −0
Original line number Diff line number Diff line
@@ -19,6 +19,10 @@ package androidx.window.extensions.embedding;
import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;

import static androidx.window.extensions.embedding.DividerPresenter.FLING_ANIMATION_DURATION;
import static androidx.window.extensions.embedding.DividerPresenter.FLING_ANIMATION_INTERPOLATOR;
import static androidx.window.extensions.embedding.DividerPresenter.MIN_DISMISS_VELOCITY_DP_PER_SECOND;
import static androidx.window.extensions.embedding.DividerPresenter.MIN_FLING_VELOCITY_DP_PER_SECOND;
import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
@@ -35,6 +39,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.res.Configuration;
import android.graphics.Color;
@@ -637,6 +642,105 @@ public class DividerPresenterTest {
                DividerPresenter.getContainerBackgroundColor(container, defaultColor));
    }

    @Test
    public void testGetValueAnimator() {
        ValueAnimator animator =
                DividerPresenter.getValueAnimator(
                        375 /* prevDividerPosition */,
                        500 /* snappedDividerPosition */);

        assertEquals(animator.getDuration(), FLING_ANIMATION_DURATION);
        assertEquals(animator.getInterpolator(), FLING_ANIMATION_INTERPOLATOR);
    }

    @Test
    public void testDividerPositionWithDraggingToFullscreenAllowed() {
        final float displayDensity = 600F;
        final float dismissVelocity = MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity + 10f;
        final float nonFlingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity - 10f;
        final float flingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity + 10f;

        // Divider position is less than minPosition and the velocity is enough to be dismissed
        assertEquals(
                0, // Closed position
                DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
                        10 /* dividerPosition */,
                        30 /* minPosition */,
                        900 /* maxPosition */,
                        1200 /* fullyExpandedPosition */,
                        -dismissVelocity,
                        displayDensity));

        // Divider position is greater than maxPosition and the velocity is enough to be dismissed
        assertEquals(
                1200, // Fully expanded position
                DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
                        1000 /* dividerPosition */,
                        30 /* minPosition */,
                        900 /* maxPosition */,
                        1200 /* fullyExpandedPosition */,
                        dismissVelocity,
                        displayDensity));

        // Divider position is returned when the velocity is not fast enough for fling and is in
        // between minPosition and maxPosition
        assertEquals(
                500, // dividerPosition is not snapped
                DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
                        500 /* dividerPosition */,
                        30 /* minPosition */,
                        900 /* maxPosition */,
                        1200 /* fullyExpandedPosition */,
                        nonFlingVelocity,
                        displayDensity));

        // Divider position is snapped when the velocity is not fast enough for fling and larger
        // than maxPosition
        assertEquals(
                900, // Closest position is maxPosition
                DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
                        950 /* dividerPosition */,
                        30 /* minPosition */,
                        900 /* maxPosition */,
                        1200 /* fullyExpandedPosition */,
                        nonFlingVelocity,
                        displayDensity));

        // Divider position is snapped when the velocity is not fast enough for fling and smaller
        // than minPosition
        assertEquals(
                30, // Closest position is minPosition
                DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
                        20 /* dividerPosition */,
                        30 /* minPosition */,
                        900 /* maxPosition */,
                        1200 /* fullyExpandedPosition */,
                        nonFlingVelocity,
                        displayDensity));

        // Divider position is greater than minPosition and the velocity is enough for fling
        assertEquals(
                0, // Closed position
                DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
                        50 /* dividerPosition */,
                        30 /* minPosition */,
                        900 /* maxPosition */,
                        1200 /* fullyExpandedPosition */,
                        -flingVelocity,
                        displayDensity));

        // Divider position is less than maxPosition and the velocity is enough for fling
        assertEquals(
                1200, // Fully expanded position
                DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
                        800 /* dividerPosition */,
                        30 /* minPosition */,
                        900 /* maxPosition */,
                        1200 /* fullyExpandedPosition */,
                        flingVelocity,
                        displayDensity));
    }

    private TaskFragmentContainer createMockTaskFragmentContainer(
            @NonNull IBinder token, @NonNull Rect bounds) {
        final TaskFragmentContainer container = mock(TaskFragmentContainer.class);