Skip to content

Commit

Permalink
Support declaring interpolation per shared element transition (#6139)
Browse files Browse the repository at this point in the history
* Implement startDelay + fix image scale transition

This commit adds support to the startDelay option in shared element transitions on Android. It also fixes image scale transition which only animated the image's scale type, but not its bounds.

* Support declaring interpolator per element transition
  • Loading branch information
guyca authored Apr 20, 2020
1 parent a65a7e9 commit e80eb92
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.reactnativenavigation.parse;

import com.reactnativenavigation.parse.params.Interpolation;
import com.reactnativenavigation.parse.params.NullNumber;
import com.reactnativenavigation.parse.params.NullText;
import com.reactnativenavigation.parse.params.Number;
import com.reactnativenavigation.parse.params.Text;
import com.reactnativenavigation.parse.parsers.InterpolationParser;
import com.reactnativenavigation.parse.parsers.NumberParser;
import com.reactnativenavigation.parse.parsers.TextParser;

Expand All @@ -15,6 +17,8 @@ public class SharedElementTransitionOptions {
public Text fromId = new NullText();
public Text toId = new NullText();
public Number duration = new NullNumber();
public Number startDelay = new NullNumber();
public Interpolation interpolation = Interpolation.NO_VALUE;

public static SharedElementTransitionOptions parse(@Nullable JSONObject json) {
SharedElementTransitionOptions transition = new SharedElementTransitionOptions();
Expand All @@ -23,6 +27,8 @@ public static SharedElementTransitionOptions parse(@Nullable JSONObject json) {
transition.fromId = TextParser.parse(json, "fromId");
transition.toId = TextParser.parse(json, "toId");
transition.duration = NumberParser.parse(json, "duration");
transition.startDelay = NumberParser.parse(json, "startDelay");
transition.interpolation = InterpolationParser.parse(json, "interpolation");

return transition;
}
Expand All @@ -31,15 +37,21 @@ void mergeWith(SharedElementTransitionOptions other) {
if (other.fromId.hasValue()) fromId = other.fromId;
if (other.toId.hasValue()) toId = other.toId;
if (other.duration.hasValue()) duration = other.duration;
if (other.startDelay.hasValue()) startDelay = other.startDelay;
}

void mergeWithDefault(SharedElementTransitionOptions defaultOptions) {
if (!fromId.hasValue()) fromId = defaultOptions.fromId;
if (!toId.hasValue()) toId = defaultOptions.toId;
if (!duration.hasValue()) duration = defaultOptions.duration;
if (!startDelay.hasValue()) startDelay = defaultOptions.startDelay;
}

public long getDuration() {
return duration.get(0).longValue();
}

public long getStartDelay() {
return startDelay.get(0).longValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.reactnativenavigation.utils

import android.animation.Animator
import android.animation.TimeInterpolator
import android.view.animation.Interpolator

fun Animator.withStartDelay(delay: Long): Animator {
startDelay = delay
return this
}

fun Animator.withDuration(duration: Long): Animator {
this.duration = duration
return this
}

fun Animator.withInterpolator(interpolator: TimeInterpolator): Animator {
this.interpolator = interpolator
return this
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ class SharedElementTransition(appearing: ViewController<*>, private val options:

private fun animators(): List<PropertyAnimatorCreator<*>> {
return listOf(
MatrixAnimator(from, to),
ClipBoundsAnimator(from, to),
XAnimator(from, to),
YAnimator(from, to),
MatrixAnimator(from, to),
ScaleXAnimator(from, to),
ScaleYAnimator(from, to),
BackgroundColorAnimator(from, to),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.widget.FrameLayout
import androidx.core.animation.doOnCancel
import androidx.core.animation.doOnEnd
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.views.image.ReactImageView
import com.reactnativenavigation.R
import com.reactnativenavigation.parse.AnimationOptions
import com.reactnativenavigation.utils.ViewTags
Expand Down Expand Up @@ -51,9 +52,7 @@ open class TransitionAnimatorCreator {
private fun reparentViews(transitions: TransitionSet) {
transitions.transitions
.sortedBy { ViewGroupManager.getViewZIndex(it.view) }
.forEach {
reparent(it)
}
.forEach { reparent(it) }
}

private fun createSharedElementTransitionAnimators(transitions: List<SharedElementTransition>): List<AnimatorSet> {
Expand All @@ -65,14 +64,15 @@ open class TransitionAnimatorCreator {
}

private fun createSharedElementAnimator(transition: SharedElementTransition): AnimatorSet {
val set = AnimatorSet()
set.playTogether(transition.createAnimators())
set.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
transition.from.alpha = 0f
}
})
return set
return transition
.createAnimators()
.apply {
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
transition.from.alpha = 0f
}
})
}
}

private fun createElementTransitionAnimators(transitions: List<ElementTransition>): List<AnimatorSet> {
Expand Down Expand Up @@ -116,8 +116,10 @@ open class TransitionAnimatorCreator {
lp.topMargin = loc.y + viewController.topInset
lp.topMargin = loc.y
lp.leftMargin = loc.x
lp.width = view.width
lp.height = view.height
if (view !is ReactImageView) {
lp.width = view.width
lp.height = view.height
}
view.layoutParams = lp
transition.viewController.requireParentController().addOverlay(view)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.reactnativenavigation.views.element.animators

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import androidx.core.animation.addListener
import androidx.core.animation.doOnStart
import com.facebook.react.views.text.ReactTextView
import com.facebook.react.views.view.ReactViewBackgroundDrawable
import com.reactnativenavigation.parse.SharedElementTransitionOptions
import com.reactnativenavigation.utils.ColorUtils
import com.reactnativenavigation.utils.ViewUtils
import com.reactnativenavigation.utils.withInterpolator
import com.reactnativenavigation.utils.withStartDelay

class BackgroundColorAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(from, to) {
override fun shouldAnimateProperty(fromChild: ViewGroup, toChild: ViewGroup): Boolean {
Expand All @@ -19,10 +24,19 @@ class BackgroundColorAnimator(from: View, to: View) : PropertyAnimatorCreator<Vi
override fun excludedViews() = listOf(ReactTextView::class.java)

override fun create(options: SharedElementTransitionOptions): Animator {
return ObjectAnimator.ofObject(
BackgroundColorEvaluator(to.background as ReactViewBackgroundDrawable),
ColorUtils.colorToLAB(ViewUtils.getBackgroundColor(from)),
ColorUtils.colorToLAB(ViewUtils.getBackgroundColor(to))
).setDuration(options.getDuration())
val backgroundColorEvaluator = BackgroundColorEvaluator(to.background as ReactViewBackgroundDrawable)
val fromColor = ColorUtils.colorToLAB(ViewUtils.getBackgroundColor(from))
val toColor = ColorUtils.colorToLAB(ViewUtils.getBackgroundColor(to))

backgroundColorEvaluator.evaluate(0f, fromColor, toColor)
return ObjectAnimator
.ofObject(
backgroundColorEvaluator,
fromColor,
toColor
)
.setDuration(options.getDuration())
.withStartDelay(options.getStartDelay())
.withInterpolator(options.interpolation.interpolator)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.reactnativenavigation.views.element.animators

import android.animation.Animator
import android.animation.ObjectAnimator
import android.graphics.Rect
import android.view.View
import com.facebook.react.views.image.ReactImageView
import com.reactnativenavigation.parse.SharedElementTransitionOptions
import com.reactnativenavigation.utils.ViewUtils
import com.reactnativenavigation.utils.withDuration
import com.reactnativenavigation.utils.withInterpolator
import com.reactnativenavigation.utils.withStartDelay

class ClipBoundsAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactImageView>(from, to) {
override fun shouldAnimateProperty(fromChild: ReactImageView, toChild: ReactImageView): Boolean {
return !ViewUtils.areDimensionsEqual(from, to)
}

override fun create(options: SharedElementTransitionOptions): Animator {
val startDrawingRect = Rect(); from.getDrawingRect(startDrawingRect)
val endDrawingRect = Rect(); to.getDrawingRect(endDrawingRect)
return ObjectAnimator.ofObject(
ClipBoundsEvaluator(),
startDrawingRect,
endDrawingRect
)
.setDuration(options.getDuration())
.withStartDelay(options.getStartDelay())
.withInterpolator(options.interpolation.interpolator)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.reactnativenavigation.views.element.animators

import android.animation.TypeEvaluator
import android.graphics.Rect

class ClipBoundsEvaluator : TypeEvaluator<Rect> {
private var fromWidth = 0
private var fromHeight = 0
private var toWidth = 0
private var toHeight = 0
private val result = Rect()

override fun evaluate(ratio: Float, from: Rect, to: Rect): Rect {
sync(from, to)
if (toHeight == fromHeight) {
result.bottom = toHeight
} else {
if (toHeight > fromHeight) {
result.bottom = (toHeight - (toHeight - fromHeight) * (1 - ratio)).toInt()
} else {
result.bottom = (toHeight + (fromHeight - toHeight) * (1 - ratio)).toInt()
}
}
if (toWidth == fromWidth) {
result.right = toWidth
} else {
if (toWidth > fromWidth) {
result.right = (toWidth - (toWidth - fromWidth) * (1 - ratio)).toInt()
} else {
result.right = (toWidth + (fromWidth - toWidth) * (1 - ratio)).toInt()
}
}
return result
}

private fun sync(from: Rect, to: Rect) {
fromWidth = from.right
fromHeight = from.bottom
toWidth = to.right
toHeight = to.bottom
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package com.reactnativenavigation.views.element.animators

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.TypeEvaluator
import android.graphics.Rect
import android.view.View
import androidx.core.animation.addListener
import androidx.core.animation.doOnStart
import com.facebook.drawee.drawable.ScalingUtils.InterpolatingScaleType
import com.facebook.react.views.image.ReactImageView
import com.reactnativenavigation.parse.SharedElementTransitionOptions
import com.reactnativenavigation.utils.ViewUtils
import com.reactnativenavigation.utils.withDuration
import com.reactnativenavigation.utils.withInterpolator
import com.reactnativenavigation.utils.withStartDelay

class MatrixAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactImageView>(from, to) {
override fun shouldAnimateProperty(fromChild: ReactImageView, toChild: ReactImageView): Boolean {
Expand All @@ -23,13 +29,18 @@ class MatrixAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactImageV
calculateBounds(from),
calculateBounds(to)
)
return ObjectAnimator.ofObject(TypeEvaluator<Float> { fraction: Float, _: Any, _: Any ->
hierarchy.actualImageScaleType?.let {
(hierarchy.actualImageScaleType as InterpolatingScaleType?)!!.value = fraction
to.invalidate()
}
null
}, 0, 1).setDuration(options.getDuration())

return ObjectAnimator
.ofObject(TypeEvaluator<Float> { fraction: Float, _: Any, _: Any ->
hierarchy.actualImageScaleType?.let {
(hierarchy.actualImageScaleType as InterpolatingScaleType?)!!.value = fraction
to.invalidate()
}
null
}, 0, 1)
.setDuration(options.getDuration())
.withStartDelay(options.getStartDelay())
.withInterpolator(options.interpolation.interpolator)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import androidx.core.animation.addListener
import androidx.core.animation.doOnStart
import com.facebook.react.views.text.ReactTextView
import com.reactnativenavigation.parse.SharedElementTransitionOptions
import com.reactnativenavigation.utils.withDuration
import com.reactnativenavigation.utils.withInterpolator
import com.reactnativenavigation.utils.withStartDelay

class ScaleXAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(from, to) {
override fun shouldAnimateProperty(fromChild: ViewGroup, toChild: ViewGroup): Boolean {
Expand All @@ -15,8 +20,11 @@ class ScaleXAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(
override fun excludedViews(): List<Class<*>> = listOf<Class<*>>(ReactTextView::class.java)

override fun create(options: SharedElementTransitionOptions): Animator {
to.scaleX = from.width.toFloat() / to.width
return ObjectAnimator
.ofFloat(to, View.SCALE_X, from.width.toFloat() / to.width, 1f)
.setDuration(options.getDuration())
.withStartDelay(options.getStartDelay())
.withInterpolator(options.interpolation.interpolator)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import androidx.core.animation.addListener
import androidx.core.animation.doOnStart
import com.facebook.react.views.text.ReactTextView
import com.reactnativenavigation.parse.SharedElementTransitionOptions
import com.reactnativenavigation.utils.withDuration
import com.reactnativenavigation.utils.withInterpolator
import com.reactnativenavigation.utils.withStartDelay

class ScaleYAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(from, to) {
override fun shouldAnimateProperty(fromChild: ViewGroup, toChild: ViewGroup): Boolean {
Expand All @@ -15,8 +20,11 @@ class ScaleYAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(
override fun excludedViews() = listOf(ReactTextView::class.java)

override fun create(options: SharedElementTransitionOptions): Animator {
to.scaleY = from.height.toFloat() / to.height
return ObjectAnimator
.ofFloat(to, View.SCALE_Y, from.height.toFloat() / to.height, 1f)
.setDuration(options.getDuration())
.withStartDelay(options.getStartDelay())
.withInterpolator(options.interpolation.interpolator)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import android.view.ViewGroup
import android.widget.TextView
import com.facebook.react.views.text.ReactTextView
import com.reactnativenavigation.parse.SharedElementTransitionOptions
import com.reactnativenavigation.utils.TextViewUtils
import com.reactnativenavigation.utils.ViewUtils
import com.reactnativenavigation.utils.*
import com.shazam.android.widget.text.reflow.ReflowTextAnimatorHelper

class TextChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactTextView>(from, to) {
Expand Down Expand Up @@ -39,5 +38,6 @@ class TextChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactTe
}
.buildAnimator()
.setDuration(options.getDuration())
.withInterpolator(options.interpolation.interpolator)
}
}
Loading

0 comments on commit e80eb92

Please sign in to comment.