Skip to content

Commit

Permalink
Implement startDelay + fix image scale transition
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
guyca committed Apr 19, 2020
1 parent 913fd75 commit c9220de
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class SharedElementTransitionOptions {
public Text fromId = new NullText();
public Text toId = new NullText();
public Number duration = new NullNumber();
public Number startDelay = new NullNumber();

public static SharedElementTransitionOptions parse(@Nullable JSONObject json) {
SharedElementTransitionOptions transition = new SharedElementTransitionOptions();
Expand All @@ -23,6 +24,7 @@ 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");

return transition;
}
Expand All @@ -31,15 +33,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,13 @@
package com.reactnativenavigation.utils

import android.animation.Animator

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

fun Animator.withDuration(duration: Long): Animator {
this.duration = duration
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,18 @@
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.withStartDelay

class BackgroundColorAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(from, to) {
override fun shouldAnimateProperty(fromChild: ViewGroup, toChild: ViewGroup): Boolean {
Expand All @@ -19,10 +23,18 @@ 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())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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.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())
}

}
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,18 @@
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.withStartDelay

class MatrixAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactImageView>(from, to) {
override fun shouldAnimateProperty(fromChild: ReactImageView, toChild: ReactImageView): Boolean {
Expand All @@ -23,13 +27,17 @@ 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())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ 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.withStartDelay

class ScaleXAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(from, to) {
override fun shouldAnimateProperty(fromChild: ViewGroup, toChild: ViewGroup): Boolean {
Expand All @@ -15,8 +18,10 @@ 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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ 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.withStartDelay

class ScaleYAnimator(from: View, to: View) : PropertyAnimatorCreator<ViewGroup>(from, to) {
override fun shouldAnimateProperty(fromChild: ViewGroup, toChild: ViewGroup): Boolean {
Expand All @@ -15,8 +18,10 @@ 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())
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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.View.TRANSLATION_X
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.ViewUtils
import com.reactnativenavigation.utils.withStartDelay

class XAnimator(from: View, to: View) : PropertyAnimatorCreator<View>(from, to) {
private val dx: Int
Expand All @@ -23,6 +27,10 @@ class XAnimator(from: View, to: View) : PropertyAnimatorCreator<View>(from, to)
override fun shouldAnimateProperty(fromChild: View, toChild: View) = dx != 0

override fun create(options: SharedElementTransitionOptions): Animator {
return ObjectAnimator.ofFloat(to, TRANSLATION_X, dx.toFloat(), 0f).setDuration(options.getDuration())
to.translationX = dx.toFloat()
return ObjectAnimator
.ofFloat(to, TRANSLATION_X, dx.toFloat(), 0f)
.setDuration(options.getDuration())
.withStartDelay(options.getStartDelay())
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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.View.TRANSLATION_Y
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.ViewUtils
import com.reactnativenavigation.utils.withStartDelay

class YAnimator(from: View, to: View) : PropertyAnimatorCreator<View>(from, to) {
private val dy: Int
Expand All @@ -24,6 +28,10 @@ class YAnimator(from: View, to: View) : PropertyAnimatorCreator<View>(from, to)
override fun excludedViews() = listOf(ReactTextView::class.java)

override fun create(options: SharedElementTransitionOptions): Animator {
return ObjectAnimator.ofFloat(to, TRANSLATION_Y, dy.toFloat(), 0f).setDuration(options.getDuration())
to.translationY = dy.toFloat()
return ObjectAnimator
.ofFloat(to, TRANSLATION_Y, dy.toFloat(), 0f)
.setDuration(options.getDuration())
.withStartDelay(options.getStartDelay())
}
}

0 comments on commit c9220de

Please sign in to comment.