Browse Source

push pop android animations

Daniel Zlotin 7 years ago
parent
commit
da693c4dad

+ 50
- 26
lib/android/app/src/main/java/com/reactnativenavigation/anim/StackAnimator.java View File

@@ -1,33 +1,57 @@
1 1
 package com.reactnativenavigation.anim;
2 2
 
3
-import android.animation.Animator;
4
-import android.animation.AnimatorListenerAdapter;
5
-import android.animation.AnimatorSet;
6
-import android.animation.ObjectAnimator;
3
+import android.content.Context;
4
+import android.content.res.TypedArray;
5
+import android.support.annotation.Nullable;
7 6
 import android.view.View;
8
-import android.view.animation.DecelerateInterpolator;
7
+import android.view.animation.Animation;
8
+import android.view.animation.AnimationUtils;
9 9
 
10
+@SuppressWarnings("ResourceType")
10 11
 public class StackAnimator {
11
-	public void animatePush(final View target, final Runnable onComplete) {
12
-		target.setAlpha(0);
13
-		target.setTranslationY(0.08f * ((View) target.getParent()).getHeight());
14
-
15
-		ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 1);
16
-		alpha.setInterpolator(new DecelerateInterpolator());
17
-		alpha.setDuration(200);
18
-
19
-		ObjectAnimator translationY = ObjectAnimator.ofFloat(target, View.TRANSLATION_Y, 0);
20
-		translationY.setInterpolator(new DecelerateInterpolator());
21
-		translationY.setDuration(350);
22
-
23
-		AnimatorSet set = new AnimatorSet();
24
-		set.playTogether(translationY, alpha);
25
-		set.addListener(new AnimatorListenerAdapter() {
26
-			@Override
27
-			public void onAnimationEnd(final Animator animation) {
28
-				if (onComplete != null) onComplete.run();
29
-			}
30
-		});
31
-		set.start();
12
+
13
+	private static int androidOpenEnterAnimResId;
14
+	private static int androidOpenExitAnimResId;
15
+	private static int androidCloseEnterAnimResId;
16
+	private static int androidCloseExitAnimResId;
17
+	private final Context context;
18
+
19
+	public StackAnimator(Context context) {
20
+		this.context = context;
21
+		loadResIfNeeded(context);
22
+	}
23
+
24
+	private void loadResIfNeeded(Context context) {
25
+		if (androidOpenEnterAnimResId > 0) return;
26
+
27
+		int[] attrs = {android.R.attr.activityOpenEnterAnimation, android.R.attr.activityOpenExitAnimation, android.R.attr.activityCloseEnterAnimation, android.R.attr.activityCloseExitAnimation};
28
+		TypedArray typedArray = context.obtainStyledAttributes(android.R.style.Animation_Activity, attrs);
29
+		androidOpenEnterAnimResId = typedArray.getResourceId(0, -1);
30
+		androidOpenExitAnimResId = typedArray.getResourceId(1, -1);
31
+		androidCloseEnterAnimResId = typedArray.getResourceId(2, -1);
32
+		androidCloseExitAnimResId = typedArray.getResourceId(3, -1);
33
+		typedArray.recycle();
34
+	}
35
+
36
+	public void animatePush(final View enteringView, final View exitingView, @Nullable final Runnable onComplete) {
37
+		Animation enterAnim = AnimationUtils.loadAnimation(context, androidOpenEnterAnimResId);
38
+		Animation exitAnim = AnimationUtils.loadAnimation(context, androidOpenExitAnimResId);
39
+
40
+		new ViewAnimationSetBuilder()
41
+				.withEndListener(onComplete)
42
+				.add(enteringView, enterAnim)
43
+				.add(exitingView, exitAnim)
44
+				.start();
45
+	}
46
+
47
+	public void animatePop(final View enteringView, final View exitingView, @Nullable final Runnable onComplete) {
48
+		Animation enterAnim = AnimationUtils.loadAnimation(context, androidCloseEnterAnimResId);
49
+		Animation exitAnim = AnimationUtils.loadAnimation(context, androidCloseExitAnimResId);
50
+
51
+		new ViewAnimationSetBuilder()
52
+				.withEndListener(onComplete)
53
+				.add(enteringView, enterAnim)
54
+				.add(exitingView, exitAnim)
55
+				.start();
32 56
 	}
33 57
 }

+ 60
- 0
lib/android/app/src/main/java/com/reactnativenavigation/anim/ViewAnimationSetBuilder.java View File

