Browse Source

SharedElement bounds and scale animator (#1096)

Animate image scale and bounds

This commit properly animates images with different aspect ratios.
By default, regular scale animator is used which scales up or down the entire ImageView.
If `animateClipBounds={true}` is specified on the **to** `SharedElementTransition`, instead of scaling the View, the actual Drawable bounds and scale is animated which prevents the image from appearing distorted during the transition.
Guy Carmeli 7 years ago
parent
commit
c44599732d

+ 1
- 1
android/app/build.gradle View File

47
 dependencies {
47
 dependencies {
48
     compile fileTree(dir: "libs", include: ["*.jar"])
48
     compile fileTree(dir: "libs", include: ["*.jar"])
49
 
49
 
50
-	  compile 'com.android.support:design:25.0.1'
50
+	compile 'com.android.support:design:25.0.1'
51
     compile "com.android.support:appcompat-v7:25.0.1"
51
     compile "com.android.support:appcompat-v7:25.0.1"
52
 
52
 
53
     // node_modules
53
     // node_modules

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

168
     public static Point getLocationOnScreen(View view) {
168
     public static Point getLocationOnScreen(View view) {
169
         int[] xy = new int[2];
169
         int[] xy = new int[2];
170
         view.getLocationOnScreen(xy);
170
         view.getLocationOnScreen(xy);
171
-        xy[1] -= getStatusBarPixelHeight();
171
+        xy[1] -= getStatusBarHeight();
172
         return new Point(xy[0], xy[1]);
172
         return new Point(xy[0], xy[1]);
173
     }
173
     }
174
 
174
 
175
-    private static int getStatusBarPixelHeight() {
175
+    private static int getStatusBarHeight() {
176
         if (statusBarHeight > 0) {
176
         if (statusBarHeight > 0) {
177
             return statusBarHeight;
177
             return statusBarHeight;
178
         }
178
         }

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

3
 import android.graphics.Rect;
3
 import android.graphics.Rect;
4
 import android.widget.TextView;
4
 import android.widget.TextView;
5
 
5
 
6
+import com.facebook.drawee.drawable.ScalingUtils;
7
+import com.facebook.drawee.generic.GenericDraweeHierarchy;
8
+import com.facebook.drawee.view.DraweeView;
9
+import com.facebook.react.views.image.ReactImageView;
6
 import com.reactnativenavigation.params.InterpolationParams;
10
 import com.reactnativenavigation.params.InterpolationParams;
7
 import com.reactnativenavigation.params.PathInterpolationParams;
11
 import com.reactnativenavigation.params.PathInterpolationParams;
8
 import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
12
 import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
9
 import com.reactnativenavigation.utils.ViewUtils;
13
 import com.reactnativenavigation.utils.ViewUtils;
10
 import com.reactnativenavigation.views.utils.Point;
14
 import com.reactnativenavigation.views.utils.Point;
11
 
15
 
12
-class AnimatorValuesResolver {
16
+public class AnimatorValuesResolver {
13
 
17
 
14
     final Point fromXy;
18
     final Point fromXy;
15
     final Point toXy;
19
     final Point toXy;
31
     int endColor;
35
     int endColor;
32
     Rect startDrawingRect = new Rect();
36
     Rect startDrawingRect = new Rect();
33
     Rect endDrawingRect = new Rect();
37
     Rect endDrawingRect = new Rect();
38
+    final Rect fromBounds;
39
+    final Rect toBounds;
40
+    final ScalingUtils.ScaleType fromScaleType;
41
+    final ScalingUtils.ScaleType toScaleType;
34
 
42
 
35
     AnimatorValuesResolver(SharedElementTransition from, SharedElementTransition to, SharedElementTransitionParams params) {
43
     AnimatorValuesResolver(SharedElementTransition from, SharedElementTransition to, SharedElementTransitionParams params) {
36
-        fromXy = calculateFromXY(from, to, params);
37
-        toXy = calculateToXY(to, from, params);
44
+        fromXy = ViewUtils.getLocationOnScreen(from.getSharedView());
45
+        toXy = ViewUtils.getLocationOnScreen(to.getSharedView());
38
         startScaleX = calculateStartScaleX(from, to);
46
         startScaleX = calculateStartScaleX(from, to);
39
         endScaleX = calculateEndScaleX(from, to);
47
         endScaleX = calculateEndScaleX(from, to);
40
         startScaleY = calculateStartScaleY(from, to);
48
         startScaleY = calculateStartScaleY(from, to);
41
         endScaleY = calculateEndScaleY(from, to);
49
         endScaleY = calculateEndScaleY(from, to);
42
         calculateColor(from, to);
50
         calculateColor(from, to);
43
         calculate(params.interpolation);
51
         calculate(params.interpolation);
52
+        fromBounds = calculateBounds(from);
53
+        toBounds = calculateBounds(to);
54
+        fromScaleType = getScaleType(from);
55
+        toScaleType = getScaleType(to);
44
         calculateDrawingReacts(from, to);
56
         calculateDrawingReacts(from, to);
45
     }
57
     }
46
 
58
 
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
-            }
59
+    private ScalingUtils.ScaleType getScaleType(SharedElementTransition view) {
60
+        if (view.getSharedView() instanceof ReactImageView) {
61
+            return ((DraweeView<GenericDraweeHierarchy>) view.getSharedView()).getHierarchy().getActualImageScaleType();
55
         }
62
         }
56
-        return loc;
63
+        return null;
57
     }
64
     }
58
 
65
 
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
-            }
66
+    private Rect calculateBounds(SharedElementTransition view) {
67
+        if (view.getSharedView() instanceof ReactImageView) {
68
+            return new Rect(0, 0, view.getSharedView().getWidth(), view.getSharedView().getHeight());
67
         }
69
         }
68
-        return loc;
70
+        return null;
69
     }
71
     }
70
 
72
 
71
-
72
     protected float calculateEndScaleY(SharedElementTransition from, SharedElementTransition to) {
73
     protected float calculateEndScaleY(SharedElementTransition from, SharedElementTransition to) {
73
         return 1;
74
         return 1;
74
     }
75
     }
122
             endColor = ViewUtils.getForegroundColorSpans((TextView) to.getSharedView())[0].getForegroundColor();
123
             endColor = ViewUtils.getForegroundColorSpans((TextView) to.getSharedView())[0].getForegroundColor();
123
         }
