Browse Source

Cancel in-flight push animation on pop (#4842)

When popToRoot or popTo where called while a screen was pushed, at the end of the push animation RNN tried to destroy an already destroyed ViewController.
Related to #3767
Guy Carmeli 5 years ago
parent
commit
47b7d2c7c5
No account linked to committer's email address

+ 16
- 1
lib/android/app/src/main/java/com/reactnativenavigation/anim/NavigationAnimator.java View File

@@ -4,6 +4,7 @@ import android.animation.Animator;
4 4
 import android.animation.AnimatorListenerAdapter;
5 5
 import android.animation.AnimatorSet;
6 6
 import android.content.Context;
7
+import android.support.annotation.RestrictTo;
7 8
 import android.view.View;
8 9
 import android.view.ViewGroup;
9 10
 
@@ -19,7 +20,7 @@ import java.util.HashMap;
19 20
 import java.util.List;
20 21
 import java.util.Map;
21 22
 
22
-import static com.reactnativenavigation.utils.CollectionUtils.merge;
23
+import static com.reactnativenavigation.utils.CollectionUtils.*;
23 24
 
24 25
 @SuppressWarnings("ResourceType")
25 26
 public class NavigationAnimator extends BaseAnimator {
@@ -108,4 +109,18 @@ public class NavigationAnimator extends BaseAnimator {
108 109
         });
109 110
         set.start();
110 111
     }
112
+
113
+    public void cancelPushAnimations() {
114
+        for (View view : runningPushAnimations.keySet()) {
115
+            runningPushAnimations.get(view).cancel();
116
+            runningPushAnimations.remove(view);
117
+        }
118
+    }
119
+
120
+    @RestrictTo(RestrictTo.Scope.TESTS)
121
+    public void endPushAnimation(View view) {
122
+        if (runningPushAnimations.containsKey(view)) {
123
+            runningPushAnimations.get(view).end();
124
+        }
125
+    }
111 126
 }

+ 2
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java View File

@@ -275,7 +275,7 @@ public class StackController extends ParentController<StackLayout> {
275 275
             return;
276 276
         }
277 277
 
278
-
278
+        animator.cancelPushAnimations();
279 279
         String currentControlId;
280 280
         for (int i = stack.size() - 2; i >= 0; i--) {
281 281
             currentControlId = stack.get(i).getId();
@@ -297,6 +297,7 @@ public class StackController extends ParentController<StackLayout> {
297 297
             return;
298 298
         }
299 299
 
300
+        animator.cancelPushAnimations();
300 301
         Iterator<String> iterator = stack.iterator();
301 302
         iterator.next();
302 303
         while (stack.size() > 2) {

+ 39
- 3
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java View File

@@ -42,9 +42,7 @@ import org.assertj.core.api.iterable.Extractor;
42 42
 import org.json.JSONException;
43 43
 import org.json.JSONObject;
44 44
 import org.junit.Test;
45
-import org.mockito.ArgumentCaptor;
46
-import org.mockito.InOrder;
47
-import org.mockito.Mockito;
45
+import org.mockito.*;
48 46
 
49 47
 import java.util.ArrayList;
50 48
 import java.util.Arrays;
@@ -297,6 +295,18 @@ public class StackControllerTest extends BaseTest {
297 295
         });
298 296
     }
299 297
 
298
+    @Test
299
+    public void pop_screenCurrentlyBeingPushedIsPopped() {
300
+        disablePushAnimation(child1, child2);
301
+        uut.push(child1, Mockito.mock(CommandListenerAdapter.class));
302
+        uut.push(child2, Mockito.mock(CommandListenerAdapter.class));
303
+
304
+        uut.push(child3, Mockito.mock(CommandListenerAdapter.class));
305
+        uut.pop(Options.EMPTY, Mockito.mock(CommandListenerAdapter.class));
306
+        assertThat(uut.size()).isEqualTo(2);
307
+        assertContainsOnlyId(child1.getId(), child2.getId());
308
+    }
309
+
300 310
     @Test
301 311
     public void pop_appliesOptionsAfterPop() {
302 312
         uut.push(child1, new CommandListenerAdapter());
@@ -622,6 +632,19 @@ public class StackControllerTest extends BaseTest {
622 632
         });
623 633
     }
624 634
 
635
+    @Test
636
+    public void popTo_pushAnimationIsCancelled() {
637
+        disablePushAnimation(child1, child2);
638
+        uut.push(child1, Mockito.mock(CommandListenerAdapter.class));
639
+        uut.push(child2, Mockito.mock(CommandListenerAdapter.class));
640
+
641
+        ViewGroup pushed = child3.getView();
642
+        uut.push(child3, Mockito.mock(CommandListenerAdapter.class));
643
+        uut.popTo(child1, Options.EMPTY, Mockito.mock(CommandListenerAdapter.class));
644
+        animator.endPushAnimation(pushed);
645
+        assertContainsOnlyId(child1.getId());
646
+    }
647
+
625 648
     @Test
626 649
     public void popToRoot_PopsEverythingAboveFirstController() {
627 650
         child1.options.animations.push.enabled = new Bool(false);
@@ -707,6 +730,19 @@ public class StackControllerTest extends BaseTest {
707 730
         verify(child1, times(0)).mergeOptions(mergeOptions);
708 731
     }
709 732
 
733
+    @Test
734
+    public void popToRoot_screenPushedBeforePopAnimationCompletesIsPopped() {
735
+        disablePushAnimation(child1, child2);
736
+        uut.push(child1, Mockito.mock(CommandListenerAdapter.class));
737
+        uut.push(child2, Mockito.mock(CommandListenerAdapter.class));
738
+
739
+        ViewGroup pushed = child3.getView();
740
+        uut.push(child3, Mockito.mock(CommandListenerAdapter.class));
741
+        uut.popToRoot(Options.EMPTY, Mockito.mock(CommandListenerAdapter.class));
742
+        animator.endPushAnimation(pushed);
743
+        assertContainsOnlyId(child1.getId());
744
+    }
745
+
710 746
     @Test
711 747
     public void findControllerById_ReturnsSelfOrChildrenById() {
712 748
         assertThat(uut.findController("123")).isNull();