Browse Source

Shared element transition (#994)

* Add view manager

* Don’t update screen style on ViewPagerScreenChangedEvent

* change version to legacy

* Fix headers after rebase

* version bump

* Add image to each list item

* Basics structure is done (I think)

toElement also animates into place, even though values are completely wrong.

* Stop relying on refs to resolve fromElements

* Hide only corresponding fromElement

* Animate sharedElements back into place on screen pop

* Update example project with Dota heroes

* Initial curved animation implementation

* Stop resolving sharedElement refs

* Move getLocationOnScreen to ViewUtils

* Parse control points passed from Js

* Scale animation works

Not sure about pivot coordinates yet.

* minor update to SharedElementsTransition screen

* Minor changes to example screens

* Draw shared elements on Screen

Ignore Z order and draw shared elements directly on screen

* stuff

* Start animation after views are drawn

* f

* Animate text color

* Less allocations when animating text color

* Switch from RGB to LAB 😎

* Save and restore SpannedString

* Use correct status bar height in Android M

* Pass show and hide duration from Js

* Fix flicker when show animation starts

* Make shared elements not clickable

motivation: might be related to some native crash

* Use linear interpolation as default interpolation

* Code cleanup

* Clear SharedElements refs when screen is destroyed

* Code cleanup

* Add decelerate interpolator

* No more occasional flicker on animation start (Hopefully 🙏)

* Add FastOutSlowIn interpolator

* Animate only visible shared elements

* Clip bounds animator somewhat works

* Hope fully no more flickery images

* Add interpolator to each animator

* some work on shared element screens

* rebase fix

* fix lint
Guy Carmeli 7 years ago
parent
commit
2fdb5a0681
57 changed files with 2345 additions and 19 deletions
  1. 4
    1
      android/app/src/main/java/com/reactnativenavigation/bridge/NavigationReactPackage.java
  2. 63
    0
      android/app/src/main/java/com/reactnativenavigation/params/InterpolationParams.java
  3. 11
    0
      android/app/src/main/java/com/reactnativenavigation/params/LinearInterpolationParams.java
  4. 20
    0
      android/app/src/main/java/com/reactnativenavigation/params/PathInterpolationParams.java
  5. 1
    0
      android/app/src/main/java/com/reactnativenavigation/params/ScreenParams.java
  6. 54
    0
      android/app/src/main/java/com/reactnativenavigation/params/parsers/InterpolationParser.java
  7. 24
    0
      android/app/src/main/java/com/reactnativenavigation/params/parsers/ScreenParamsParser.java
  8. 53
    0
      android/app/src/main/java/com/reactnativenavigation/params/parsers/SharedElementParamsParser.java
  9. 9
    0
      android/app/src/main/java/com/reactnativenavigation/params/parsers/SharedElementTransitionParams.java
  10. 13
    0
      android/app/src/main/java/com/reactnativenavigation/react/ReactViewHacks.java
  11. 52
    2
      android/app/src/main/java/com/reactnativenavigation/screens/Screen.java
  12. 12
    2
      android/app/src/main/java/com/reactnativenavigation/screens/ScreenAnimator.java
  13. 35
    6
      android/app/src/main/java/com/reactnativenavigation/screens/ScreenStack.java
  14. 15
    0
      android/app/src/main/java/com/reactnativenavigation/utils/ArrayUtils.java
  15. 41
    4
      android/app/src/main/java/com/reactnativenavigation/utils/ViewUtils.java
  16. 22
    0
      android/app/src/main/java/com/reactnativenavigation/utils/ViewVisibilityChecker.java
  17. 66
    0
      android/app/src/main/java/com/reactnativenavigation/views/managers/SharedElementTransitionManager.java
  18. 130
    0
      android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/AnimatorValuesResolver.java
  19. 13
    0
      android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/ControlPoint.java
  20. 57
    0
      android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/ReversedAnimatorValuesResolver.java
  21. 166
    0
      android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElementAnimatorCreator.java
  22. 169
    0
      android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElementTransition.java
  23. 143
    0
      android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElements.java
  24. 84
    0
      android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElementsAnimator.java
  25. 63
    0
      android/app/src/main/java/com/reactnativenavigation/views/utils/AnimatorPath.java
  26. 49
    0
      android/app/src/main/java/com/reactnativenavigation/views/utils/ClipBoundsEvaluator.java
  27. 13
    0
      android/app/src/main/java/com/reactnativenavigation/views/utils/ColorUtils.java
  28. 14
    0
      android/app/src/main/java/com/reactnativenavigation/views/utils/LabColorEvaluator.java
  29. 50
    0
      android/app/src/main/java/com/reactnativenavigation/views/utils/PathEvaluator.java
  30. 94
    0
      android/app/src/main/java/com/reactnativenavigation/views/utils/PathPoint.java
  31. 11
    0
      android/app/src/main/java/com/reactnativenavigation/views/utils/Point.java
  32. 1
    1
      example/android/app/proguard-rules.pro
  33. BIN
      example/img/beach.jpg
  34. BIN
      example/img/heroes/bouny_hunter.png
  35. BIN
      example/img/heroes/earthspirit.png
  36. BIN
      example/img/heroes/oracle.png
  37. BIN
      example/img/heroes/skywrath_mage.png
  38. BIN
      example/img/heroes/templar_assasin.png
  39. BIN
      example/img/list@1.5x.android.png
  40. BIN
      example/img/list@1x.android.png
  41. BIN
      example/img/list@2x.android.png
  42. BIN
      example/img/list@3x.android.png
  43. BIN
      example/img/list@4x.android.png
  44. 1
    0
      example/index.android.js
  45. 1
    0
      example/package.json
  46. 12
    0
      example/src/app.js
  47. 8
    0
      example/src/screens/index.android.js
  48. 118
    0
      example/src/screens/set/CardScreen.js
  49. 216
    0
      example/src/screens/set/HeroScreen.js
  50. 124
    0
      example/src/screens/set/InformationScreen.js
  51. 235
    0
      example/src/screens/set/ListScreen.js
  52. 34
    0
      example/src/screens/set/heroes.js
  53. 26
    0
      example/src/screens/set/styles.js
  54. 1
    1
      package.json
  55. 3
    1
      src/deprecated/indexDeprecated.android.js
  56. 2
    1
      src/deprecated/platformSpecificDeprecated.android.js
  57. 12
    0
      src/views/sharedElementTransition.js

+ 4
- 1
android/app/src/main/java/com/reactnativenavigation/bridge/NavigationReactPackage.java View File

@@ -5,6 +5,7 @@ import com.facebook.react.bridge.JavaScriptModule;
5 5
 import com.facebook.react.bridge.NativeModule;
6 6
 import com.facebook.react.bridge.ReactApplicationContext;
7 7
 import com.facebook.react.uimanager.ViewManager;
8
+import com.reactnativenavigation.views.managers.SharedElementTransitionManager;
8 9
 
9 10
 import java.util.Arrays;
10 11
 import java.util.Collections;
@@ -26,6 +27,8 @@ public class NavigationReactPackage implements ReactPackage {
26 27
 
27 28
     @Override
28 29
     public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
29
-        return Collections.emptyList();
30
+        return Arrays.<ViewManager>asList(
31
+                new SharedElementTransitionManager()
32
+        );
30 33
     }
31 34
 }

+ 63
- 0
android/app/src/main/java/com/reactnativenavigation/params/InterpolationParams.java View File

@@ -0,0 +1,63 @@
1
+package com.reactnativenavigation.params;
2
+
3
+import android.animation.TimeInterpolator;
4
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
5
+import android.view.animation.AccelerateDecelerateInterpolator;
6
+import android.view.animation.AccelerateInterpolator;
7
+import android.view.animation.DecelerateInterpolator;
8
+import android.view.animation.Interpolator;
9
+import android.view.animation.LinearInterpolator;
10
+
11
+public abstract class InterpolationParams {
12
+    public enum Type {
13
+        Path("path"), Linear("linear");
14
+        private String name;
15
+
16
+        Type(String name) {
17
+            this.name = name;
18
+        }
19
+
20
+        public static Type fromString(String name) {
21
+            for (Type type : values()) {
22
+                if (type.name.equals(name)) {
23
+                    return type;
24
+                }
25
+            }
26
+            return Linear;
27
+        }
28
+    }
29
+
30
+    public enum Easing {
31
+        AccelerateDecelerate("accelerateDecelerate", new AccelerateDecelerateInterpolator()),
32
+        Accelerate("accelerate", new AccelerateInterpolator()),
33
+        Decelerate("decelerate", new DecelerateInterpolator()),
34
+        FastOutSlowIn("FastOutSlowIn", new FastOutSlowInInterpolator()),
35
+        Linear("linear", new LinearInterpolator());
36
+
37
+        private String name;
38
+        private TimeInterpolator interpolator;
39
+
40
+        Easing(String name, TimeInterpolator interpolator) {
41
+            this.name = name;
42
+            this.interpolator = interpolator;
43
+        }
44
+
45
+        public static Easing fromString(String name) {
46
+            for (Easing easing : values()) {
47
+                if (easing.name.equals(name)) {
48
+                    return easing;
49
+                }
50
+            }
51
+            return Linear;
52
+        }
53
+
54
+        public TimeInterpolator getInterpolator() {
55
+            return interpolator;
56
+        }
57
+    }
58
+
59
+    public Type type;
60
+    public Easing easing;
61
+
62
+    public abstract Interpolator get();
63
+}

+ 11
- 0
android/app/src/main/java/com/reactnativenavigation/params/LinearInterpolationParams.java View File

@@ -0,0 +1,11 @@
1
+package com.reactnativenavigation.params;
2
+
3
+import android.view.animation.Interpolator;
4
+import android.view.animation.LinearInterpolator;
5
+
6
+public class LinearInterpolationParams extends InterpolationParams {
7
+    @Override
8
+    public Interpolator get() {
9
+        return new LinearInterpolator();
10
+    }
11
+}

+ 20
- 0
android/app/src/main/java/com/reactnativenavigation/params/PathInterpolationParams.java View File

@@ -0,0 +1,20 @@
1
+package com.reactnativenavigation.params;
2
+
3
+import android.annotation.TargetApi;
4
+import android.os.Build;
5
+import android.view.animation.Interpolator;
6
+
7
+import com.reactnativenavigation.views.sharedElementTransition.ControlPoint;
8
+
9
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
10
+public class PathInterpolationParams extends InterpolationParams {
11
+
12
+    public ControlPoint p1;
13
+    public ControlPoint p2;
14
+
15
+    @Override
16
+    public Interpolator get() {
17
+        // Not called
18
+        return null;
19
+    }
20
+}

+ 1
- 0
android/app/src/main/java/com/reactnativenavigation/params/ScreenParams.java View File