124
         }
124
     }
125
     }
125
-
126
     private void calculateDrawingReacts(SharedElementTransition from, SharedElementTransition to) {
126
     private void calculateDrawingReacts(SharedElementTransition from, SharedElementTransition to) {
127
         from.getDrawingRect(startDrawingRect);
127
         from.getDrawingRect(startDrawingRect);
128
         to.getDrawingRect(endDrawingRect);
128
         to.getDrawingRect(endDrawingRect);

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

5
 import android.support.annotation.NonNull;
5
 import android.support.annotation.NonNull;
6
 import android.view.View;
6
 import android.view.View;
7
 
7
 
8
+import com.facebook.drawee.drawable.ScalingUtils;
9
+import com.facebook.drawee.generic.GenericDraweeHierarchy;
10
+import com.facebook.drawee.view.DraweeView;
8
 import com.reactnativenavigation.params.InterpolationParams;
11
 import com.reactnativenavigation.params.InterpolationParams;
9
 import com.reactnativenavigation.params.PathInterpolationParams;
12
 import com.reactnativenavigation.params.PathInterpolationParams;
10
 import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
13
 import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
49
                 result.add(createYAnimator(resolver, params));
52
                 result.add(createYAnimator(resolver, params));
50
             }
53
             }
51
         }
54
         }
52
-        if (shouldCreateScaleXAnimator(resolver, params)) {
55
+        if (shouldAddScaleXAnimator(resolver, params)) {
53
             result.add(createScaleXAnimator(resolver, params));
56
             result.add(createScaleXAnimator(resolver, params));
54
         }
57
         }
55
-        if (shouldCreateScaleYAnimator(resolver, params)) {
58
+        if (shouldAddScaleYAnimator(resolver, params)) {
56
             result.add(createScaleYAnimator(resolver, params));
59
             result.add(createScaleYAnimator(resolver, params));
57
         }
60
         }
58
-        if (shouldCreateColorAnimator(resolver)) {
61
+        if (shouldAddColorAnimator(resolver)) {
59
             result.add(createColorAnimator(resolver, params.duration));
62
             result.add(createColorAnimator(resolver, params.duration));
60
         }
63
         }
