diff options
2 files changed, 281 insertions, 46 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 1fbaeeac8608..29936cc2cac3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -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 @@ -109,6 +123,14 @@ class DividerPresenter implements View.OnTouchListener { 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 * is not available. @@ -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,24 +507,27 @@ class DividerPresenter implements View.OnTouchListener { @Override public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) { synchronized (mLock) { - final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); - mDividerPosition = calculateDividerPosition( - event, taskBounds, mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, - mProperties.mIsVerticalSplit, calculateMinPosition(), calculateMaxPosition()); - mRenderer.setDividerPosition(mDividerPosition); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - onStartDragging(); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - onFinishDragging(); - break; - case MotionEvent.ACTION_MOVE: - onDrag(); - break; - default: - break; + if (mProperties != null && mRenderer != null) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + mDividerPosition = calculateDividerPosition( + event, taskBounds, mRenderer.mDividerWidthPx, + mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, + calculateMinPosition(), calculateMaxPosition()); + mRenderer.setDividerPosition(mDividerPosition); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onStartDragging(event); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onFinishDragging(event); + break; + case MotionEvent.ACTION_MOVE: + onDrag(event); + break; + default: + break; + } } } @@ -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) { - return 0; + 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 && velocity > minDismissVelocityPxPerSecond) { + return fullyExpandedPosition; + } + if (Math.abs(velocity) < minFlingVelocityPxPerSecond) { + if (dividerPosition >= minPosition && dividerPosition <= maxPosition) { + return dividerPosition; } - if (dividerPosition > maxPosition) { - return fullyExpandedPosition; + int[] possiblePositions = {0, minPosition, maxPosition, fullyExpandedPosition}; + return snap(dividerPosition, possiblePositions); + } + 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 Math.clamp(dividerPosition, minPosition, maxPosition); + return snappedPosition; } private static void setDecorSurfaceBoosted( diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index b0a45e285896..746607c8094c 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -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); |