@@ -0,0 +1,60 @@
1
+package com.reactnativenavigation.anim;
2
+
3
+import android.view.View;
4
+import android.view.animation.Animation;
5
+
6
+import java.util.ArrayList;
7
+import java.util.List;
8
+
9
+public class ViewAnimationSetBuilder implements Animation.AnimationListener {
10
+
11
+	private Runnable onComplete;
12
+	private List<View> views = new ArrayList<>();
13
+	private List<Animation> pendingAnimations = new ArrayList<>();
14
+	private boolean started = false;
15
+
16
+	public ViewAnimationSetBuilder withEndListener(Runnable onComplete) {
17
+		this.onComplete = onComplete;
18
+		return this;
19
+	}
20
+
21
+	public ViewAnimationSetBuilder add(View view, Animation animation) {
22
+		views.add(view);
23
+		pendingAnimations.add(animation);
24
+		animation.setAnimationListener(this);
25
+		view.setAnimation(animation);
26
+		return this;
27
+	}
28
+
29
+	public void start() {
30
+		for (Animation animation : pendingAnimations) {
31
+			animation.start();
32
+		}
33
+		started = true;
34
+		if (pendingAnimations.isEmpty()) finish();
35
+	}
36
+
37
+	@Override
38
+	public void onAnimationStart(final Animation animation) {
39
+		// nothing
40
+	}
41
+
42
+	@Override
43
+	public void onAnimationEnd(final Animation animation) {
44
+		pendingAnimations.remove(animation);
45
+		if (started && pendingAnimations.isEmpty()) finish();
46
+	}
47
+
48
+	@Override
49
+	public void onAnimationRepeat(final Animation animation) {
50
+		// nothing
51
+	}
52
+
53
+	private void finish() {
54
+		for (View view : views) {
55
+			view.clearAnimation();
56
+		}
57
+		views.clear();
58
+		if (onComplete != null) onComplete.run();
59
+	}
60
+}

+ 14
- 9
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java View File