61
-        if (shouldCreateImageClipBoundsAnimator(params)) {
64
+        if (shouldAddImageClipBoundsAnimator(params)) {
62
             result.add(createImageClipBoundsAnimator(resolver, params.duration));
65
             result.add(createImageClipBoundsAnimator(resolver, params.duration));
66
+            result.add(createImageTransformAnimator(resolver, params));
63
         }
67
         }
64
         return result;
68
         return result;
65
     }
69
     }
66
 
70
 
67
-    private boolean shouldCreateScaleYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
71
+    private boolean shouldAddScaleYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
68
         return resolver.startScaleY != resolver.endScaleY && !params.animateClipBounds;
72
         return resolver.startScaleY != resolver.endScaleY && !params.animateClipBounds;
69
     }
73
     }
70
 
74
 
71
-    private boolean shouldCreateScaleXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
75
+    private boolean shouldAddScaleXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
72
         return resolver.startScaleX != resolver.endScaleX && !params.animateClipBounds;
76
         return resolver.startScaleX != resolver.endScaleX && !params.animateClipBounds;
73
     }
77
     }
74
 
78
 
75
     private boolean shouldAddLinearMotionXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
79
     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
-        }
80
+        return params.animateClipBounds || resolver.dx != 0;
81
     }
81
     }
82
 
82
 
83
     private boolean shouldAddLinearMotionYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
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
-        }
84
+        return params.animateClipBounds || resolver.dy != 0;
89
     }
85
     }
90
 
86
 
91
     private boolean shouldAddCurvedMotionAnimator(AnimatorValuesResolver resolver, InterpolationParams interpolation) {
87
     private boolean shouldAddCurvedMotionAnimator(AnimatorValuesResolver resolver, InterpolationParams interpolation) {
92
         return interpolation instanceof PathInterpolationParams && (resolver.dx != 0 || resolver.dy != 0);
88
         return interpolation instanceof PathInterpolationParams && (resolver.dx != 0 || resolver.dy != 0);
93
     }
89
     }
94
 
90
 
95
-    private boolean shouldCreateColorAnimator(AnimatorValuesResolver resolver) {
91
+    private boolean shouldAddColorAnimator(AnimatorValuesResolver resolver) {
96
         return resolver.startColor != resolver.endColor;
92
         return resolver.startColor != resolver.endColor;
97
     }
93
     }
98
 
94
 
99
-    private boolean shouldCreateImageClipBoundsAnimator(SharedElementTransitionParams params) {
95
+    private boolean shouldAddImageClipBoundsAnimator(SharedElementTransitionParams params) {
100
         return params.animateClipBounds;
96
         return params.animateClipBounds;
101
     }
97
     }
102
 
98
 
130
 
126
 
131
     private ObjectAnimator createScaleXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
127
     private ObjectAnimator createScaleXAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
132
         to.getSharedView().setPivotX(0);
128
         to.getSharedView().setPivotX(0);
133
-        ObjectAnimator animator = ofFloat(to.getSharedView(), View.SCALE_X, resolver.startScaleX, resolver.endScaleX)
134
-                .setDuration(params.duration);
129
+        ObjectAnimator animator =
130
+                ofFloat(to.getSharedView(), View.SCALE_X, resolver.startScaleX, resolver.endScaleX)
131
+                        .setDuration(params.duration);
135
         animator.setInterpolator(params.interpolation.easing.getInterpolator());
132
         animator.setInterpolator(params.interpolation.easing.getInterpolator());
136
         return animator;
133
         return animator;
137
     }
134
     }
138
 
135
 
139
     private ObjectAnimator createScaleYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
136
     private ObjectAnimator createScaleYAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
140
         to.getSharedView().setPivotY(0);
137
         to.getSharedView().setPivotY(0);
141
-        ObjectAnimator animator = ofFloat(to.getSharedView(), View.SCALE_Y, resolver.startScaleY, resolver.endScaleY)
142
-                .setDuration(params.duration);
138
+        ObjectAnimator animator =
139
+                ofFloat(to.getSharedView(), View.SCALE_Y, resolver.startScaleY, resolver.endScaleY)
140
+                        .setDuration(params.duration);
143
         animator.setInterpolator(params.interpolation.easing.getInterpolator());
