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

Commit 4da73c7a authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add snap algorithm and animation for AE interactive divider." into main

parents 14a94594 34064089
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);