@@ -5,6 +5,7 @@ import java.util.List;
5 5
 public class ScreenParams extends BaseScreenParams {
6 6
     public String tabLabel;
7 7
     public List<PageParams> topTabParams;
8
+    public List<String> sharedElementsTransitions;
8 9
 
9 10
     public boolean hasTopTabs() {
10 11
         return topTabParams != null && !topTabParams.isEmpty();

+ 54
- 0
android/app/src/main/java/com/reactnativenavigation/params/parsers/InterpolationParser.java View File

@@ -0,0 +1,54 @@
1
+package com.reactnativenavigation.params.parsers;
2
+
3
+import android.os.Bundle;
4
+
5
+import com.reactnativenavigation.params.InterpolationParams;
6
+import com.reactnativenavigation.params.LinearInterpolationParams;
7
+import com.reactnativenavigation.params.PathInterpolationParams;
8
+import com.reactnativenavigation.views.sharedElementTransition.ControlPoint;
9
+
10
+class InterpolationParser extends Parser {
11
+    private Bundle params;
12
+
13
+    private static final float[] defaultShowControlPoints = new float[]{0.5f, 1, 0, 0.5f};
14
+    private static final float[] defaultHideControlPoints = new float[]{0.5f, 0, 1, 0.5f};
15
+
16
+    InterpolationParser(Bundle params) {
17
+        this.params = params;
18
+    }
19
+
20
+    InterpolationParams parseShowInterpolation() {
21
+        if (params.isEmpty()) {
22
+            return new LinearInterpolationParams();
23
+        }
24
+        return parse(params, defaultShowControlPoints);
25
+    }
26
+
27
+    InterpolationParams parseHideInterpolation() {
28
+        if (params.isEmpty()) {
29
+            return new LinearInterpolationParams();
30
+        }
31
+        return parse(params, defaultHideControlPoints);
32
+    }
33
+
34
+    private InterpolationParams parse(Bundle params, float[] defaultControlPoints) {
35
+        InterpolationParams.Type type = InterpolationParams.Type.fromString(params.getString("type"));
36
+        InterpolationParams result = InterpolationParams.Type.Path.equals(type) ?
37
+                parsePathInterpolation(params, defaultControlPoints) :
38
+                new LinearInterpolationParams();
39
+        result.easing = InterpolationParams.Easing.fromString(params.getString("easing"));
40
+        return result;
41
+    }
42
+
43
+    private InterpolationParams parsePathInterpolation(Bundle params, float[] defaultValues) {
44
+        PathInterpolationParams result = new PathInterpolationParams();
45
+        result.p1 = new ControlPoint(
46
+                Float.valueOf(params.getString("controlX1")), defaultValues[0],
47
+                Float.valueOf(params.getString("controlY1")), defaultValues[1]);
48
+        result.p2 = new ControlPoint(
49
+                Float.valueOf(params.getString("controlX2")), defaultValues[2],
50
+                Float.valueOf(params.getString("controlY2")), defaultValues[3]
51
+        );
52
+        return result;
53
+    }
54
+}

+ 24
- 0
android/app/src/main/java/com/reactnativenavigation/params/parsers/ScreenParamsParser.java View File

@@ -1,11 +1,14 @@
1 1
 package com.reactnativenavigation.params.parsers;
2 2
 
3
+import android.graphics.drawable.Drawable;
3 4
 import android.os.Bundle;
4 5
 
5 6
 import com.reactnativenavigation.params.NavigationParams;
6 7
 import com.reactnativenavigation.params.PageParams;
7 8
 import com.reactnativenavigation.params.ScreenParams;
9
+import com.reactnativenavigation.react.ImageLoader;
8 10
 
11
+import java.util.ArrayList;
9 12
 import java.util.List;
10 13
 
11 14
 public class ScreenParamsParser extends Parser {
@@ -47,10 +50,31 @@ public class ScreenParamsParser extends Parser {
47 50
         result.tabIcon = new TabIconParser(params).parse();
48 51
 
49 52
         result.animateScreenTransitions = new AnimationParser(params).parse();
53
+        result.sharedElementsTransitions = getSharedElementsTransitions(params);
50 54
 
51 55
         return result;
52 56
     }
53 57
 
58
+    private static List<String> getSharedElementsTransitions(Bundle params) {
59
+        Bundle sharedElements = params.getBundle("sharedElements");
60
+        if (sharedElements == null) {
61
+            return new ArrayList<>();
62
+        }
63
+        List<String> result = new ArrayList<>();
64
+        for (String key : sharedElements.keySet()) {
65
+            result.add(sharedElements.getString(key));
66
+        }
67
+        return result;
68
+    }
69
+
70
+    private static Drawable getTabIcon(Bundle params) {
71
+        Drawable tabIcon = null;
72
+        if (hasKey(params, "icon")) {
73
+            tabIcon = ImageLoader.loadImage(params.getString("icon"));
74
+        }
75
+        return tabIcon;
76
+    }
77
+
54 78
     private static String getTabLabel(Bundle params) {
55 79
         String tabLabel = null;
56 80
         if (hasKey(params, "label")) {

+ 53
- 0
android/app/src/main/java/com/reactnativenavigation/params/parsers/SharedElementParamsParser.java View File

@@ -0,0 +1,53 @@
1
+package com.reactnativenavigation.params.parsers;
2
+
3
+import android.os.Bundle;
4
+
5
+import com.facebook.react.bridge.ReadableMap;
6
+import com.reactnativenavigation.bridge.BundleConverter;
7
+
8
+public class SharedElementParamsParser {
9
+    private static final int DEFAULT_DURATION = 300;
10
+
11
+    private int showDuration = DEFAULT_DURATION;
12
+    private int hideDuration = DEFAULT_DURATION;
13
+    private Bundle showInterpolation = Bundle.EMPTY;
14
+    private Bundle hideInterpolation = Bundle.EMPTY;
15
+    public boolean animateClipBounds;
16
+
17
+    public void setDuration(int duration) {
18
+        showDuration = duration;
19
+        hideDuration = duration;
20
+    }
21
+
22
+    public void setShowDuration(int duration) {
23
+        showDuration = duration;
24
+    }
25
+
26
+    public void setHideDuration(int duration) {
27
+        hideDuration = duration;
28
+    }
29
+
30
+    public void setShowInterpolation(ReadableMap showInterpolation) {
31
+        this.showInterpolation = BundleConverter.toBundle(showInterpolation);
32
+    }
33
+
34
+    public void setHideInterpolation(ReadableMap hideInterpolation) {
35
+        this.hideInterpolation = BundleConverter.toBundle(hideInterpolation);
36
+    }
37
+
38
+    public SharedElementTransitionParams parseShowTransitionParams() {
39
+        SharedElementTransitionParams result = new SharedElementTransitionParams();
40
+        result.duration = showDuration;
41
+        result.interpolation = new InterpolationParser(showInterpolation).parseShowInterpolation();
42
+        result.animateClipBounds = animateClipBounds;
43
+        return result;
44
+    }
45
+
46
+    public SharedElementTransitionParams parseHideTransitionParams() {
47
+        SharedElementTransitionParams result = new SharedElementTransitionParams();
48
+        result.duration = hideDuration;
49
+        result.interpolation = new InterpolationParser(hideInterpolation).parseHideInterpolation();
50
+        result.animateClipBounds = animateClipBounds;
51
+        return result;
52
+    }
53
+}

+ 9
- 0
android/app/src/main/java/com/reactnativenavigation/params/parsers/SharedElementTransitionParams.java View File

@@ -0,0 +1,9 @@
1
+package com.reactnativenavigation.params.parsers;
2
+
3
+import com.reactnativenavigation.params.InterpolationParams;
4
+
5
+public class SharedElementTransitionParams {
6
+    public InterpolationParams interpolation;
7
+    public int duration;
8
+    public boolean animateClipBounds;
9
+}

+ 13
- 0
android/app/src/main/java/com/reactnativenavigation/react/ReactViewHacks.java View File

@@ -0,0 +1,13 @@
1
+package com.reactnativenavigation.react;
2
+
3
+import com.facebook.react.views.image.ReactImageView;
4
+import com.reactnativenavigation.utils.ReflectionUtils;
5
+
6
+public class ReactViewHacks {
7
+    // Hack to prevent Image flicker until https://github.com/facebook/react-native/issues/10194 is fixed
8
+    public static void disableReactImageViewRemoteImageFadeInAnimation(ReactImageView reactImageView) {
9
+        reactImageView.setFadeDuration(0);
10
+        ReflectionUtils.setField(reactImageView, "mIsDirty", true);
11
+        reactImageView.maybeUpdateView();
12
+    }
13
+}

+ 52
- 2
android/app/src/main/java/com/reactnativenavigation/screens/Screen.java View File

@@ -27,8 +27,11 @@ import com.reactnativenavigation.params.TitleBarLeftButtonParams;
27 27
 import com.reactnativenavigation.utils.ViewUtils;
28 28
 import com.reactnativenavigation.views.LeftButtonOnClickListener;
29 29
 import com.reactnativenavigation.views.TopBar;
30
+import com.reactnativenavigation.views.sharedElementTransition.SharedElementTransition;
31
+import com.reactnativenavigation.views.sharedElementTransition.SharedElements;
30 32
 
31 33
 import java.util.List;
34
+import java.util.Map;
32 35
 
33 36
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
34 37
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -46,6 +49,7 @@ public abstract class Screen extends RelativeLayout implements Subscriber {
46 49
     private VisibilityAnimator topBarVisibilityAnimator;
47 50
     private ScreenAnimator screenAnimator;
48 51
     protected final StyleParams styleParams;
52
+    public final SharedElements sharedElements;
49 53
 
50 54
     public Screen(AppCompatActivity activity, ScreenParams screenParams, LeftButtonOnClickListener leftButtonOnClickListener) {
51 55
         super(activity);
@@ -56,6 +60,11 @@ public abstract class Screen extends RelativeLayout implements Subscriber {
56 60
         screenAnimator = new ScreenAnimator(this);
57 61
         createViews();
58 62
         EventBus.instance.register(this);
63
+        sharedElements = new SharedElements();
64
+    }
65
+
66
+    public void registerSharedElement(SharedElementTransition toView, String key) {
67
+        sharedElements.addToElement(toView, key);
59 68
     }
60 69
 
61 70
     @Override
@@ -65,7 +74,6 @@ public abstract class Screen extends RelativeLayout implements Subscriber {
65 74
             topBar.onContextualMenuHidden();
66 75
         }
67 76
         if (ViewPagerScreenChangedEvent.TYPE.equals(event.getType()) && isShown() ) {
68
-            setStyle();
69 77
             topBar.dismissContextualMenu();
70 78
         }
71 79
     }
@@ -239,7 +247,48 @@ public abstract class Screen extends RelativeLayout implements Subscriber {
239 247
         screenAnimator.show(animated, onAnimationEnd);
240 248
     }
241 249
 
242
-    public void hide(boolean animated, Runnable onAnimatedEnd) {
250
+    public void showWithSharedElementsTransitions(Map<String, SharedElementTransition> fromElements, final Runnable onAnimationEnd) {
251
+        setStyle();
252
+        sharedElements.setFromElements(fromElements);
253
+        screenAnimator.showWithSharedElementsTransitions(onAnimationEnd);
254
+    }
255
+
256
+    public void hideWithSharedElementTransitions(Map<String, SharedElementTransition> toElements, final Runnable onAnimationEnd) {
257
+        sharedElements.setFromElements(sharedElements.getToElements());
258
+        sharedElements.setToElements(toElements);
259
+        screenAnimator.hideWithSharedElementsTransition(onAnimationEnd);
260
+    }
261
+
262
+    public void hide(Map<String, SharedElementTransition> sharedElements, Runnable onAnimationEnd) {
263
+        removeHiddenSharedElements();
264
+        if (hasVisibleSharedElements()) {
265
+            hideWithSharedElementTransitions(sharedElements, onAnimationEnd);
266
+        } else {
267
+            hide(false, onAnimationEnd);
268
+        }
269
+    }
270
+
271
+    public void animateHide(Map<String, SharedElementTransition> sharedElements, Runnable onAnimationEnd) {
272
+        removeHiddenSharedElements();
273
+        if (hasVisibleSharedElements()) {
274
+            hideWithSharedElementTransitions(sharedElements, onAnimationEnd);
275
+        } else {
276
+            hide(true, onAnimationEnd);
277
+        }
278
+    }
279
+
280
+    private boolean hasVisibleSharedElements() {
281
+        if (screenParams.sharedElementsTransitions.isEmpty()) {
282
+            return false;
283
+        }
284
+        return !sharedElements.getToElements().isEmpty();
285
+    }
286
+
287
+    public void removeHiddenSharedElements() {
288
+        sharedElements.removeHiddenElements();
289
+    }
290
+
291
+    private void hide(boolean animated, Runnable onAnimatedEnd) {
243 292
         NavigationApplication.instance.getEventEmitter().sendNavigatorEvent("willDisappear", screenParams.getNavigatorEventId());
244 293
         NavigationApplication.instance.getEventEmitter().sendNavigatorEvent("didDisappear", screenParams.getNavigatorEventId());
245 294
         screenAnimator.hide(animated, onAnimatedEnd);
@@ -257,5 +306,6 @@ public abstract class Screen extends RelativeLayout implements Subscriber {
257 306
     public void destroy() {
258 307
         unmountReactView();
259 308
         EventBus.instance.unregister(this);
309
+        sharedElements.destroy();
260 310
     }
261 311
 }

+ 12
- 2
android/app/src/main/java/com/reactnativenavigation/screens/ScreenAnimator.java View File

@@ -11,14 +11,15 @@ import android.view.animation.LinearInterpolator;
11 11
 
12 12
 import com.reactnativenavigation.NavigationApplication;
13 13
 import com.reactnativenavigation.utils.ViewUtils;
14
+import com.reactnativenavigation.views.sharedElementTransition.SharedElementsAnimator;
14 15
 
15 16
 import javax.annotation.Nullable;
16 17
 
17
-public class ScreenAnimator {
18
+class ScreenAnimator {
18 19
     private final float translationY;
19 20
     private Screen screen;
20 21
 
21
-    public ScreenAnimator(Screen screen) {
22
+    ScreenAnimator(Screen screen) {
22 23
         this.screen = screen;
23 24
         translationY = 0.08f * ViewUtils.getScreenHeight();
24 25
     }
@@ -96,4 +97,13 @@ public class ScreenAnimator {
96 97
         });
97 98
         return set;
98 99
     }
100
+
101
+    void showWithSharedElementsTransitions(Runnable onAnimationEnd) {
102
+        screen.setVisibility(View.VISIBLE);
103
+        new SharedElementsAnimator(this.screen.sharedElements).show(onAnimationEnd);
104
+    }
105
+
106
+    void hideWithSharedElementsTransition(Runnable onAnimationEnd) {
107
+        new SharedElementsAnimator(screen.sharedElements).hide(onAnimationEnd);
108
+    }
99 109
 }

+ 35
- 6
android/app/src/main/java/com/reactnativenavigation/screens/ScreenStack.java View File

@@ -100,7 +100,12 @@ public class ScreenStack {
100 100
         Screen nextScreen = ScreenFactory.create(activity, params, leftButtonOnClickListener);
101 101
         final Screen previousScreen = stack.peek();
102 102
         if (isStackVisible) {
103
-            pushScreenToVisibleStack(layoutParams, nextScreen, previousScreen);
103
+            if (nextScreen.screenParams.sharedElementsTransitions.isEmpty()) {
104
+                pushScreenToVisibleStack(layoutParams, nextScreen, previousScreen);
105
+            } else {
106
+//                pushScreenToVisibleStack(layoutParams, nextScreen, previousScreen);
107
+                pushScreenToVisibleStackWithSharedElementTransition(layoutParams, nextScreen, previousScreen);
108
+            }
104 109
         } else {
105 110
             pushScreenToInvisibleStack(layoutParams, nextScreen, previousScreen);
106 111
         }
@@ -133,6 +138,22 @@ public class ScreenStack {
133 138
         });
134 139
     }
135 140
 
141
+    private void pushScreenToVisibleStackWithSharedElementTransition(LayoutParams layoutParams, final Screen nextScreen, final Screen previousScreen) {
142
+        nextScreen.setVisibility(View.INVISIBLE);
143
+        nextScreen.setOnDisplayListener(new Screen.OnDisplayListener() {
144
+            @Override
145
+            public void onDisplay() {
146
+                nextScreen.showWithSharedElementsTransitions(previousScreen.sharedElements.getToElements(), new Runnable() {
147
+                    @Override
148
+                    public void run() {
149
+                        parent.removeView(previousScreen);
150
+                    }
151
+                });
152
+            }
153
+        });
154
+        addScreen(nextScreen, layoutParams);
155
+    }
156
+
136 157
     private void pushScreenToInvisibleStack(LayoutParams layoutParams, Screen nextScreen, Screen previousScreen) {
137 158
         nextScreen.setVisibility(View.INVISIBLE);
138 159
         addScreen(nextScreen, layoutParams);
@@ -179,16 +200,24 @@ public class ScreenStack {
179 200
     private void swapScreens(boolean animated, final Screen toRemove, Screen previous, OnScreenPop onScreenPop) {
180 201
         readdPrevious(previous);
181 202
         previous.setStyle();
182
-        toRemove.hide(animated, new Runnable() {
203
+        hideScreen(animated, toRemove, previous);
204
+        if (onScreenPop != null) {
205
+            onScreenPop.onScreenPopAnimationEnd();
206
+        }
207
+    }
208
+
209
+    private void hideScreen(boolean animated, final Screen toRemove, Screen previous) {
210
+        Runnable onAnimationEnd = new Runnable() {
183 211
             @Override
184 212
             public void run() {
185 213
                 toRemove.destroy();
186 214
                 parent.removeView(toRemove);
187 215
             }
188
-        });
189
-
190
-        if (onScreenPop != null) {
191
-            onScreenPop.onScreenPopAnimationEnd();
216
+        };
217
+        if (animated) {
218
+            toRemove.animateHide(previous.sharedElements.getToElements(), onAnimationEnd);
219
+        } else {
220
+            toRemove.hide(previous.sharedElements.getToElements(), onAnimationEnd);
192 221
         }
193 222
     }
194 223
 

+ 15
- 0
android/app/src/main/java/com/reactnativenavigation/utils/ArrayUtils.java View File

@@ -0,0 +1,15 @@
1
+package com.reactnativenavigation.utils;
2
+
3
+import java.util.Arrays;
4
+
5
+public class ArrayUtils {
6
+    public static float[] reverse(float[] array) {
7
+        float[] result = Arrays.copyOf(array, array.length);
8
+        for(int i = 0; i < result.length / 2; i++) {
9
+            float temp = result[i];
10
+            result[i] = result[result.length - i - 1];
11
+            result[result.length - i - 1] = temp;
12
+        }
13
+        return result;
14
+    }
15
+}

+ 41
- 4
android/app/src/main/java/com/reactnativenavigation/utils/ViewUtils.java View File

@@ -1,26 +1,34 @@
1 1
 package com.reactnativenavigation.utils;
2 2
 
3 3
 import android.content.Context;
4
+import android.content.res.Resources;
4 5
 import android.graphics.PorterDuff;
5 6
 import android.graphics.PorterDuffColorFilter;
6 7
 import android.graphics.drawable.Drawable;
7 8
 import android.os.Build;
8 9
 import android.support.annotation.Nullable;
10
+import android.text.SpannableString;
11
+import android.text.Spanned;
12
+import android.text.SpannedString;
13
+import android.text.style.ForegroundColorSpan;
9 14
 import android.util.DisplayMetrics;
10 15
 import android.view.View;
11 16
 import android.view.ViewGroup;
12 17
 import android.view.ViewParent;
13 18
 import android.view.ViewTreeObserver;
14 19
 import android.view.WindowManager;
20
+import android.widget.TextView;
15 21
 
16 22
 import com.reactnativenavigation.NavigationApplication;
17 23
 import com.reactnativenavigation.params.AppStyle;
18 24
 import com.reactnativenavigation.screens.Screen;
25
+import com.reactnativenavigation.views.utils.Point;
19 26
 
20 27
 import java.util.concurrent.atomic.AtomicInteger;
21 28
 
22 29
 public class ViewUtils {
23 30
     private static final AtomicInteger viewId = new AtomicInteger(1);
31
+    private static int statusBarHeight = -1;
24 32
 
25 33
     public static void runOnPreDraw(final View view, final Runnable task) {
26 34
         view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@@ -49,7 +57,7 @@ public class ViewUtils {
49 57
 
50 58
     public static float convertPixelToSp(float pixels) {
51 59
         float scaledDensity = NavigationApplication.instance.getResources().getDisplayMetrics().scaledDensity;
52
-        return pixels/scaledDensity;
60
+        return pixels / scaledDensity;
53 61
     }
54 62
 
55 63
     public static float convertSpToPixel(float pixels) {
@@ -91,11 +99,13 @@ public class ViewUtils {
91 99
     /**
92 100
      * Returns the first instance of clazz in root
93 101
      */
94
-    @Nullable public static <T> T findChildByClass(ViewGroup root, Class clazz) {
102
+    @Nullable
103
+    public static <T> T findChildByClass(ViewGroup root, Class clazz) {
95 104
         return findChildByClass(root, clazz, null);
96 105
     }
97 106
 
98
-    @Nullable public static <T> T findChildByClass(ViewGroup root, Class clazz, Matcher<T> matcher) {
107
+    @Nullable
108
+    public static <T> T findChildByClass(ViewGroup root, Class clazz, Matcher<T> matcher) {
99 109
         for (int i = 0; i < root.getChildCount(); i++) {
100 110
             View view = root.getChildAt(i);
101 111
             if (clazz.isAssignableFrom(view.getClass())) {
@@ -147,5 +157,32 @@ public class ViewUtils {
147 157
         }
148 158
         return findParentScreen(parent.getParent());
149 159
     }
150
-}
151 160
 
161
+    public static Point getLocationOnScreen(View view) {
162
+        int[] xy = new int[2];
163
+        view.getLocationOnScreen(xy);
164
+        xy[1] -= getStatusBarPixelHeight();
165
+        return new Point(xy[0], xy[1]);
166
+    }
167
+
168
+    private static int getStatusBarPixelHeight() {
169
+        if (statusBarHeight > 0) {
170
+            return statusBarHeight;
171
+        }
172
+        final Resources resources = NavigationApplication.instance.getResources();
173
+        final int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
174
+        statusBarHeight = resourceId > 0 ?
175
+                resources.getDimensionPixelSize(resourceId) :
176
+                (int) convertDpToPixel(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? 24 : 25);
177
+        return statusBarHeight;
178
+    }
179
+
180
+    public static ForegroundColorSpan[] getForegroundColorSpans(TextView view) {
181
+        SpannedString text = (SpannedString) view.getText();
182
+        return text.getSpans(0, text.length(), ForegroundColorSpan.class);
183
+    }
184
+
185
+    public static void setSpanColor(SpannableString span, int color) {
186
+        span.setSpan(new ForegroundColorSpan(color), 0, span.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
187
+    }
188
+}

+ 22
- 0
android/app/src/main/java/com/reactnativenavigation/utils/ViewVisibilityChecker.java View File

@@ -0,0 +1,22 @@
1
+package com.reactnativenavigation.utils;
2
+
3
+import android.view.View;
4
+
5
+import com.reactnativenavigation.views.ContentView;
6
+
7
+public class ViewVisibilityChecker {
8
+
9
+    public static boolean check(View view) {
10
+        final int top = getTopRelativeToContentView(view);
11
+        final int scrollYInScreen = getScrollYInScreen(view);
12
+        return top + view.getHeight() > scrollYInScreen;
13
+    }
14
+
15
+    private static int getTopRelativeToContentView(View view) {
16
+        return view instanceof ContentView ? 0 : view.getTop() + getTopRelativeToContentView((View) view.getParent());
17
+    }
18
+
19
+    private static int getScrollYInScreen(View view) {
20
+        return view instanceof ContentView ? 0 : view.getScrollY() + getScrollYInScreen((View) view.getParent());
21
+    }
22
+}

+ 66
- 0
android/app/src/main/java/com/reactnativenavigation/views/managers/SharedElementTransitionManager.java View File

@@ -0,0 +1,66 @@
1
+package com.reactnativenavigation.views.managers;
2
+
3
+import com.facebook.react.bridge.ReadableMap;
4
+import com.facebook.react.uimanager.ThemedReactContext;
5
+import com.facebook.react.uimanager.ViewGroupManager;
6
+import com.facebook.react.uimanager.annotations.ReactProp;
7
+import com.reactnativenavigation.views.sharedElementTransition.SharedElementTransition;
8
+
9
+public class SharedElementTransitionManager extends ViewGroupManager<SharedElementTransition> {
10
+
11
+    @Override
12
+    public String getName() {
13
+        return "SharedElementTransition";
14
+    }
15
+
16
+    @Override
17
+    protected SharedElementTransition createViewInstance(ThemedReactContext reactContext) {
18
+        return new SharedElementTransition(reactContext);
19
+    }
20
+
21
+    @ReactProp(name = "sharedElementId")
22
+    public void setSharedElementId(SharedElementTransition elementTransition, String key) {
23
+        elementTransition.registerSharedElementTransition(key);
24
+    }
25
+
26
+    @ReactProp(name = "duration")
27
+    public void setDuration(SharedElementTransition view, int duration) {
28
+        view.paramsParser.setDuration(duration);
29
+    }
30
+
31
+    @ReactProp(name = "hideDuration")
32
+    public void setHideDuration(SharedElementTransition view, int duration) {
33
+        view.paramsParser.setHideDuration(duration);
34
+    }
35
+
36
+    @ReactProp(name = "showDuration")
37
+    public void setShowDuration(SharedElementTransition view, int duration) {
38
+        view.paramsParser.setShowDuration(duration);
39
+    }
40
+
41
+    @ReactProp(name = "showInterpolation")
42
+    public void setShowInterpolation(SharedElementTransition view, ReadableMap interpolation) {
43
+        view.paramsParser.setShowInterpolation(interpolation);
44
+    }
45
+
46
+    @ReactProp(name = "hideInterpolation")
47
+    public void setHideInterpolation(SharedElementTransition view, ReadableMap interpolation) {
48
+        view.paramsParser.setHideInterpolation(interpolation);
49
+    }
50
+
51
+    @ReactProp(name = "animateClipBounds")
52
+    public void setAnimateClipBounds(SharedElementTransition view, boolean animateClipBounds) {
53
+        view.paramsParser.animateClipBounds = animateClipBounds;
54
+    }
55
+
56
+    @Override
57
+    protected void onAfterUpdateTransaction(SharedElementTransition view) {
58
+        view.showTransitionParams = view.paramsParser.parseShowTransitionParams();
59
+        view.hideTransitionParams = view.paramsParser.parseHideTransitionParams();
60
+    }
61
+
62
+    @Override
63
+    public boolean needsCustomLayoutForChildren() {
64
+        return true;
65
+    }
66
+}

+ 130
- 0
android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/AnimatorValuesResolver.java View File

@@ -0,0 +1,130 @@
1
+package com.reactnativenavigation.views.sharedElementTransition;
2
+
3
+import android.graphics.Rect;
4
+import android.widget.TextView;
5
+
6
+import com.reactnativenavigation.params.InterpolationParams;
7
+import com.reactnativenavigation.params.PathInterpolationParams;
8
+import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
9
+import com.reactnativenavigation.utils.ViewUtils;
10
+import com.reactnativenavigation.views.utils.Point;
11
+
12
+class AnimatorValuesResolver {
13
+
14
+    final Point fromXy;
15
+    final Point toXy;
16
+    final float startScaleX;
17
+    final float endScaleX;
18
+    final float startScaleY;
19
+    final float endScaleY;
20
+    int dx;
21
+    int dy;
22
+    int startX;
23
+    int startY;
24
+    int endX;
25
+    int endY;
26
+    float controlX1;
27
+    float controlY1;
28
+    float controlX2;
29
+    float controlY2;
30
+    int startColor;
31
+    int endColor;
32
+    Rect startDrawingRect = new Rect();
33
+    Rect endDrawingRect = new Rect();
34
+
35
+    AnimatorValuesResolver(SharedElementTransition from, SharedElementTransition to, SharedElementTransitionParams params) {
36
+        fromXy = calculateFromXY(from, to, params);
37
+        toXy = calculateToXY(to, from, params);
38
+        startScaleX = calculateStartScaleX(from, to);
39
+        endScaleX = calculateEndScaleX(from, to);
40
+        startScaleY = calculateStartScaleY(from, to);
41
+        endScaleY = calculateEndScaleY(from, to);
42
+        calculateColor(from, to);
43
+        calculate(params.interpolation);
44
+        calculateDrawingReacts(from, to);
45
+    }
46
+
47
+    private Point calculateFromXY(SharedElementTransition from, SharedElementTransition to, SharedElementTransitionParams params) {
48
+        Point loc = ViewUtils.getLocationOnScreen(from.getSharedView());
49
+        if (params.animateClipBounds) {
50
+            if (from.getHeight() != to.getHeight()) {
51
+                if (from.getHeight() < to.getHeight()) {
52
+                    loc.y -= (to.getHeight() - from.getHeight()) / 2;
53
+                }
54
+            }
55
+        }
56
+        return loc;
57
+    }
58
+
59
+    private Point calculateToXY(SharedElementTransition to, SharedElementTransition from, SharedElementTransitionParams params) {
60
+        Point loc = ViewUtils.getLocationOnScreen(to.getSharedView());
61
+        if (params.animateClipBounds) {
62
+            if (from.getHeight() != to.getHeight()) {
63
+                if (from.getHeight() > to.getHeight()) {
64
+                    loc.y -= (from.getHeight() - to.getHeight()) / 2;
65
+                }
66
+            }
67
+        }
68
+        return loc;
69
+    }
70
+
71
+
72
+    protected float calculateEndScaleY(SharedElementTransition from, SharedElementTransition to) {
73
+        return 1;
74
+    }
75
+
76
+    protected float calculateStartScaleY(SharedElementTransition from, SharedElementTransition to) {
77
+        return ((float) from.getHeight()) / to.getHeight();
78
+    }
79
+
80
+    protected float calculateEndScaleX(SharedElementTransition from, SharedElementTransition to) {
81
+        return 1;
82
+    }
83
+
84
+    protected float calculateStartScaleX(SharedElementTransition from, SharedElementTransition to) {
85
+        return ((float) from.getWidth()) / to.getWidth();
86
+    }
87
+
88
+    private void calculate(InterpolationParams interpolation) {
89
+        calculateDeltas();
90
+        calculateStartPoint();
91
+        calculateEndPoint();
92
+        if (interpolation instanceof PathInterpolationParams) {
93
+            calculateControlPoints((PathInterpolationParams) interpolation);
94
+        }
95
+    }
96
+
97
+    protected void calculateDeltas() {
98
+        dx = fromXy.x - toXy.x;
99
+        dy = fromXy.y - toXy.y;
100
+    }
101
+
102
+    protected void calculateEndPoint() {
103
+        endX = 0;
104
+        endY = 0;
105
+    }
106
+
107
+    protected void calculateStartPoint() {
108
+        startX = dx;
109
+        startY = dy;
110
+    }
111
+
112
+    protected void calculateControlPoints(PathInterpolationParams interpolation) {
113
+        controlX1 = dx * interpolation.p1.x;
114
+        controlY1 = dy * interpolation.p1.y;
115
+        controlX2 = dx * interpolation.p2.x;
116
+        controlY2 = dy * interpolation.p2.y;
117
+    }
118
+
119
+    private void calculateColor(SharedElementTransition from, SharedElementTransition to) {
120
+        if (from.getSharedView() instanceof TextView && to.getSharedView() instanceof TextView) {
121
+            startColor = ViewUtils.getForegroundColorSpans((TextView) from.getSharedView())[0].getForegroundColor();
122
+            endColor = ViewUtils.getForegroundColorSpans((TextView) to.getSharedView())[0].getForegroundColor();
123
+        }
124
+    }
125
+
126
+    private void calculateDrawingReacts(SharedElementTransition from, SharedElementTransition to) {
127
+        from.getDrawingRect(startDrawingRect);
128
+        to.getDrawingRect(endDrawingRect);
129
+    }
130
+}

+ 13
- 0
android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/ControlPoint.java View File

@@ -0,0 +1,13 @@
1
+package com.reactnativenavigation.views.sharedElementTransition;
2
+
3
+import android.support.annotation.Nullable;
4
+
5
+public class ControlPoint {
6
+    public final float x;
7
+    public final float y;
8
+
9
+    public ControlPoint(@Nullable Float x, float defaultX, Float y, float defaultY) {
10
+        this.x = x == null ? defaultX : x;
11
+        this.y = y == null ? defaultY : y;
12
+    }
13
+}

+ 57
- 0
android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/ReversedAnimatorValuesResolver.java View File

@@ -0,0 +1,57 @@
1
+package com.reactnativenavigation.views.sharedElementTransition;
2
+
3
+import com.reactnativenavigation.params.PathInterpolationParams;
4
+import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
5
+
6
+class ReversedAnimatorValuesResolver extends AnimatorValuesResolver {
7
+
8
+    ReversedAnimatorValuesResolver(SharedElementTransition from, SharedElementTransition to, SharedElementTransitionParams params) {
9
+        super(from, to, params);
10
+    }
11
+
12
+    @Override
13
+    protected void calculateControlPoints(PathInterpolationParams interpolation) {
14
+        controlX1 = dx * interpolation.p1.x;
15
+        controlY1 = dy * interpolation.p1.y;
16
+        controlX2 = dx * interpolation.p2.x;
17
+        controlY2 = dy * interpolation.p2.y;
18
+    }
19
+
20
+    @Override
21
+    protected float calculateEndScaleY(SharedElementTransition from, SharedElementTransition to) {
22
+        return ((float) to.getHeight()) / from.getHeight();
23
+    }
24
+
25
+    @Override
26
+    protected float calculateStartScaleY(SharedElementTransition from, SharedElementTransition to) {
27
+        return 1;
28
+    }
29
+
30
+    @Override
31
+    protected float calculateEndScaleX(SharedElementTransition from, SharedElementTransition to) {
32
+        return ((float) to.getWidth()) / from.getWidth();
33
+    }
34
+
35
+    @Override
36
+    protected float calculateStartScaleX(SharedElementTransition from, SharedElementTransition to) {
37
+        return 1;
38
+    }
39
+
40
+    @Override
41
+    protected void calculateEndPoint() {
42
+        endX = dx;
43
+        endY = dy;
44
+    }
45
+
46
+    @Override
47
+    protected void calculateStartPoint() {
48
+        startX = 0;
49
+        startY = 0;
50
+    }
51
+
52
+    @Override
53
+    protected void calculateDeltas() {
54
+        dx = toXy.x - fromXy.x;
55
+        dy = toXy.y - fromXy.y;
56
+    }
57
+}

+ 166
- 0
android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElementAnimatorCreator.java View File

@@ -0,0 +1,166 @@
1
+package com.reactnativenavigation.views.sharedElementTransition;
2
+
3
+import android.animation.Animator;
4
+import android.animation.ObjectAnimator;
5
+import android.support.annotation.NonNull;
6
+import android.view.View;
7
+
8
+import com.reactnativenavigation.params.InterpolationParams;
9
+import com.reactnativenavigation.params.PathInterpolationParams;
10
+import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
11
+import com.reactnativenavigation.views.utils.AnimatorPath;
12
+import com.reactnativenavigation.views.utils.ClipBoundsEvaluator;
13
+import com.reactnativenavigation.views.utils.ColorUtils;
14
+import com.reactnativenavigation.views.utils.LabColorEvaluator;
15
+import com.reactnativenavigation.views.utils.PathEvaluator;
16
+
17
+import java.util.ArrayList;
18
+import java.util.List;
19
+
20
+import static android.animation.ObjectAnimator.ofFloat;
21
+
22
+class SharedElementAnimatorCreator {
23
+    private final SharedElementTransition from;
24
+    private final SharedElementTransition to;
25
+
26
+    SharedElementAnimatorCreator(SharedElementTransition from, SharedElementTransition to) {
27
+        this.from = from;
28
+        this.to = to;
29
+    }
30
+
31
+    List<Animator> createShow() {
32
+        return create(new AnimatorValuesResolver(from, to, to.showTransitionParams), to.showTransitionParams);
33
+    }
34
+
35
+    List<Animator> createHide() {
36
+        return create(new ReversedAnimatorValuesResolver(to, from, to.hideTransitionParams), to.hideTransitionParams);
37
+    }
38
+
39
+    @NonNull
40
+    private List<Animator> create(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
41
+        List<Animator> result = new ArrayList<>();
42
+        if (shouldAddCurvedMotionAnimator(resolver, params.interpolation)) {
43
+            result.add(createCurvedMotionAnimator(resolver, params));
44
+        } else {
45
+            if (shouldAddLinearMotionXAnimator(resolver, params)) {
46
+                result.add(createXAnimator(resolver, params));
47
+            }
48
+            if (shouldAddLinearMotionYAnimator(resolver, params)) {
49
+                result.add(createYAnimator(resolver, params));
50
+            }
51
+        }
52
+        if (shouldCreateScaleXAnimator(resolver, params)) {
53
+            result.add(createScaleXAnimator(resolver, params));
54
+        }
55
+        if (shouldCreateScaleYAnimator(resolver, params)) {
56
+            result.add(createScaleYAnimator(resolver, params));
57
+        }
58
+        if (shouldCreateColorAnimator(resolver)) {
59
+            result.add(createColorAnimator(resolver, params.duration));
60
+        }
61
+        if (shouldCreateImageClipBoundsAnimator(params)) {
62
+            result.add(createImageClipBoundsAnimator(resolver, params.duration));
63
+        }
64
+        return result;
65
+    }
66
+
67
+    private boolean shouldCreateScaleYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
68
+        return resolver.startScaleY != resolver.endScaleY && !params.animateClipBounds;
69
+    }
70
+
71
+    private boolean shouldCreateScaleXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
72
+        return resolver.startScaleX != resolver.endScaleX && !params.animateClipBounds;
73
+    }
74
+
75
+    private boolean shouldAddLinearMotionXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
76
+        if (params.animateClipBounds) {
77
+            return to.getWidth() - from.getWidth() != resolver.dx * 2;
78
+        } else {
79
+            return resolver.dx != 0;
80
+        }
81
+    }
82
+
83
+    private boolean shouldAddLinearMotionYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
84
+        if (params.animateClipBounds) {
85
+            return to.getHeight() - from.getHeight() != resolver.dy * 2;
86
+        } else {
87
+            return resolver.dy != 0;
88
+        }
89
+    }
90
+
91
+    private boolean shouldAddCurvedMotionAnimator(AnimatorValuesResolver resolver, InterpolationParams interpolation) {
92
+        return interpolation instanceof PathInterpolationParams && (resolver.dx != 0 || resolver.dy != 0);
93
+    }
94
+
95
+    private boolean shouldCreateColorAnimator(AnimatorValuesResolver resolver) {
96
+        return resolver.startColor != resolver.endColor;
97
+    }
98
+
99
+    private boolean shouldCreateImageClipBoundsAnimator(SharedElementTransitionParams params) {
100
+        return params.animateClipBounds;
101
+    }
102
+
103
+    private ObjectAnimator createCurvedMotionAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
104
+        AnimatorPath path = new AnimatorPath();
105
+        path.moveTo(resolver.startX, resolver.startY);
106
+        path.curveTo(resolver.controlX1, resolver.controlY1, resolver.controlX2, resolver.controlY2, resolver.endX, resolver.endY);
107
+        ObjectAnimator animator = ObjectAnimator.ofObject(
108
+                to,
109
+                "curvedMotion",
110
+                new PathEvaluator(),
111
+                path.getPoints().toArray());
112
+        animator.setInterpolator(params.interpolation.easing.getInterpolator());
113
+        animator.setDuration(params.duration);
114
+        return animator;
115
+    }
116
+
117
+    private ObjectAnimator createXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
118
+        ObjectAnimator animator = ofFloat(to.getSharedView(), View.TRANSLATION_X, resolver.startX, resolver.endX)
119
+                .setDuration(params.duration);
120
+        animator.setInterpolator(params.interpolation.easing.getInterpolator());
121
+        return animator;
122
+    }
123
+
124
+    private ObjectAnimator createYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
125
+        ObjectAnimator animator = ofFloat(to.getSharedView(), View.TRANSLATION_Y, resolver.startY, resolver.endY)
126
+                .setDuration(params.duration);
127
+        animator.setInterpolator(params.interpolation.easing.getInterpolator());
128
+        return animator;
129
+    }
130
+
131
+    private ObjectAnimator createScaleXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
132
+        to.getSharedView().setPivotX(0);
133
+        ObjectAnimator animator = ofFloat(to.getSharedView(), View.SCALE_X, resolver.startScaleX, resolver.endScaleX)
134
+                .setDuration(params.duration);
135
+        animator.setInterpolator(params.interpolation.easing.getInterpolator());
136
+        return animator;
137
+    }
138
+
139
+    private ObjectAnimator createScaleYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
140
+        to.getSharedView().setPivotY(0);
141
+        ObjectAnimator animator = ofFloat(to.getSharedView(), View.SCALE_Y, resolver.startScaleY, resolver.endScaleY)
142
+                .setDuration(params.duration);
143
+        animator.setInterpolator(params.interpolation.easing.getInterpolator());
144
+        return animator;
145
+    }
146
+
147
+    private ObjectAnimator createColorAnimator(AnimatorValuesResolver resolver, int duration) {
148
+        return ObjectAnimator.ofObject(
149
+                to,
150
+                "textColor",
151
+                new LabColorEvaluator(),
152
+                ColorUtils.colorToLAB(resolver.startColor),
153
+                ColorUtils.colorToLAB(resolver.endColor))
154
+                .setDuration(duration);
155
+    }
156
+
157
+    private ObjectAnimator createImageClipBoundsAnimator(AnimatorValuesResolver resolver, int duration) {
158
+        return ObjectAnimator.ofObject(
159
+                to,
160
+                "clipBounds",
161
+                new ClipBoundsEvaluator(),
162
+                resolver.startDrawingRect,
163
+                resolver.endDrawingRect)
164
+                .setDuration(duration);
165
+    }
166
+}

+ 169
- 0
android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElementTransition.java View File

@@ -0,0 +1,169 @@
1
+package com.reactnativenavigation.views.sharedElementTransition;
2
+
3
+import android.content.Context;
4
+import android.graphics.Rect;
5
+import android.os.Build;
6
+import android.support.annotation.Keep;
7
+import android.text.SpannableString;
8
+import android.text.SpannedString;
9
+import android.view.View;
10
+import android.view.ViewGroup;
11
+import android.view.ViewManager;
12
+import android.widget.FrameLayout;
13
+import android.widget.RelativeLayout;
14
+import android.widget.TextView;
15
+
16
+import com.facebook.react.views.image.ReactImageView;
17
+import com.reactnativenavigation.params.parsers.SharedElementParamsParser;
18
+import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
19
+import com.reactnativenavigation.react.ReactViewHacks;
20
+import com.reactnativenavigation.screens.Screen;
21
+import com.reactnativenavigation.utils.Task;
22
+import com.reactnativenavigation.utils.ViewUtils;
23
+import com.reactnativenavigation.views.utils.ColorUtils;
24
+import com.reactnativenavigation.views.utils.PathPoint;
25
+import com.reactnativenavigation.views.utils.Point;
26
+
27
+public class SharedElementTransition extends FrameLayout {
28
+    public ViewGroup.LayoutParams childLayoutParams;
29
+
30
+    public SharedElementParamsParser paramsParser = new SharedElementParamsParser();
31
+    public SharedElementTransitionParams showTransitionParams;
32
+    public SharedElementTransitionParams hideTransitionParams;
33
+    private View child;
34
+    private int childLeft;
35
+    private int childTop;
36
+    private int childWidth = -1;
37
+    private int childHeight = -1;
38
+    private SpannableString spannableText;
39
+    private SpannedString spannedText;
40
+
41
+    public View getSharedView() {
42
+        return child;
43
+    }
44
+
45
+    public SharedElementTransition(Context context) {
46
+        super(context);
47
+    }
48
+
49
+    public void registerSharedElementTransition(final String key) {
50
+        ViewUtils.runOnPreDraw(this, new Runnable() {
51
+            @Override
52
+            public void run() {
53
+                ViewUtils.performOnParentScreen(SharedElementTransition.this, new Task<Screen>() {
54
+                    @Override
55
+                    public void run(Screen screen) {
56
+                        screen.registerSharedElement(SharedElementTransition.this, key);
57
+                    }
58
+                });
59
+            }
60
+        });
61
+    }
62
+
63
+    @Override
64
+    public void onViewAdded(final View child) {
65
+        if (child instanceof ReactImageView && this.child == null) {
66
+            ReactViewHacks.disableReactImageViewRemoteImageFadeInAnimation((ReactImageView) child);
67
+        }
68
+        this.child = child;
69
+        if (child instanceof TextView) {
70
+            saveTextViewSpannedText((TextView) child);
71
+        }
72
+        super.onViewAdded(child);
73
+    }
74
+
75
+    private void saveTextViewSpannedText(final TextView view) {
76
+        ViewUtils.runOnPreDraw(view, new Runnable() {
77
+            @Override
78
+            public void run() {
79
+                if (spannableText == null) {
80
+                    spannedText = (SpannedString) view.getText();
81
+                }
82
+                if (spannableText == null) {
83
+                    spannableText = new SpannableString(spannedText);
84
+                }
85
+            }
86
+        });
87
+    }
88
+
89
+    @Keep
90
+    public void setCurvedMotion(PathPoint xy) {
91
+        child.setTranslationX(xy.mX);
92
+        child.setTranslationY(xy.mY);
93
+    }
94
+
95
+    @Keep
96
+    public void setTextColor(double[] color) {
97
+        if (child instanceof TextView) {
98
+            ViewUtils.setSpanColor(spannableText, ColorUtils.labToColor(color));
99
+            ((TextView) child).setText(spannableText);
100
+        }
101
+    }
102
+
103
+    @Keep
104
+    public void setClipBounds(Rect clipBounds) {
105
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
106
+            child.setClipBounds(clipBounds);
107
+        }
108
+    }
109
+
110
+    public void attachChildToScreen() {
111
+        ViewUtils.performOnParentScreen(this, new Task<Screen>() {
112
+            @Override
113
+            public void run(Screen screen) {
114
+                saveChildParams(child);
115
+                Point childLocationInScreen = ViewUtils.getLocationOnScreen(child);
116
+
117
+                RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(childWidth, childHeight);
118
+                removeView(child);
119
+                lp.leftMargin = childLocationInScreen.x;
120
+                lp.topMargin = childLocationInScreen.y;
121
+
122
+                screen.addView(child, lp);
123
+            }
124
+
125
+            private void saveChildParams(final View child) {
126
+                childLayoutParams = child.getLayoutParams();
127
+                childLeft = child.getLeft();
128
+                childTop = child.getTop();
129
+                if (childWidth == -1) {
130
+                    childWidth = child.getWidth();
131
+                }
132
+                if (childHeight == -1) {
133
+                    childHeight = child.getHeight();
134
+                }
135
+            }
136
+        });
137
+    }
138
+
139
+    public void attachChildToSelf() {
140
+        ((ViewManager) child.getParent()).removeView(child);
141
+        child.setLeft(childLeft);
142
+        child.setTop(childTop);
143
+        restoreTextViewSpannedText();
144
+        addView(child, new LayoutParams(childLayoutParams));
145
+    }
146
+
147
+    private void restoreTextViewSpannedText() {
148
+        if (child instanceof TextView) {
149
+            ((TextView) child).setText(spannedText);
150
+            spannedText = null;
151
+            spannableText = null;
152
+        }
153
+    }
154
+
155
+    public void show() {
156
+        setVisibility(VISIBLE);
157
+        ViewUtils.runOnPreDraw(child, new Runnable() {
158
+            @Override
159
+            public void run() {
160
+                child.setAlpha(1);
161
+            }
162
+        });
163
+    }
164
+
165
+    public void hide() {
166
+        setVisibility(INVISIBLE);
167
+        child.setAlpha(0);
168
+    }
169
+}

+ 143
- 0
android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElements.java View File

@@ -0,0 +1,143 @@
1
+package com.reactnativenavigation.views.sharedElementTransition;
2
+
3
+import android.view.View;
4
+
5
+import com.reactnativenavigation.utils.ViewUtils;
6
+import com.reactnativenavigation.utils.ViewVisibilityChecker;
7
+
8
+import java.util.HashMap;
9
+import java.util.Iterator;
10
+import java.util.Map;
11
+import java.util.concurrent.atomic.AtomicInteger;
12
+
13
+public class SharedElements {
14
+    // These need to be weak references or better yet - clear them in `onViewRemoved`
15
+    Map<String, SharedElementTransition> toElements;
16
+    private Map<String, SharedElementTransition> fromElements;
17
+
18
+    public void setFromElements(Map<String, SharedElementTransition> fromElements) {
19
+        this.fromElements.clear();
20
+        for (String fromElementKey : fromElements.keySet()) {
21
+            if (toElements.containsKey(fromElementKey)) {
22
+                this.fromElements.put(fromElementKey, fromElements.get(fromElementKey));
23
+            }
24
+        }
25
+    }
26
+
27
+    public void setToElements(Map<String, SharedElementTransition> toElements) {
28
+        this.toElements.clear();
29
+        for (String toElementKey : toElements.keySet()) {
30
+            if (fromElements.containsKey(toElementKey)) {
31
+                this.toElements.put(toElementKey, toElements.get(toElementKey));
32
+            }
33
+        }
34
+    }
35
+
36
+    public Map<String, SharedElementTransition> getToElements() {
37
+        return toElements;
38
+    }
39
+
40
+    SharedElementTransition getFromElement(String key) {
41
+        return fromElements.get(key);
42
+    }
43
+
44
+    SharedElementTransition getToElement(String key) {
45
+        return toElements.get(key);
46
+    }
47
+
48
+    public void addToElement(SharedElementTransition sharedElement, String key) {
49
+        toElements.put(key, sharedElement);
50
+    }
51
+
52
+    public SharedElements() {
53
+        toElements = new HashMap<>();
54
+        this.fromElements = new HashMap<>();
55
+    }
56
+
57
+    void performWhenChildViewsAreDrawn(final Runnable onReady) {
58
+        final AtomicInteger latch = new AtomicInteger(toElements.size());
59
+        for (SharedElementTransition toElement : toElements.values()) {
60
+            ViewUtils.runOnPreDraw(toElement.getSharedView(), new Runnable() {
61
+                @Override
62
+                public void run() {
63
+                    if (latch.decrementAndGet() == 0) {
64
+                        onReady.run();
65
+                    }
66
+                }
67
+            });
68
+        }
69
+    }
70
+
71
+    void attachChildViewsToScreen() {
72
+        for (SharedElementTransition toElement : toElements.values()) {
73
+            toElement.attachChildToScreen();
74
+        }
75
+    }
76
+
77
+    void hideToElements() {
78
+        for (SharedElementTransition toElement : toElements.values()) {
79
+            toElement.hide();
80
+        }
81
+    }
82
+
83
+    void showToElements(final Runnable onReady) {
84
+        final AtomicInteger latch = new AtomicInteger(toElements.size());
85
+        for (final SharedElementTransition toElement : toElements.values()) {
86
+            toElement.show();
87
+            ViewUtils.runOnPreDraw(toElement, new Runnable() {
88
+                @Override
89
+                public void run() {
90
+                    if (latch.decrementAndGet() == 0) {
91
+                        onReady.run();
92
+                    }
93
+                }
94
+            });
95
+        }
96
+    }
97
+
98
+    void onShowAnimationEnd() {
99
+        for (SharedElementTransition toElement : toElements.values()) {
100
+            toElement.attachChildToSelf();
101
+        }
102
+    }
103
+
104
+    void hideFromElements() {
105
+        for (final SharedElementTransition fromElement : fromElements.values()) {
106
+            fromElement.post(new Runnable() {
107
+                @Override
108
+                public void run() {
109
+                    fromElement.setVisibility(View.INVISIBLE);
110
+                }
111
+            });
112
+        }
113
+    }
114
+
115
+    void showToElements() {
116
+        for (SharedElementTransition toElement : toElements.values()) {
117
+            toElement.setVisibility(View.VISIBLE);
118
+        }
119
+    }
120
+
121
+    void onHideAnimationStart() {
122
+        for (SharedElementTransition fromElement : fromElements.values()) {
123
+            fromElement.attachChildToScreen();
124
+            fromElement.getSharedView().setAlpha(1);
125
+        }
126
+    }
127
+
128
+    public void destroy() {
129
+        toElements.clear();
130
+        fromElements.clear();
131
+    }
132
+
133
+    public void removeHiddenElements() {
134
+        Iterator<String> iterator = toElements.keySet().iterator();
135
+        while (iterator.hasNext()) {
136
+            String key = iterator.next();
137
+            if (!ViewVisibilityChecker.check(toElements.get(key))) {
138
+                iterator.remove();
139
+                fromElements.get(key).show();
140
+            }
141
+        }
142
+    }
143
+}

+ 84
- 0
android/app/src/main/java/com/reactnativenavigation/views/sharedElementTransition/SharedElementsAnimator.java View File

@@ -0,0 +1,84 @@
1
+package com.reactnativenavigation.views.sharedElementTransition;
2
+
3
+import android.animation.Animator;
4
+import android.animation.AnimatorListenerAdapter;
5
+import android.animation.AnimatorSet;
6
+
7
+import java.util.ArrayList;
8
+import java.util.List;
9
+
10
+public class SharedElementsAnimator {
11
+    private final SharedElements sharedElements;
12
+
13
+    public SharedElementsAnimator(SharedElements sharedElements) {
14
+        this.sharedElements = sharedElements;
15
+    }
16
+
17
+    public void show(final Runnable onAnimationEnd) {
18
+        sharedElements.hideToElements();
19
+        sharedElements.performWhenChildViewsAreDrawn(new Runnable()  {
20
+            @Override
21
+            public void run() {
22
+                final AnimatorSet animatorSet = createAnimatorSet();
23
+                sharedElements.attachChildViewsToScreen();
24
+                sharedElements.showToElements(new Runnable() {
25
+                    @Override
26
+                    public void run() {
27
+                        sharedElements.hideFromElements();
28
+                        animatorSet.start();
29
+                    }
30
+                });
31
+            }
32
+
33
+            private AnimatorSet createAnimatorSet() {
34
+                final AnimatorSet animatorSet = new AnimatorSet();
35
+                animatorSet.playTogether(createTransitionAnimators());
36
+                animatorSet.addListener(new AnimatorListenerAdapter() {
37
+                    @Override
38
+                    public void onAnimationEnd(Animator animation) {
39
+                        sharedElements.onShowAnimationEnd();
40
+                        onAnimationEnd.run();
41
+                    }
42
+
43
+                    @Override
44
+                    public void onAnimationCancel(Animator animation) {
45
+                        sharedElements.onShowAnimationEnd();
46
+                    }
47
+                });
48
+                return animatorSet;
49
+            }
50
+
51
+            private List<Animator> createTransitionAnimators() {
52
+                List<Animator> result = new ArrayList<>();
53
+                for (String key : sharedElements.toElements.keySet()) {
54
+                    SharedElementTransition toElement = sharedElements.getToElement(key);
55
+                    SharedElementTransition fromElement = sharedElements.getFromElement(key);
56
+                    result.addAll(new SharedElementAnimatorCreator(fromElement, toElement).createShow());
57
+                }
58
+                return result;
59
+            }
60
+        });
61
+    }
62
+
63
+    public void hide(final Runnable onAnimationEnd) {
64
+        AnimatorSet animatorSet = new AnimatorSet();
65
+        animatorSet.playTogether(createHideTransitionAnimators());
66
+        animatorSet.addListener(new AnimatorListenerAdapter() {
67
+            @Override
68
+            public void onAnimationEnd(Animator animation) {
69
+                sharedElements.showToElements();
70
+                onAnimationEnd.run();
71
+            }
72
+        });
73
+        sharedElements.onHideAnimationStart();
74
+        animatorSet.start();
75
+    }
76
+
77
+    private List<Animator> createHideTransitionAnimators() {
78
+        List<Animator> result = new ArrayList<>();
79
+        for (String key : sharedElements.toElements.keySet()) {
80
+            result.addAll(new SharedElementAnimatorCreator(sharedElements.getToElement(key), sharedElements.getFromElement(key)).createHide());
81
+        }
82
+        return result;
83
+    }
84
+}

+ 63
- 0
android/app/src/main/java/com/reactnativenavigation/views/utils/AnimatorPath.java View File

@@ -0,0 +1,63 @@
1
+/*
2
+ * Copyright (C) 2013 The Android Open Source Project
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *      http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+package com.reactnativenavigation.views.utils;
18
+
19
+import java.util.ArrayList;
20
+import java.util.Collection;
21
+
22
+/**
23
+ * A simple Path object that holds information about the points along
24
+ * a path. The API allows you to specify a move location (which essentially
25
+ * jumps from the previous point in the path to the new one), a line location
26
+ * (which creates a line segment from the previous location) and a curve
27
+ * location (which creates a B�zier curve from the previous location).
28
+ */
29
+public class AnimatorPath {
30
+
31
+    // The points in the path
32
+    ArrayList<PathPoint> mPoints = new ArrayList<PathPoint>();
33
+    /**
34
+     * Move from the current path point to the new one
35
+     * specified by x and y. This will create a discontinuity if this point is
36
+     * neither the first point in the path nor the same as the previous point
37
+     * in the path.
38
+     */
39
+    public void moveTo(float x, float y) {
40
+        mPoints.add(PathPoint.moveTo(x, y));
41
+    }
42
+    /**
43
+     * Create a straight line from the current path point to the new one
44
+     * specified by x and y.
45
+     */
46
+    public void lineTo(float x, float y) {
47
+        mPoints.add(PathPoint.lineTo(x, y));
48
+    }
49
+    /**
50
+     * Create a quadratic B�zier curve from the current path point to the new one
51
+     * specified by x and y. The curve uses the current path location as the first anchor
52
+     * point, the control points (c0X, c0Y) and (c1X, c1Y), and (x, y) as the end anchor.
53
+     */
54
+    public void curveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y) {
55
+        mPoints.add(PathPoint.curveTo(c0X, c0Y, c1X, c1Y, x, y));
56
+    }
57
+    /**
58
+     * Returns a Collection of PathPoint objects that describe all points in the path.
59
+     */
60
+    public Collection<PathPoint> getPoints() {
61
+        return mPoints;
62
+    }
63
+}

+ 49
- 0
android/app/src/main/java/com/reactnativenavigation/views/utils/ClipBoundsEvaluator.java View File

@@ -0,0 +1,49 @@
1
+package com.reactnativenavigation.views.utils;
2
+
3
+import android.animation.TypeEvaluator;
4
+import android.graphics.Rect;
5
+
6
+public class ClipBoundsEvaluator implements TypeEvaluator<Rect> {
7
+    private int fromWidth;
8
+    private int fromHeight;
9
+    private int toWidth;
10
+    private int toHeight;
11
+    private final Rect result = new Rect();
12
+
13
+    @Override
14
+    public Rect evaluate(float ratio, Rect from, Rect to) {
15
+        sync(from, to);
16
+
17
+        if (toHeight == fromHeight ) {
18
+            result.bottom = toHeight;
19
+        } else {
20
+            if (toHeight > fromHeight) {
21
+                result.top = (int) (Math.abs(toHeight - fromHeight) / 2 * (1 - ratio));
22
+                result.bottom = toHeight- result.top;
23
+            } else {
24
+                result.top = (int) (Math.abs(toHeight - fromHeight) / 2 * ratio);
25
+                result.bottom = fromHeight - result.top;
26
+            }
27
+        }
28
+
29
+        if (toWidth == fromWidth) {
30
+            result.right = toWidth;
31
+        } else {
32
+            if (toWidth > fromWidth) {
33
+                result.left = (int) (Math.abs(toWidth - fromWidth) / 2 * (1 - ratio));
34
+                result.right = toWidth - result.left;
35
+            } else {
36
+                result.left = (int) (Math.abs(toWidth - fromWidth) / 2 * ratio);
37
+                result.right = fromWidth - result.left;
38
+            }
39
+        }
40
+        return result;
41
+    }
42
+
43
+    private void sync(Rect from, Rect to) {
44
+        fromWidth = from.right;
45
+        fromHeight = from.bottom;
46
+        toWidth = to.right;
47
+        toHeight = to.bottom;
48
+    }
49
+}

+ 13
- 0
android/app/src/main/java/com/reactnativenavigation/views/utils/ColorUtils.java View File

@@ -0,0 +1,13 @@
1
+package com.reactnativenavigation.views.utils;
2
+
3
+public class ColorUtils {
4
+    public static double[] colorToLAB(int color) {
5
+        final double[] result = new double[3];
6
+        android.support.v4.graphics.ColorUtils.colorToLAB(color, result);
7
+        return result;
8
+    }
9
+
10
+    public static int labToColor(double[] lab) {
11
+        return android.support.v4.graphics.ColorUtils.LABToColor(lab[0], lab[1], lab[2]);
12
+    }
13
+}

+ 14
- 0
android/app/src/main/java/com/reactnativenavigation/views/utils/LabColorEvaluator.java View File

@@ -0,0 +1,14 @@
1
+package com.reactnativenavigation.views.utils;
2
+
3
+import android.animation.TypeEvaluator;
4
+import android.support.v4.graphics.ColorUtils;
5
+
6
+public class LabColorEvaluator implements TypeEvaluator<double[]> {
7
+    private final double[] color = new double[3];
8
+
9
+    @Override
10
+    public double[] evaluate(float ratio, double[] from, double[] to) {
11
+        ColorUtils.blendLAB(from, to, ratio, color);
12
+        return color;
13
+    }
14
+}

+ 50
- 0
android/app/src/main/java/com/reactnativenavigation/views/utils/PathEvaluator.java View File

@@ -0,0 +1,50 @@
1
+/*
2
+ * Copyright (C) 2013 The Android Open Source Project
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *      http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+package com.reactnativenavigation.views.utils;
18
+
19
+import android.animation.TypeEvaluator;
20
+
21
+/**
22
+ * This evaluator interpolates between two PathPoint values given the value t, the
23
+ * proportion traveled between those points. The value of the interpolation depends
24
+ * on the operation specified by the endValue (the operation for the interval between
25
+ * PathPoints is always specified by the end point of that interval).
26
+ */
27
+public class PathEvaluator implements TypeEvaluator<PathPoint> {
28
+    @Override
29
+    public PathPoint evaluate(float t, PathPoint startValue, PathPoint endValue) {
30
+        float x, y;
31
+        if (endValue.mOperation == PathPoint.CURVE) {
32
+            float oneMinusT = 1 - t;
33
+            x = oneMinusT * oneMinusT * oneMinusT * startValue.mX +
34
+                3 * oneMinusT * oneMinusT * t * endValue.mControl0X +
35
+                3 * oneMinusT * t * t * endValue.mControl1X +
36
+                t * t * t * endValue.mX;
37
+            y = oneMinusT * oneMinusT * oneMinusT * startValue.mY +
38
+                3 * oneMinusT * oneMinusT * t * endValue.mControl0Y +
39
+                3 * oneMinusT * t * t * endValue.mControl1Y +
40
+                t * t * t * endValue.mY;
41
+        } else if (endValue.mOperation == PathPoint.LINE) {
42
+            x = startValue.mX + t * (endValue.mX - startValue.mX);
43
+            y = startValue.mY + t * (endValue.mY - startValue.mY);
44
+        } else {
45
+            x = endValue.mX;
46
+            y = endValue.mY;
47
+        }
48
+        return PathPoint.moveTo(x, y);
49
+    }
50
+}

+ 94
- 0
android/app/src/main/java/com/reactnativenavigation/views/utils/PathPoint.java View File

@@ -0,0 +1,94 @@
1
+/*
2
+ * Copyright (C) 2013 The Android Open Source Project
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *      http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+package com.reactnativenavigation.views.utils;
18
+
19
+/**
20
+ * A class that holds information about a location and how the path should get to that
21
+ * location from the previous path location (if any). Any PathPoint holds the information for
22
+ * its location as well as the instructions on how to traverse the preceding interval from the
23
+ * previous location.
24
+ */
25
+public class PathPoint {
26
+    /**
27
+     * The possible path operations that describe how to move from a preceding PathPoint to the
28
+     * location described by this PathPoint.
29
+     */
30
+    public static final int MOVE = 0;
31
+    public static final int LINE = 1;
32
+    public static final int CURVE = 2;
33
+    /**
34
+     * The location of this PathPoint
35
+     */
36
+    public float mX;
37
+    public float mY;
38
+
39
+    /**
40
+     * The first control point, if any, for a PathPoint of type CURVE
41
+     */
42
+    float mControl0X, mControl0Y;
43
+    /**
44
+     * The second control point, if any, for a PathPoint of type CURVE
45
+     */
46
+    float mControl1X, mControl1Y;
47
+    /**
48
+     * The motion described by the path to get from the previous PathPoint in an AnimatorPath
49
+     * to the location of this PathPoint. This can be one of MOVE, LINE, or CURVE.
50
+     */
51
+    int mOperation;
52
+    /**
53
+     * Line/Move constructor
54
+     */
55
+    private PathPoint(int operation, float x, float y) {
56
+        mOperation = operation;
57
+        mX = x;
58
+        mY = y;
59
+    }
60
+    /**
61
+     * Curve constructor
62
+     */
63
+    private PathPoint(float c0X, float c0Y, float c1X, float c1Y, float x, float y) {
64
+        mControl0X = c0X;
65
+        mControl0Y = c0Y;
66
+        mControl1X = c1X;
67
+        mControl1Y = c1Y;
68
+        mX = x;
69
+        mY = y;
70
+        mOperation = CURVE;
71
+    }
72
+    /**
73
+     * Constructs and returns a PathPoint object that describes a line to the given xy location.
74
+     */
75
+    public static PathPoint lineTo(float x, float y) {
76
+        return new PathPoint(LINE, x, y);
77
+    }
78
+    /**
79
+     * Constructs and returns a PathPoint object that describes a curve to the given xy location
80
+     * with the control points at c0 and c1.
81
+     */
82
+    public static PathPoint curveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y) {
83
+        return new PathPoint(c0X,  c0Y, c1X, c1Y, x, y);
84
+    }
85
+
86
+    /**
87
+     * Constructs and returns a PathPoint object that describes a discontinuous move to the given
88
+     * xy location.
89
+     */
90
+    public static PathPoint moveTo(float x, float y) {
91
+        return new PathPoint(MOVE, x, y);
92
+    }
93
+}
94
+

+ 11
- 0
android/app/src/main/java/com/reactnativenavigation/views/utils/Point.java View File

@@ -0,0 +1,11 @@
1
+package com.reactnativenavigation.views.utils;
2
+
3
+public class Point {
4
+    public int x;
5
+    public int y;
6
+
7
+    public Point(int x, int y) {
8
+        this.x = x;
9
+        this.y = y;
10
+    }
11
+}

+ 1
- 1
example/android/app/proguard-rules.pro View File

@@ -35,7 +35,7 @@
35 35
 
36 36
 -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
37 37
   void set*(***);
38
-  *** get*();
38
+  *** withOrder*();
39 39
 }
40 40
 
41 41
 -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }

BIN
example/img/beach.jpg View File


BIN
example/img/heroes/bouny_hunter.png View File


BIN
example/img/heroes/earthspirit.png View File


BIN
example/img/heroes/oracle.png View File


BIN
example/img/heroes/skywrath_mage.png View File


BIN
example/img/heroes/templar_assasin.png View File


BIN
example/img/list@1.5x.android.png View File


BIN
example/img/list@1x.android.png View File


BIN
example/img/list@2x.android.png View File


BIN
example/img/list@3x.android.png View File


BIN
example/img/list@4x.android.png View File


+ 1
- 0
example/index.android.js View File

@@ -1 +1,2 @@
1
+__STRESS_TEST__ = false;
1 2
 import App from './src/app';

+ 1
- 0
example/package.json View File

@@ -12,6 +12,7 @@
12 12
   "dependencies": {
13 13
     "react-native": "0.41.2",
14 14
     "react": "15.4.2",
15
+    "react-native-animatable": "^1.1.0",
15 16
     "react-native-navigation": "file:../"
16 17
   },
17 18
   "devDependencies": {

+ 12
- 0
example/src/app.js View File

@@ -9,6 +9,18 @@ registerScreens();
9 9
 
10 10
 const createTabs = () => {
11 11
   let tabs = [
12
+    {
13
+      label: 'Card',
14
+      screen: 'example.CardScreen',
15
+      icon: require('../img/list.png'),
16
+      title: 'Shared Element Transition'
17
+    },
18
+    {
19
+      label: 'SET',
20
+      screen: 'example.ListScreen',
21
+      icon: require('../img/list.png'),
22
+      title: 'Shared Element Transition'
23
+    },
12 24
     {
13 25
       label: 'One',
14 26
       screen: 'example.FirstTabScreen',

+ 8
- 0
example/src/screens/index.android.js View File

@@ -9,6 +9,10 @@ import ModalScreen from './ModalScreen';
9 9
 import CollapsingTopBarScreen from './CollapsingTopBarScreen';
10 10
 import InAppNotification from './InAppNotification';
11 11
 import LightBoxScreen from './LightBoxScreen';
12
+import ListScreen from './set/ListScreen';
13
+import HeroScreen from './set/HeroScreen';
14
+import CardScreen from './set/CardScreen';
15
+import InfoScreen from './set/InformationScreen';
12 16
 
13 17
 // register all screens of the app (including internal ones)
14 18
 export function registerScreens() {
@@ -21,4 +25,8 @@ export function registerScreens() {
21 25
   Navigation.registerComponent('example.CollapsingTopBarScreen', () => CollapsingTopBarScreen);
22 26
   Navigation.registerComponent('example.InAppNotification', () => InAppNotification);
23 27
   Navigation.registerComponent('example.LightBoxScreen', () => LightBoxScreen);
28
+  Navigation.registerComponent('example.ListScreen', () => ListScreen);
29
+  Navigation.registerComponent('example.HeroScreen', () => HeroScreen);
30
+  Navigation.registerComponent('example.CardScreen', () => CardScreen);
31
+  Navigation.registerComponent('example.infoScreen', () => InfoScreen);
24 32
 }

+ 118
- 0
example/src/screens/set/CardScreen.js View File

@@ -0,0 +1,118 @@
1
+import React, {Component} from 'react';
2
+import {
3
+  ScrollView,
4
+  TouchableWithoutFeedback,
5
+  TouchableNativeFeedback,
6
+  StyleSheet,
7
+  Image,
8
+  Text,
9
+  View,
10
+  Platform,
11
+  ScrolView
12
+} from 'react-native';
13
+import {SharedElementTransition} from 'react-native-navigation';
14
+import * as Animatable from 'react-native-animatable';
15
+import * as setStyles from './styles';
16
+
17
+const IMAGE_HEIGHT = 190;
18
+
19
+export default class CardScreen extends Component {
20
+  static navigatorStyle = {
21
+    ...setStyles.navigatorStyle
22
+  };
23
+
24
+  constructor(props) {
25
+    super(props);
26
+    this.state = {};
27
+  }
28
+
29
+  render() {
30
+    return (
31
+      <ScrollView>
32
+        <View style={[styles.container]}>
33
+          {this._renderCard(0)}
34
+          {this._renderCard(1)}
35
+          {this._renderCard(2)}
36
+        </View>
37
+      </ScrollView>
38
+    );
39
+  }
40
+
41
+  _renderCard(index) {
42
+    return (
43
+      <View style={styles.cardContainer}>
44
+        <TouchableWithoutFeedback onPress={() => this._onCardPress(index)}>
45
+          {this._renderImage(index)}
46
+        </TouchableWithoutFeedback>
47
+        <TouchableNativeFeedback onPress={() => this._onCardPress(index)}>
48
+          {this._renderCardContent()}
49
+        </TouchableNativeFeedback>
50
+      </View>
51
+    );
52
+  }
53
+
54
+  _onCardPress(index) {
55
+    this.props.navigator.push({
56
+      screen: 'example.infoScreen',
57
+      _title: 'Shared Element Transition',
58
+      sharedElements: [`image${index}`],
59
+      animated: false,
60
+      overrideBackPress: true,
61
+      passProps: {
62
+        sharedImageId: `image${index}`
63
+      }
64
+    })
65
+  }
66
+
67
+  _renderImage(index) {
68
+    return (
69
+      <SharedElementTransition
70
+        style={styles.imageContainer}
71
+        sharedElementId={`image${index}`}
72
+      >
73
+        <Image
74
+          style={styles.image}
75
+          source={require('../../../img/beach.jpg')}
76
+        />
77
+      </SharedElementTransition>
78
+    );
79
+  }
80
+
81
+  _renderCardContent() {
82
+    return (
83
+      <View style={styles.cardContentContainer}>
84
+        <Text style={styles.title}>This is a title</Text>
85
+        <Text>This is a very long long long long long long long long long long content</Text>
86
+      </View>
87
+    );
88
+  }
89
+}
90
+
91
+const styles = StyleSheet.create({
92
+  container: {
93
+    marginVertical: 16,
94
+    marginHorizontal: 8,
95
+  },
96
+  cardContainer: {
97
+    marginVertical: 8,
98
+    elevation: 2,
99
+    borderRadius: 2,
100
+    backgroundColor: '#F5F5F5'
101
+  },
102
+  imageContainer: {
103
+    justifyContent: 'flex-start'
104
+  },
105
+  image: {
106
+    height: IMAGE_HEIGHT,
107
+    borderTopLeftRadius: 2,
108
+    borderTopRightRadius: 2
109
+  },
110
+  cardContentContainer: {
111
+    padding: 8
112
+  },
113
+  title: {
114
+    fontWeight: '500',
115
+    paddingBottom: 8,
116
+    fontSize: 17
117
+  }
118
+});

+ 216
- 0
example/src/screens/set/HeroScreen.js View File

@@ -0,0 +1,216 @@
1
+import React, {Component} from 'react';
2
+import {
3
+  ScrollView,
4
+  TouchableOpacity,
5
+  StyleSheet,
6
+  Image,
7
+  Text,
8
+  View,
9
+  ScrolView
10
+} from 'react-native';
11
+import {SharedElementTransition} from 'react-native-navigation';
12
+import * as Animatable from 'react-native-animatable';
13
+import * as heroStyles from './styles';
14
+
15
+const FADE_DURATION = 500;
16
+const SHOW_DURATION = 500;
17
+const HIDE_DURATION = 500;
18
+
19
+const HEADER_HEIGHT = 120;
20
+const ICON_MARGIN = 24;
21
+
22
+export default class HeroScreen extends Component {
23
+  static navigatorStyle = {
24
+    ...heroStyles.navigatorStyle
25
+  };
26
+
27
+  constructor(props) {
28
+    super(props);
29
+    this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
30
+    this.state = {
31
+      animationType: 'fadeIn'
32
+    }
33
+  }
34
+
35
+  componentDidMount() {
36
+    if (__STRESS_TEST__) {
37
+      setTimeout(() => {
38
+        this.setState({
39
+          animationType: 'fadeOut'
40
+        });
41
+        this.props.navigator.pop();
42
+      }, 650);
43
+    }
44
+  }
45
+
46
+  render() {
47
+    return (
48
+      <ScrollView style={[styles.container]}>
49
+        {this._renderHeader()}
50
+        {this._renderContent()}
51
+      </ScrollView>
52
+    );
53
+  }
54
+
55
+  _renderHeader() {
56
+    return (
57
+        <Animatable.View
58
+          animation={this.state.animationType}
59
+          duration={FADE_DURATION}
60
+          style={[styles.header, {backgroundColor: this.props.primaryColor}]}
61
+          useNativeDriver={true}
62
+        >
63
+          <View style={{height: ICON_MARGIN, flexDirection: 'row', backgroundColor: 'white'}}/>
64
+          <View style={{flex: 1, flexDirection: 'row'}}>
65
+            {this._renderIcon()}
66
+            {this._renderTitle()}
67
+          </View>
68
+      </Animatable.View>
69
+    );
70
+  }
71
+
72
+  _renderTitle() {
73
+    return (
74
+      <View style={{alignSelf: 'flex-end'}}>
75
+        <SharedElementTransition
76
+          style={styles.titleContainer}
77
+          sharedElementId={this.props.sharedTitleId}
78
+          showDuration={SHOW_DURATION}
79
+          hideDuration={HIDE_DURATION}
80
+          showInterpolation={
81
+                {
82
+                  type: 'path',
83
+                  controlX1: '0.5',
84
+                  controlY1: '1',
85
+                  controlX2: '0',
86
+                  controlY2: '0.5',
87
+                  easing: 'FastOutSlowIn'
88
+                }
89
+              }
90
+          hideInterpolation={
91
+                {
92
+                  type: 'path',
93
+                  controlX1: '0.5',
94
+                  controlY1: '0',
95
+                  controlX2: '1',
96
+                  controlY2: '0.5',
97
+                  easing:'FastOutSlowIn'
98
+                }
99
+              }
100
+        >
101
+          <Text style={[styles.title, {color: this.props.titleColor}]}>{this.props.title}</Text>
102
+        </SharedElementTransition>
103
+      </View>
104
+    );
105
+  }
106
+
107
+  _renderContent() {
108
+    return (
109
+      <Animatable.View
110
+        animation={this.state.animationType}
111
+        duration={FADE_DURATION}
112
+        style={styles.body}
113
+        useNativeDriver={true}>
114
+        <View
115
+          style={styles.list}
116
+          initialListSize={20}>
117
+          {this._genRows()}
118
+        </View>
119
+      </Animatable.View>
120
+    );
121
+  }
122
+
123
+  _genRows() {
124
+    const children = [];
125
+    for (let ii = 0; ii < 30; ii++) {
126
+      children.push(
127
+        <Text>{'row ' + ii}</Text>
128
+      );
129
+    }
130
+    return children;
131
+  }
132
+
133
+  _renderIcon() {
134
+    return (
135
+      <SharedElementTransition
136
+        sharedElementId={this.props.sharedIconId}
137
+        style={styles.iconContainer}
138
+        showDuration={SHOW_DURATION}
139
+        hideDuration={HIDE_DURATION}
140
+        showInterpolation={
141
+          {
142
+            type: 'path',
143
+            controlX1: '0.5',
144
+            controlY1: '1',
145
+            controlX2: '0',
146
+            controlY2: '0.5',
147
+            easing: 'FastOutSlowIn'
148
+          }
149
+        }
150
+        hideInterpolation={
151
+          {
152
+            type: 'path',
153
+            controlX1: '0.5',
154
+            controlY1: '0',
155
+            controlX2: '1',
156
+            controlY2: '0.5',
157
+            easing:'FastOutSlowIn'
158
+          }
159
+        }
160
+      >
161
+        <Image
162
+          source={this.props.icon}
163
+          style={styles.heroIcon}
164
+          fadeDuration={0}
165
+        />
166
+      </SharedElementTransition>
167
+    );
168
+  }
169
+
170
+  onNavigatorEvent(event) {
171
+    if (event.id === 'backPress') {
172
+      this.setState({
173
+        animationType: 'fadeOut'
174
+      });
175
+      this.props.navigator.pop();
176
+    }
177
+  }
178
+}
179
+
180
+const styles = StyleSheet.create({
181
+  container: {
182
+    flex: 1,
183
+    flexDirection: 'column',
184
+    backgroundColor: 'transparent'
185
+  },
186
+  header: {
187
+    height: 110,
188
+    flexDirection: 'column-reverse'
189
+  },
190
+  titleContainer: {
191
+    marginLeft: ICON_MARGIN + 90 +  + 16,
192
+    marginBottom: 8
193
+  },
194
+  title: {
195
+    fontSize: 23,
196
+    ...heroStyles.textLight
197
+  },
198
+  iconContainer: {
199
+    position: 'absolute',
200
+    bottom: -ICON_MARGIN,
201
+    left: ICON_MARGIN
202
+  },
203
+  heroIcon: {
204
+    width: 90,
205
+    height: 90,
206
+    resizeMode: 'contain'
207
+  },
208
+  body: {
209
+    flex: 4,
210
+    backgroundColor: 'white',
211
+  },
212
+  list: {
213
+    marginTop: 16,
214
+    marginHorizontal: 8
215
+  }
216
+});

+ 124
- 0
example/src/screens/set/InformationScreen.js View File

@@ -0,0 +1,124 @@
1
+import React, {Component} from 'react';
2
+import {
3
+  ScrollView,
4
+  TouchableOpacity,
5
+  StyleSheet,
6
+  Image,
7
+  Text,
8
+  View,
9
+  Platform,
10
+  ScrolView
11
+} from 'react-native';
12
+import {SharedElementTransition} from 'react-native-navigation';
13
+import * as Animatable from 'react-native-animatable';
14
+import * as setStyles from './styles';
15
+
16
+const SHOW_DURATION = 400;
17
+const HIDE_DURATION = 300;
18
+
19
+export default class InfoScreen extends Component {
20
+  static navigatorStyle = {
21
+    ...setStyles.navigatorStyle,
22
+    navBarHideOnScroll: false
23
+  };
24
+
25
+  constructor(props) {
26
+    super(props);
27
+    this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
28
+    this.state = {
29
+      animationType: 'fadeInRight',
30
+      animationDuration: SHOW_DURATION
31
+    }
32
+  }
33
+
34
+  onNavigatorEvent(event) {
35
+    if (event.id === 'backPress') {
36
+      this.setState({
37
+        animationType: 'fadeOutRight',
38
+        animationDuration: HIDE_DURATION
39
+      });
40
+      this.props.navigator.pop();
41
+    }
42
+  }
43
+
44
+  render() {
45
+    return (
46
+      <View style={styles.container}>
47
+        {this._renderImage()}
48
+        {this._renderContent()}
49
+      </View>
50
+    );
51
+  }
52
+
53
+  _renderImage() {
54
+    return (
55
+      <SharedElementTransition
56
+        style={styles.imageContainer}
57
+        sharedElementId={this.props.sharedImageId}
58
+        showDuration={SHOW_DURATION}
59
+        hideDuration={HIDE_DURATION}
60
+        animateClipBounds={true}
61
+        showInterpolation={
62
+          {
63
+            type: 'linear',
64
+            easing: 'FastOutSlowIn'
65
+          }
66
+        }
67
+        hideInterpolation={
68
+        {
69
+          type: 'linear',
70
+          easing:'FastOutSlowIn'
71
+        }
72
+      }
73
+      >
74
+        <Image
75
+          style={styles.image}
76
+          source={require('../../../img/beach.jpg')}
77
+        />
78
+      </SharedElementTransition>
79
+    );
80
+  }
81
+
82
+  _renderContent() {
83
+    return (
84
+      <Animatable.View
85
+        style={styles.content}
86
+        duration={this.state.animationDuration}
87
+        animation={this.state.animationType}
88
+        useNativeDriver={true}
89
+      >
90
+        <Text style={styles.text}>Line 1</Text>
91
+        <Text style={styles.text}>Line 2</Text>
92
+        <Text style={styles.text}>Line 3</Text>
93
+        <Text style={styles.text}>Line 4</Text>
94
+        <Text style={styles.text}>Line 5</Text>
95
+        <Text style={styles.text}>Line 6</Text>
96
+        <Text style={styles.text}>Line 7</Text>
97
+        <Text style={styles.text}>Line 8</Text>
98
+      </Animatable.View>
99
+    );
100
+  }
101
+}
102
+
103
+const styles = StyleSheet.create({
104
+  container: {
105
+    flex: 1
106
+  },
107
+  content: {
108
+    flex: 1,
109
+    marginTop: 190,
110
+    backgroundColor: 'white'
111
+  },
112
+  imageContainer: {
113
+    position: 'absolute',
114
+    top: 0
115
+  },
116
+  image: {
117
+    height: 190
118
+  },
119
+  text: {
120
+    fontSize: 17,
121
+    paddingVertical: 4,
122
+    paddingLeft: 8
123
+  }
124
+});

+ 235
- 0
example/src/screens/set/ListScreen.js View File

@@ -0,0 +1,235 @@
1
+import React, {Component} from 'react';
2
+import {
3
+  Text,
4
+  View,
5
+  ScrollView,
6
+  TouchableOpacity,
7
+  TouchableWithoutFeedback,
8
+  StyleSheet,
9
+  ListView,
10
+  Image
11
+} from 'react-native';
12
+import heroes from './heroes';
13
+import {SharedElementTransition} from 'react-native-navigation';
14
+import * as heroStyles from './styles';
15
+
16
+const LOREM_IPSUM = 'Lorem ipsum dolor sit amet, ius ad pertinax oportere accommodare, an vix civibus corrumpit referrentur. Te nam case ludus inciderint, te mea facilisi adipiscing. Sea id integre luptatum. In tota sale consequuntur nec. Erat ocurreret mei ei. Eu paulo sapientem vulputate est, vel an accusam intellegam interesset. Nam eu stet pericula reprimique, ea vim illud modus, putant invidunt reprehendunt ne qui.';
17
+const hashCode = function(str) {
18
+  var hash = 15;
19
+  for (var ii = str.length - 1; ii >= 0; ii--) {
20
+    hash = ((hash << 5) - hash) + str.charCodeAt(ii);
21
+  }
22
+  return hash;
23
+};
24
+
25
+export default class ListScreen extends Component {
26
+  static navigatorStyle = {
27
+    ...heroStyles.navigatorStyle
28
+  };
29
+
30
+  static navigatorButtons = {
31
+    rightButtons: [
32
+      {
33
+        title: 'Add',
34
+        icon: require('../../../img/navicon_add.png'),
35
+        id: 'add',
36
+        showAsAction: 'always'
37
+      },
38
+      {
39
+        title: 'All',
40
+        id: 'all',
41
+        showAsAction: 'never'
42
+      },
43
+      {
44
+        title: 'Upcoming',
45
+        id: 'upcoming',
46
+        showAsAction: 'never'
47
+      },
48
+      {
49
+        title: 'My',
50
+        id: 'my',
51
+        showAsAction: 'never'
52
+      },
53
+      {
54
+        title: 'Participant',
55
+        id: 'participant',
56
+        showAsAction: 'never'
57
+      },
58
+    ],
59
+    fab: {
60
+      collapsedId: 'share',
61
+      collapsedIcon: require('../../../img/navicon_add.png'),
62
+      backgroundColor: '#31363c'
63
+    }
64
+  };
65
+
66
+  constructor(props) {
67
+    super(props);
68
+    this._onRowPress = this._onRowPress.bind(this);
69
+    this._renderRow = this._renderRow.bind(this);
70
+    this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
71
+    this.onPress = [];
72
+    this.counter = 0;
73
+
74
+    const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
75
+    this.state = {
76
+      dataSource: ds.cloneWithRows(this._genRows({}))
77
+    }
78
+  }
79
+
80
+  onNavigatorEvent(event) {
81
+    console.log('ListScreen', 'onNavigatorEvent ', event.id);
82
+    if (event.id === 'didAppear' && __STRESS_TEST__) {
83
+      setTimeout(() => {
84
+        this.onPress[Math.floor((Math.random() * 4))]();
85
+        console.log('count', this.counter++);
86
+      }, 750);
87
+    }
88
+  }
89
+
90
+  render() {
91
+    return (
92
+      <ListView
93
+        dataSource={this.state.dataSource}
94
+        renderRow={(rowData, sectionID, rowID) => this._renderRow(rowData, sectionID, rowID)}/>
95
+    );
96
+  }
97
+
98
+  _renderRow(rowData, sectionID, rowID) {
99
+    console.log('ListScreen', '_renderRow ', rowID);
100
+    const rowHash = Math.abs(hashCode(rowData.rowTitle));
101
+    const sharedIconId = "image" + rowID;
102
+    const sharedTitleId = "title" + rowID;
103
+
104
+    const onPress = () => this._onRowPress({
105
+      rowData,
106
+      sharedIconId,
107
+      sharedTitleId
108
+    });
109
+    if (['0', '1', '2', '3'].includes(rowID)) {
110
+      this.onPress.push(onPress);
111
+    }
112
+    return (
113
+      <TouchableWithoutFeedback
114
+        style={styles.row}
115
+        onPress={onPress}
116
+      >
117
+        <View style={[styles.sharedRowContent, heroStyles.primaryLight]}>
118
+          {this._renderRowIcon({rowData, sharedIconId})}
119
+          {this._renderRowContent({rowData, sharedTitleId, rowHash})}
120
+        </View>
121
+      </TouchableWithoutFeedback>
122
+    );
123
+  }
124
+
125
+  _renderRowIcon({rowData, sharedIconId}) {
126
+    return (
127
+      <View>
128
+        <SharedElementTransition
129
+          key={sharedIconId}
130
+          sharedElementId={sharedIconId}
131
+          style={styles.imageContainer}
132
+        >
133
+          <Image
134
+            source={rowData.icon}
135
+            style={styles.heroIcon}
136
+            fadeDuration={0}
137
+          />
138
+        </SharedElementTransition>
139
+      </View>
140
+    );
141
+  }
142
+
143
+  _renderRowContent({rowData, sharedTitleId, rowHash}) {
144
+    return (
145
+      <View style={styles.itemContentContainer}>
146
+        <SharedElementTransition sharedElementId={sharedTitleId}>
147
+          <Text style={styles.itemTitle}>{rowData.title}</Text>
148
+        </SharedElementTransition>
149
+        <Text style={styles.text}>
150
+          {rowData.rowTitle + ' - ' + LOREM_IPSUM.substr(0, rowHash % 301 + 10)}
151
+        </Text>
152
+      </View>
153
+    );
154
+  }
155
+
156
+  _onRowPress(data) {
157
+    const {rowData, sharedIconId, sharedTitleId} = data;
158
+    this.props.navigator.push({
159
+      screen: 'example.HeroScreen',
160
+      sharedElements: [
161
+        data.sharedElementId,
162
+        data.sharedElementId + 'container'
163
+      ],
164
+      animated: false,
165
+      overrideBackPress: true,
166
+      passProps: {
167
+        sharedIconId: sharedIconId,
168
+        sharedTitleId: sharedTitleId,
169
+        ...rowData
170
+      }
171
+    });
172
+  }
173
+
174
+  _genRows() {
175
+    const dataBlob = [];
176
+    for (let ii = 0; ii < 100; ii++) {
177
+      dataBlob.push({
178
+        rowTitle: 'Row ' + ii + ' ',
179
+        icon: heroes[ii % 5].icon,
180
+        title: heroes[ii % 5].title,
181
+        primaryColor: heroes[ii % 5].primaryColor,
182
+        titleColor: heroes[ii % 5].titleColor
183
+      });
184
+    }
185
+    return dataBlob;
186
+  }
187
+
188
+  _renderSeparator(sectionID, rowID) {
189
+    return (
190
+      <View
191
+        key={`${sectionID}-${rowID}`}
192
+        style={{
193
+          height: 1,
194
+          backgroundColor: rowID % 2 == 0 ? '#3B5998' : '#CCCCCC'
195
+        }}
196
+      />
197
+    );
198
+  }
199
+}
200
+
201
+const styles = StyleSheet.create({
202
+  row: {
203
+    padding: 5,
204
+    height: 110,
205
+    flexDirection: 'row',
206
+    justifyContent: 'center',
207
+    borderWidth: 1,
208
+  },
209
+  sharedRowContent: {
210
+    flexDirection: 'row',
211
+  },
212
+  imageContainer: {
213
+    justifyContent: 'center'
214
+  },
215
+  heroIcon: {
216
+    width: 100,
217
+    height: 100,
218
+    resizeMode: 'contain'
219
+  },
220
+  itemContentContainer: {
221
+    flex: 1,
222
+    paddingLeft: 5,
223
+    flexDirection: 'column',
224
+    alignItems: 'flex-start'
225
+  },
226
+  itemTitle: {
227
+    fontSize: 19,
228
+    ...heroStyles.textDark
229
+  },
230
+  text: {
231
+    ...heroStyles.textDark
232
+  }
233
+});
234
+
235
+

+ 34
- 0
example/src/screens/set/heroes.js View File

@@ -0,0 +1,34 @@
1
+import * as heroStyles from './styles';
2
+
3
+export default heroes = [
4
+  {
5
+    title: 'Bounty Hunter',
6
+    icon: require('../../../img/heroes/bouny_hunter.png'),
7
+    primaryColor: '#f0cb3c',
8
+    titleColor: '#993825'
9
+  },
10
+  {
11
+    title: 'Templar Assasin',
12
+    icon: require('../../../img/heroes/templar_assasin.png'),
13
+    primaryColor: '#f6f6f6',
14
+    titleColor: heroStyles.textDark.color
15
+  },
16
+  {
17
+    title: 'Oracle',
18
+    icon: require('../../../img/heroes/oracle.png'),
19
+    primaryColor: '#19b0b9',
20
+    titleColor: '#a2195b'
21
+  },
22
+  {
23
+    title: 'Earthspirit',
24
+    icon: require('../../../img/heroes/earthspirit.png'),
25
+    primaryColor: '#819c97',
26
+    titleColor: heroStyles.textDark.color
27
+  },
28
+  {
29
+    title: 'Skywrath Mage',
30
+    icon: require('../../../img/heroes/skywrath_mage.png'),
31
+    primaryColor: '#dfb42e',
32
+    titleColor: '#1e5ea6'
33
+  }
34
+];

+ 26
- 0
example/src/screens/set/styles.js View File

@@ -0,0 +1,26 @@
1
+export const navigatorStyle = {
2
+  navBarHideOnScroll: true,
3
+  drawUnderTabBar: true,
4
+  navBarButtonColor: '#cacaca',
5
+  navBarBackgroundColor: '#31363c',
6
+  navBarTextColor: '#cacaca',
7
+  tabBarBackgroundColor: '#31363c',
8
+  tabBarButtonColor: '#cacaca',
9
+  tabBarSelectedButtonColor: '#cacaca'
10
+};
11
+
12
+export const primaryDark = {
13
+  backgroundColor: '#1f2222'
14
+};
15
+
16
+export const primaryLight = {
17
+  backgroundColor: '#cacaca'
18
+};
19
+
20
+export const textLight = {
21
+  color: '#cacaca'
22
+};
23
+
24
+export const textDark = {
25
+  color: '#1f2222'
26
+};

+ 1
- 1
package.json View File

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "react-native-navigation",
3
-  "version": "2.0.0-experimental.0",
3
+  "version": "2.0.0-legacy.2",
4 4
   "description": "React Native Navigation - truly native navigation for iOS and Android",
5 5
   "license": "MIT",
6 6
   "nativePackage": true,

+ 3
- 1
src/deprecated/indexDeprecated.android.js View File

@@ -1,6 +1,8 @@
1 1
 import Navigation from './../Navigation';
2
+import SharedElementTransition from './../views/sharedElementTransition';
2 3
 
3 4
 module.exports = {
4
-  Navigation
5
+  Navigation,
6
+  SharedElementTransition
5 7
 };
6 8
 

+ 2
- 1
src/deprecated/platformSpecificDeprecated.android.js View File

@@ -1,6 +1,6 @@
1 1
 /*eslint-disable*/
2 2
 import React, {Component} from 'react';
3
-import {AppRegistry, NativeModules, processColor} from 'react-native';
3
+import ReactNative, {AppRegistry, NativeModules, processColor} from 'react-native';
4 4
 import _ from 'lodash';
5 5
 
6 6
 import Navigation from './../Navigation';
@@ -73,6 +73,7 @@ function navigatorPush(navigator, params) {
73 73
   addNavigationStyleParams(params);
74 74
 
75 75
   adaptTopTabs(params, params.navigatorID);
76
+  // findSharedElementsNodeHandles(params);
76 77
 
77 78
   params.screenId = params.screen;
78 79
   let adapted = adaptNavigationStyleToScreenStyle(params);

+ 12
- 0
src/views/sharedElementTransition.js View File

@@ -0,0 +1,12 @@
1
+import React, {Component} from 'react';
2
+import {
3
+  requireNativeComponent
4
+} from 'react-native';
5
+
6
+const SharedElementTransitionModule = requireNativeComponent('SharedElementTransition', null);
7
+
8
+export default class SharedElementTransition extends React.Component {
9
+  render() {
10
+    return <SharedElementTransitionModule {...this.props} />;
11
+  }
12
+}