141
         animator.setInterpolator(params.interpolation.easing.getInterpolator());
144
         return animator;
142
         return animator;
145
     }
143
     }
163
                 resolver.endDrawingRect)
161
                 resolver.endDrawingRect)
164
                 .setDuration(duration);
162
                 .setDuration(duration);
165
     }
163
     }
164
+    private Animator createImageTransformAnimator(AnimatorValuesResolver resolver, SharedElementTransitionParams params) {
165
+        ScalingUtils.InterpolatingScaleType ist = new ScalingUtils.InterpolatingScaleType(
166
+                resolver.fromScaleType,
167
+                resolver.toScaleType,
168
+                resolver.fromBounds,
169
+                resolver.toBounds
170
+        );
171
+        ((DraweeView<GenericDraweeHierarchy>) to.getSharedView()).getHierarchy().setActualImageScaleType(ist);
172
+        ObjectAnimator animator = ObjectAnimator.ofFloat(to, "matrixTransform", 0, 1).setDuration(params.duration);
173
+        animator.setInterpolator(params.interpolation.easing.getInterpolator());
174
+        return animator;
175
+    }
166
 }
176
 }

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

13
 import android.widget.RelativeLayout;
13
 import android.widget.RelativeLayout;
14
 import android.widget.TextView;
14
 import android.widget.TextView;
15
 
15
 
16
+import com.facebook.drawee.drawable.ScalingUtils;
17
+import com.facebook.drawee.generic.GenericDraweeHierarchy;
18
+import com.facebook.drawee.view.DraweeView;
16
 import com.facebook.react.views.image.ReactImageView;
19
 import com.facebook.react.views.image.ReactImageView;
17
 import com.reactnativenavigation.params.parsers.SharedElementParamsParser;
20
 import com.reactnativenavigation.params.parsers.SharedElementParamsParser;
18
 import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
21
 import com.reactnativenavigation.params.parsers.SharedElementTransitionParams;
107
         }
110
         }
108
     }
111
     }
109
 
112
 
113
+    @Keep
114
+    public void setMatrixTransform(float value) {
115
+        GenericDraweeHierarchy hierarchy = ((DraweeView<GenericDraweeHierarchy>) child).getHierarchy();
116
+        ((ScalingUtils.InterpolatingScaleType) hierarchy.getActualImageScaleType()).setValue(value);
117
+        child.invalidate();
118
+    }
119
+
110
     public void attachChildToScreen() {
120
     public void attachChildToScreen() {
111
         ViewUtils.performOnParentScreen(this, new Task<Screen>() {
121
         ViewUtils.performOnParentScreen(this, new Task<Screen>() {
112
             @Override
122
             @Override

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

18
             result.bottom = toHeight;
18
             result.bottom = toHeight;
19
         } else {
19
         } else {
20
             if (toHeight > fromHeight) {
20
             if (toHeight > fromHeight) {
21
-                result.top = (int) (Math.abs(toHeight - fromHeight) / 2 * (1 - ratio));
22
-                result.bottom = toHeight- result.top;
21
+                result.bottom = (int) (toHeight - (toHeight - fromHeight) * (1 - ratio));
23
             } else {
22
             } else {
24
-                result.top = (int) (Math.abs(toHeight - fromHeight) / 2 * ratio);
25
-                result.bottom = fromHeight - result.top;
23
+                result.bottom = (int) (toHeight + (fromHeight - toHeight) * (1 - ratio));
26
             }
24
             }
27
         }
25
         }
28
 
26
 
30
             result.right = toWidth;
28
             result.right = toWidth;
31
         } else {
29
         } else {
32
             if (toWidth > fromWidth) {
30
             if (toWidth > fromWidth) {
33
-                result.left = (int) (Math.abs(toWidth - fromWidth) / 2 * (1 - ratio));
34
-                result.right = toWidth - result.left;
31
+                result.right = (int) (toWidth - (toWidth - fromWidth) * (1 - ratio));
35
             } else {
32
             } else {
36
-                result.left = (int) (Math.abs(toWidth - fromWidth) / 2 * ratio);
37
-                result.right = fromWidth - result.left;
33
+                result.right = (int) (toWidth + (fromWidth - toWidth) * (1 - ratio));
38
             }
34
             }
39
         }
35
         }
40
         return result;
36
         return result;