Let BackProgressAnimator could play cancel animation

Add cancel and the cancel callback in BackProgressAnimator so it could
animate to start position and invoke the callback after finished.

Bug: 259608500
Test: atest BackAnimationControllerTest BackNavigationControllerTests
Test: atest BackProgressAnimatorTest
Change-Id: I94303ba530d155f4b264dafa21bd23185a6b44bd
This commit is contained in:
Arthur Hung
2022-12-07 08:17:19 +00:00
parent 26af2c72fe
commit 0956f6e2fd
6 changed files with 144 additions and 12 deletions

View File

@@ -74,6 +74,9 @@ public final class BackEvent {
* and animation should seek to its end state. Exact end value may vary depending on
* screen size.
* </ol>
* <li> After the gesture finishes in cancel state, this method keeps getting invoked until the
* progress value animates back to 0.
* </ol>
* In-between locations are linearly interpolated based on horizontal distance from the starting
* edge and smooth clamped to 1 when the distance exceeds a system-wide threshold.
*/

View File

@@ -16,8 +16,10 @@
package android.window;
import android.annotation.NonNull;
import android.util.FloatProperty;
import com.android.internal.dynamicanimation.animation.DynamicAnimation;
import com.android.internal.dynamicanimation.animation.SpringAnimation;
import com.android.internal.dynamicanimation.animation.SpringForce;
@@ -126,6 +128,27 @@ public class BackProgressAnimator {
mProgress = 0;
}
/**
* Animate the back progress animation from current progress to start position.
* This should be called when back is cancelled.
*
* @param finishCallback the callback to be invoked when the progress is reach to 0.
*/
public void onBackCancelled(@NonNull Runnable finishCallback) {
final DynamicAnimation.OnAnimationEndListener listener =
new DynamicAnimation.OnAnimationEndListener() {
@Override
public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity) {
mSpring.removeEndListener(this);
finishCallback.run();
reset();
}
};
mSpring.addEndListener(listener);
mSpring.animateToFinalPosition(0);
}
private void updateProgressValue(float progress) {
if (mLastBackEvent == null || mCallback == null || !mStarted) {
return;

View File

@@ -266,11 +266,12 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher {
@Override
public void onBackCancelled() {
Handler.getMain().post(() -> {
mProgressAnimator.reset();
final OnBackAnimationCallback callback = getBackAnimationCallback();
if (callback != null) {
callback.onBackCancelled();
}
mProgressAnimator.onBackCancelled(() -> {
final OnBackAnimationCallback callback = getBackAnimationCallback();
if (callback != null) {
callback.onBackCancelled();
}
});
});
}

View File

@@ -171,6 +171,7 @@ class CrossActivityAnimation {
mInitialTouchPos.set(0, 0);
mEnteringWindowShow = false;
mEnteringMargin = 0;
mEnteringAnimator = null;
if (mFinishCallback != null) {
try {
@@ -276,7 +277,7 @@ class CrossActivityAnimation {
}
// End the fade in animation.
if (mEnteringAnimator.isRunning()) {
if (mEnteringAnimator != null && mEnteringAnimator.isRunning()) {
mEnteringAnimator.cancel();
}
@@ -329,12 +330,10 @@ class CrossActivityAnimation {
@Override
public void onBackCancelled() {
// End the fade in animation.
if (mEnteringAnimator.isRunning()) {
if (mEnteringAnimator != null && mEnteringAnimator.isRunning()) {
mEnteringAnimator.cancel();
}
// TODO (b259608500): Let BackProgressAnimator could play cancel animation.
mProgressAnimator.reset();
finishAnimation();
mProgressAnimator.onBackCancelled(CrossActivityAnimation.this::finishAnimation);
}
@Override

View File

@@ -329,8 +329,7 @@ class CrossTaskBackAnimation {
@Override
public void onBackCancelled() {
mProgressAnimator.reset();
finishAnimation();
mProgressAnimator.onBackCancelled(CrossTaskBackAnimation.this::finishAnimation);
}
@Override

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2022 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.wm.shell.back;
import static android.window.BackEvent.EDGE_LEFT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.os.Handler;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackProgressAnimator;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@SmallTest
@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner.class)
public class BackProgressAnimatorTest {
private BackProgressAnimator mProgressAnimator;
private BackEvent mReceivedBackEvent;
private float mTargetProgress = 0.5f;
private CountDownLatch mTargetProgressCalled = new CountDownLatch(1);
private Handler mMainThreadHandler;
@Before
public void setUp() throws Exception {
mMainThreadHandler = new Handler(Looper.getMainLooper());
final BackMotionEvent backEvent = new BackMotionEvent(
0, 0,
0, EDGE_LEFT, null);
mMainThreadHandler.post(
() -> {
mProgressAnimator = new BackProgressAnimator();
mProgressAnimator.onBackStarted(backEvent, this::onGestureProgress);
});
}
@Test
public void testBackProgressed() throws InterruptedException {
final BackMotionEvent backEvent = new BackMotionEvent(
100, 0,
mTargetProgress, EDGE_LEFT, null);
mMainThreadHandler.post(
() -> mProgressAnimator.onBackProgressed(backEvent));
mTargetProgressCalled.await(1, TimeUnit.SECONDS);
assertNotNull(mReceivedBackEvent);
assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */);
}
@Test
public void testBackCancelled() throws InterruptedException {
// Give the animator some progress.
final BackMotionEvent backEvent = new BackMotionEvent(
100, 0,
mTargetProgress, EDGE_LEFT, null);
mMainThreadHandler.post(
() -> mProgressAnimator.onBackProgressed(backEvent));
mTargetProgressCalled.await(1, TimeUnit.SECONDS);
assertNotNull(mReceivedBackEvent);
// Trigger animation cancel, the target progress should be 0.
mTargetProgress = 0;
mTargetProgressCalled = new CountDownLatch(1);
CountDownLatch cancelCallbackCalled = new CountDownLatch(1);
mMainThreadHandler.post(
() -> mProgressAnimator.onBackCancelled(() -> cancelCallbackCalled.countDown()));
cancelCallbackCalled.await(1, TimeUnit.SECONDS);
mTargetProgressCalled.await(1, TimeUnit.SECONDS);
assertNotNull(mReceivedBackEvent);
assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */);
}
private void onGestureProgress(BackEvent backEvent) {
if (mTargetProgress == backEvent.getProgress()) {
mReceivedBackEvent = backEvent;
mTargetProgressCalled.countDown();
}
}
}