@@ -15,7 +15,7 @@ public class StackController extends ParentController {
15 15
 	private StackAnimator animator;
16 16
 
17 17
 	public StackController(final Activity activity, String id) {
18
-		this(activity, id, new StackAnimator());
18
+		this(activity, id, new StackAnimator(activity));
19 19
 	}
20 20
 
21 21
 	public StackController(final Activity activity, String id, StackAnimator animator) {
@@ -32,7 +32,7 @@ public class StackController extends ParentController {
32 32
 		getView().addView(child.getView());
33 33
 
34 34
 		if (previousTop != null) {
35
-			animator.animatePush(child.getView(), new Runnable() {
35
+			animator.animatePush(child.getView(), previousTop.getView(), new Runnable() {
36 36
 				@Override
37 37
 				public void run() {
38 38
 					getView().removeView(previousTop.getView());
@@ -46,14 +46,19 @@ public class StackController extends ParentController {
46 46
 	}
47 47
 
48 48
 	public void pop() {
49
-		if (!canPop()) {
50
-			return;
51
-		}
52
-		ViewController poppedController = stack.pop();
53
-		getView().removeView(poppedController.getView());
49
+		if (!canPop()) return;
50
+
51
+		final ViewController poppedTop = stack.pop();
52
+		ViewController newTop = peek();
53
+
54
+		getView().addView(newTop.getView());
54 55
 
55
-		ViewController previousTop = peek();
56
-		getView().addView(previousTop.getView());
56
+		animator.animatePop(newTop.getView(), poppedTop.getView(), new Runnable() {
57
+			@Override
58
+			public void run() {
59
+				getView().removeView(poppedTop.getView());
60
+			}
61
+		});
57 62
 	}
58 63
 
59 64
 	public void popSpecific(final ViewController childController) {

+ 85
- 0
lib/android/app/src/test/java/com/reactnativenavigation/anim/ViewAnimationSetBuilderTest.java View File

@@ -0,0 +1,85 @@
1
+package com.reactnativenavigation.anim;
2
+
3
+import android.view.View;
4
+import android.view.animation.AlphaAnimation;
5
+import android.view.animation.Animation;
6
+
7
+import com.reactnativenavigation.BaseTest;
8
+
9
+import org.junit.Test;
10
+import org.robolectric.shadow.api.Shadow;
11
+
12
+import static org.assertj.core.api.Java6Assertions.assertThat;
13
+import static org.mockito.Mockito.mock;
14
+import static org.mockito.Mockito.times;
15
+import static org.mockito.Mockito.verify;
16
+
17
+public class ViewAnimationSetBuilderTest extends BaseTest {
18
+
19
+	private Runnable mockListener;
20
+
21
+	@Override
22
+	public void beforeEach() {
23
+		super.beforeEach();
24
+		mockListener = mock(Runnable.class);
25
+	}
26
+
27
+	@Test
28
+	public void implementsViewAnimationListener() throws Exception {
29
+		assertThat(new ViewAnimationSetBuilder()).isInstanceOf(Animation.AnimationListener.class);
30
+	}
31
+
32
+	@Test
33
+	public void optionalCompletionListener() throws Exception {
34
+		new ViewAnimationSetBuilder()
35
+				.withEndListener(mockListener)
36
+				.add(someView(), someAnimation())
37
+				.start();
38
+		verify(mockListener, times(1)).run();
39
+	}
40
+
41
+	@Test
42
+	public void startsAllAnimations() throws Exception {
43
+		Animation anim1 = someAnimation();
44
+		Animation anim2 = someAnimation();
45
+		new ViewAnimationSetBuilder()
46
+				.withEndListener(mockListener)
47
+				.add(someView(), anim1)
48
+				.add(someView(), anim2)
49
+				.start();
50
+		assertThat(anim1.hasStarted()).isTrue();
51
+		assertThat(anim2.hasStarted()).isTrue();
52
+	}
53
+
54
+	@Test
55
+	public void callsEndListenerOnlyAfterAllAnimationsFinish() throws Exception {
56
+		Animation anim1 = someAnimation();
57
+		Animation anim2 = someAnimation();
58
+		ViewAnimationSetBuilder uut = new ViewAnimationSetBuilder();
59
+		uut.withEndListener(mockListener)
60
+				.add(someView(), anim1)
61
+				.add(someView(), anim2)
62
+				.start();
63
+		verify(mockListener, times(1)).run();
64
+	}
65
+
66
+	@Test
67
+	public void clearsAnimationFromViewsAfterFinished() throws Exception {
68
+		View v1 = someView();
69
+		View v2 = someView();
70
+		new ViewAnimationSetBuilder()
71
+				.withEndListener(mockListener)
72
+				.add(v1, someAnimation())
73
+				.start();
74
+		assertThat(v1.getAnimation()).isNull();
75
+		assertThat(v2.getAnimation()).isNull();
76
+	}
77
+
78
+	private Animation someAnimation() {
79
+		return Shadow.newInstanceOf(AlphaAnimation.class);
80
+	}
81
+
82
+	private View someView() {
83
+		return Shadow.newInstanceOf(View.class);
84
+	}
85
+}

+ 14
- 1
lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestStackAnimator.java View File

@@ -1,12 +1,25 @@
1 1
 package com.reactnativenavigation.mocks;
2 2
 
3
+import android.support.annotation.Nullable;
3 4
 import android.view.View;
4 5
 
5 6
 import com.reactnativenavigation.anim.StackAnimator;
6 7
 
8
+import org.robolectric.RuntimeEnvironment;
9
+
7 10
 public class TestStackAnimator extends StackAnimator {
11
+
12
+	public TestStackAnimator() {
13
+		super(RuntimeEnvironment.application);
14
+	}
15
+
16
+	@Override
17
+	public void animatePush(final View enteringView, final View exitingView, final Runnable onComplete) {
18
+		if (onComplete != null) onComplete.run();
19
+	}
20
+
8 21
 	@Override
9
-	public void animatePush(final View target, final Runnable onComplete) {
22
+	public void animatePop(final View enteringView, final View exitingView, @Nullable final Runnable onComplete) {
10 23
 		if (onComplete != null) onComplete.run();
11 24
 	}
12 25
 }

+ 2
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ParentControllerTest.java View File

@@ -7,6 +7,7 @@ import android.widget.FrameLayout;
7 7
 
8 8
 import com.reactnativenavigation.BaseTest;
9 9
 import com.reactnativenavigation.mocks.SimpleViewController;
10
+import com.reactnativenavigation.mocks.TestStackAnimator;
10 11
 
11 12
 import org.junit.Test;
12 13
 
@@ -66,7 +67,7 @@ public class ParentControllerTest extends BaseTest {
66 67
 
67 68
 	@Test
68 69
 	public void findControllerById_Recursive() throws Exception {
69
-		StackController stackController = new StackController(activity, "stack");
70
+		StackController stackController = new StackController(activity, "stack", new TestStackAnimator());
70 71
 		SimpleViewController child1 = new SimpleViewController(activity, "child1");
71 72
 		SimpleViewController child2 = new SimpleViewController(activity, "child2");
72 73
 		stackController.push(child1);