From 5bcc25806967ae62fc6e4eb7301d77ad579e67ef Mon Sep 17 00:00:00 2001 From: Krzysztof Piaskowy Date: Fri, 17 Feb 2023 13:16:24 +0100 Subject: [PATCH] [Shared Element Transition] Animation restart (#4043) ## Summary This change allows restarting of animation in two cases: - when someone changes the current screen during transition animation - when an update of layout comes for shared view during the transition animation **Known limitation**: If the parent of shared view updates owns the layout during the shared transition it doesn't affect for shared view layout. This issue will be fixed in the future. --- .../LayoutAnimationsManager.cpp | 24 ++ .../LayoutAnimationsManager.h | 6 + .../com/swmansion/reanimated/NativeProxy.java | 3 + android/src/main/cpp/LayoutAnimations.cpp | 15 + android/src/main/cpp/LayoutAnimations.h | 10 + android/src/main/cpp/NativeProxy.cpp | 15 + .../layoutReanimation/AnimationsManager.java | 11 +- .../layoutReanimation/LayoutAnimations.java | 3 + .../NativeMethodsHolder.java | 2 + .../SharedTransitionManager.java | 278 ++++++++++++++---- .../layoutReanimation/Snapshot.java | 6 +- .../com/swmansion/reanimated/NativeProxy.java | 8 + .../ReanimatedNativeHierarchyManager.java | 6 +- ios/LayoutReanimation/REAAnimationsManager.h | 5 +- ios/LayoutReanimation/REAAnimationsManager.m | 19 +- ios/LayoutReanimation/REAFrame.h | 10 + ios/LayoutReanimation/REAFrame.m | 15 + .../REASharedTransitionManager.h | 2 + .../REASharedTransitionManager.m | 221 +++++++++++--- ios/LayoutReanimation/REASnapshot.m | 1 + ios/LayoutReanimation/REAUIManager.mm | 2 +- ios/native/NativeProxy.mm | 11 + .../layoutReanimation/animationsManager.ts | 11 +- .../sharedTransitions/index.ts | 3 +- 24 files changed, 576 insertions(+), 111 deletions(-) create mode 100644 ios/LayoutReanimation/REAFrame.h create mode 100644 ios/LayoutReanimation/REAFrame.m diff --git a/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp b/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp index 8f3872c7003..1d438969a3f 100644 --- a/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp +++ b/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp @@ -94,6 +94,30 @@ void LayoutAnimationsManager::startLayoutAnimation( config->getJSValue(rt)); } +void LayoutAnimationsManager::cancelLayoutAnimation( + jsi::Runtime &rt, + int tag, + const std::string &type, + bool cancelled = true, + bool removeView = true) { + jsi::Value layoutAnimationRepositoryAsValue = + rt.global() + .getPropertyAsObject(rt, "global") + .getProperty(rt, "LayoutAnimationsManager"); + jsi::Function cancelLayoutAnimation = + layoutAnimationRepositoryAsValue.getObject(rt).getPropertyAsFunction( + rt, "stop"); + std::shared_ptr config; + { + auto lock = std::unique_lock(animationsMutex_); + config = sharedTransitionAnimations_[tag]; + } + if (config != nullptr) { + cancelLayoutAnimation.call( + rt, jsi::Value(tag), config->getJSValue(rt), cancelled, removeView); + } +} + /* The top screen on the stack triggers the animation, so we need to find the sibling view registered in the past. This method finds view diff --git a/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h b/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h index 3e0256d52bc..ab2a3e9b314 100644 --- a/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h +++ b/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h @@ -30,6 +30,12 @@ class LayoutAnimationsManager { const std::string &type, const jsi::Object &values); void clearLayoutAnimationConfig(int tag); + void cancelLayoutAnimation( + jsi::Runtime &rt, + int tag, + const std::string &type, + bool cancelled /* = true */, + bool removeView /* = true */); int findPrecedingViewTagForTransition(int tag); private: diff --git a/android/src/fabric/java/com/swmansion/reanimated/NativeProxy.java b/android/src/fabric/java/com/swmansion/reanimated/NativeProxy.java index fe6f821d1a6..4fe114fbe71 100644 --- a/android/src/fabric/java/com/swmansion/reanimated/NativeProxy.java +++ b/android/src/fabric/java/com/swmansion/reanimated/NativeProxy.java @@ -88,6 +88,9 @@ public boolean hasAnimation(int tag, String type) { @Override public void clearAnimationConfig(int tag) {} + + @Override + public void cancelAnimation(int tag, String type, boolean cancelled, boolean removeView) {} }; } } diff --git a/android/src/main/cpp/LayoutAnimations.cpp b/android/src/main/cpp/LayoutAnimations.cpp index cf53f7fa917..d21a080bf3c 100644 --- a/android/src/main/cpp/LayoutAnimations.cpp +++ b/android/src/main/cpp/LayoutAnimations.cpp @@ -64,6 +64,19 @@ void LayoutAnimations::clearAnimationConfigForTag(int tag) { clearAnimationConfigBlock_(tag); } +void LayoutAnimations::setCancelAnimationForTag( + CancelAnimationConfigBlock cancelAnimationBlock) { + this->cancelAnimationBlock_ = cancelAnimationBlock; +} + +void LayoutAnimations::cancelAnimationForTag( + int tag, + alias_ref type, + jboolean cancelled, + jboolean removeView) { + this->cancelAnimationBlock_(tag, type, cancelled, removeView); +} + bool LayoutAnimations::isLayoutAnimationEnabled() { return FeaturesConfig::isLayoutAnimationEnabled(); } @@ -89,6 +102,8 @@ void LayoutAnimations::registerNatives() { makeNativeMethod( "clearAnimationConfigForTag", LayoutAnimations::clearAnimationConfigForTag), + makeNativeMethod( + "cancelAnimationForTag", LayoutAnimations::cancelAnimationForTag), makeNativeMethod( "isLayoutAnimationEnabled", LayoutAnimations::isLayoutAnimationEnabled), diff --git a/android/src/main/cpp/LayoutAnimations.h b/android/src/main/cpp/LayoutAnimations.h index 3966b07da53..87db0a8a9a1 100644 --- a/android/src/main/cpp/LayoutAnimations.h +++ b/android/src/main/cpp/LayoutAnimations.h @@ -16,6 +16,8 @@ class LayoutAnimations : public jni::HybridClass { void(int, alias_ref, alias_ref>)>; using HasAnimationBlock = std::function; using ClearAnimationConfigBlock = std::function; + using CancelAnimationConfigBlock = + std::function, jboolean, jboolean)>; using FindPrecedingViewTagForTransitionBlock = std::function; public: @@ -36,6 +38,8 @@ class LayoutAnimations : public jni::HybridClass { void setHasAnimationBlock(HasAnimationBlock hasAnimationBlock); void setClearAnimationConfigBlock( ClearAnimationConfigBlock clearAnimationConfigBlock); + void setCancelAnimationForTag( + CancelAnimationConfigBlock cancelAnimationBlock); void setFindPrecedingViewTagForTransition( FindPrecedingViewTagForTransitionBlock findPrecedingViewTagForTransitionBlock); @@ -46,6 +50,11 @@ class LayoutAnimations : public jni::HybridClass { bool isSharedTransition); void endLayoutAnimation(int tag, bool cancelled, bool removeView); void clearAnimationConfigForTag(int tag); + void cancelAnimationForTag( + int tag, + alias_ref type, + jboolean cancelled, + jboolean removeView); int findPrecedingViewTagForTransition(int tag); private: @@ -54,6 +63,7 @@ class LayoutAnimations : public jni::HybridClass { AnimationStartingBlock animationStartingBlock_; HasAnimationBlock hasAnimationBlock_; ClearAnimationConfigBlock clearAnimationConfigBlock_; + CancelAnimationConfigBlock cancelAnimationBlock_; FindPrecedingViewTagForTransitionBlock findPrecedingViewTagForTransitionBlock_; diff --git a/android/src/main/cpp/NativeProxy.cpp b/android/src/main/cpp/NativeProxy.cpp index 9b4c95641c7..7ae41f83621 100644 --- a/android/src/main/cpp/NativeProxy.cpp +++ b/android/src/main/cpp/NativeProxy.cpp @@ -385,6 +385,21 @@ void NativeProxy::installJSIBindings( tag); }); + layoutAnimations->cthis()->setCancelAnimationForTag( + [wrt, weakModule]( + int tag, + alias_ref type, + jboolean cancelled, + jboolean removeView) { + if (auto reaModule = weakModule.lock()) { + if (auto runtime = wrt.lock()) { + jsi::Runtime &rt = *runtime; + reaModule->layoutAnimationsManager().cancelLayoutAnimation( + rt, tag, type->toStdString(), cancelled, removeView); + } + } + }); + layoutAnimations->cthis()->setFindPrecedingViewTagForTransition( [weakModule](int tag) { if (auto module = weakModule.lock()) { diff --git a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java index 0fab38812ed..80eb6ad2ec5 100644 --- a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java +++ b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java @@ -535,6 +535,9 @@ private void registerExitingAncestors(View view) { } private void maybeDropAncestors(View exitingView) { + if (!(exitingView.getParent() instanceof View)) { + return; + } View parent = (View) exitingView.getParent(); while (parent != null && !(parent instanceof RootView)) { View view = parent; @@ -631,8 +634,12 @@ private static Point convertScreenLocationToViewCoordinates(Point fromPoint, Vie return new Point(fromPoint.x - toPoint[0], fromPoint.y - toPoint[1]); } - public void viewsDidLayout() { - mSharedTransitionManager.viewsDidLayout(); + public void screenDidLayout() { + mSharedTransitionManager.screenDidLayout(); + } + + public void viewDidLayout(View view) { + mSharedTransitionManager.viewDidLayout(view); } public void notifyAboutViewsRemoval(int[] tagsToDelete) { diff --git a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java index 1283e848c0d..810e8cf02fb 100644 --- a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java +++ b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java @@ -33,6 +33,9 @@ public LayoutAnimations(ReactApplicationContext context) { public native void clearAnimationConfigForTag(int tag); + public native void cancelAnimationForTag( + int tag, String type, boolean cancelled, boolean removeView); + public native boolean isLayoutAnimationEnabled(); public native int findPrecedingViewTagForTransition(int tag); diff --git a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java index c196279fdb7..0de020d6338 100644 --- a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java +++ b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java @@ -9,6 +9,8 @@ public interface NativeMethodsHolder { void clearAnimationConfig(int tag); + void cancelAnimation(int tag, String type, boolean cancelled, boolean removeView); + boolean isLayoutAnimationEnabled(); int findPrecedingViewTagForTransition(int tag); diff --git a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java index c2625b1ce52..4fd8b942eb2 100644 --- a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java +++ b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java @@ -30,6 +30,9 @@ public class SharedTransitionManager { private final Map mCurrentSharedTransitionViews = new HashMap<>(); private View mTransitionContainer; private final List mRemovedSharedViews = new ArrayList<>(); + private final Map mDisableCleaningForViewTag = new HashMap<>(); + private List mSharedElements = new ArrayList<>(); + private final Map mViewsWithCanceledAnimation = new HashMap<>(); public SharedTransitionManager(AnimationsManager animationsManager) { mAnimationsManager = animationsManager; @@ -48,17 +51,25 @@ protected View getTransitioningView(int tag) { return mCurrentSharedTransitionViews.get(tag); } - protected void viewsDidLayout() { + protected void screenDidLayout() { tryStartSharedTransitionForViews(mAddedSharedViews, true); mAddedSharedViews.clear(); } - protected void onViewsRemoval(int[] tagsToDelete) { - if (tagsToDelete != null) { - visitTreeForTags(tagsToDelete, new SnapshotTreeVisitor()); + protected void viewDidLayout(View view) { + maybeRestartAnimationWithNewLayout(view); + } + protected void onViewsRemoval(int[] tagsToDelete) { + if (tagsToDelete == null) { + return; + } + visitTreeForTags(tagsToDelete, new SnapshotTreeVisitor()); + if (mRemovedSharedViews.size() > 0) { + // this happens when navigation goes back boolean animationStarted = tryStartSharedTransitionForViews(mRemovedSharedViews, false); if (!animationStarted) { + mRemovedSharedViews.clear(); return; } ConfigCleanerTreeVisitor configCleanerTreeVisitor = new ConfigCleanerTreeVisitor(); @@ -66,9 +77,40 @@ protected void onViewsRemoval(int[] tagsToDelete) { visitTree(removedSharedView, configCleanerTreeVisitor); } mRemovedSharedViews.clear(); - visitTreeForTags(tagsToDelete, configCleanerTreeVisitor); + } else if (mCurrentSharedTransitionViews.size() > 0) { + // this happens when navigation goes back and previous shared animation is still running + List viewsWithNewTransition = new ArrayList<>(); + for (View view : mCurrentSharedTransitionViews.values()) { + for (int tagToDelete : tagsToDelete) { + if (isViewChildParentWithTag(view, tagToDelete)) { + viewsWithNewTransition.add(view); + } + } + } + tryStartSharedTransitionForViews(viewsWithNewTransition, false); + for (View view : viewsWithNewTransition) { + clearAllSharedConfigsForView(view); + } + } + } + + private boolean isViewChildParentWithTag(View view, int parentTag) { + View parent = mSharedTransitionParent.get(view.getId()); + while (parent != null) { + if (parent.getId() == parentTag) { + return true; + } + if (parent.getClass().getSimpleName().equals("Screen")) { + return false; + } + if (parent instanceof View) { + parent = (View) parent.getParent(); + } else { + return false; + } } + return false; } protected void doSnapshotForTopScreenViews(ViewGroup stack) { @@ -88,6 +130,49 @@ protected void setNativeMethods(NativeMethodsHolder nativeMethods) { mNativeMethodsHolder = nativeMethods; } + private void maybeRestartAnimationWithNewLayout(View view) { + View sharedView = mCurrentSharedTransitionViews.get(view.getId()); + if (sharedView == null) { + return; + } + List sharedElementsToRestart = new ArrayList<>(); + for (SharedElement sharedElement : mSharedElements) { + if (sharedElement.targetView == sharedView) { + sharedElementsToRestart.add(sharedElement); + View sourceView = sharedElement.sourceView; + View targetView = sharedElement.targetView; + + Snapshot newSourceViewSnapshot = new Snapshot(sourceView); + Snapshot currentTargetViewSnapshot = mSnapshotRegistry.get(targetView.getId()); + Snapshot newTargetViewSnapshot = new Snapshot(targetView); + + int newOriginX = + currentTargetViewSnapshot.originX + - currentTargetViewSnapshot.originXByParent + + newTargetViewSnapshot.originX; + int newOriginY = + currentTargetViewSnapshot.originY + - currentTargetViewSnapshot.originYByParent + + newTargetViewSnapshot.originY; + + currentTargetViewSnapshot.originX = newOriginX; + currentTargetViewSnapshot.originY = newOriginY; + currentTargetViewSnapshot.globalOriginX = newOriginX; + currentTargetViewSnapshot.globalOriginY = newOriginY; + currentTargetViewSnapshot.originXByParent = newTargetViewSnapshot.originXByParent; + currentTargetViewSnapshot.originYByParent = newTargetViewSnapshot.originYByParent; + currentTargetViewSnapshot.height = newTargetViewSnapshot.height; + currentTargetViewSnapshot.width = newTargetViewSnapshot.width; + sharedElement.sourceViewSnapshot = newSourceViewSnapshot; + sharedElement.targetViewSnapshot = currentTargetViewSnapshot; + + disableCleaningForViewTag(sourceView.getId()); + disableCleaningForViewTag(targetView.getId()); + } + } + startSharedTransition(sharedElementsToRestart); + } + private boolean tryStartSharedTransitionForViews( List sharedViews, boolean withNewElements) { if (sharedViews.isEmpty()) { @@ -116,6 +201,7 @@ private void sortViewsByTags(List views) { private List getSharedElementsForCurrentTransition( List sharedViews, boolean addedNewScreen) { + List newTransitionViews = new ArrayList<>(); Set viewTags = new HashSet<>(); if (!addedNewScreen) { for (View view : sharedViews) { @@ -147,58 +233,92 @@ private List getSharedElementsForCurrentTransition( continue; } - View viewSourceScreen = findScreen(viewSource); - View viewTargetScreen = findScreen(viewTarget); - if (viewSourceScreen == null || viewTargetScreen == null) { - continue; - } + boolean isSourceViewInTransition = + mCurrentSharedTransitionViews.containsKey(viewSource.getId()); + boolean isTargetViewInTransition = + mCurrentSharedTransitionViews.containsKey(viewTarget.getId()); - ViewGroup stack = (ViewGroup) findStack(viewSourceScreen); - if (stack == null) { - continue; - } + if (!(isSourceViewInTransition || isTargetViewInTransition)) { + View viewSourceScreen = findScreen(viewSource); + View viewTargetScreen = findScreen(viewTarget); + if (viewSourceScreen == null || viewTargetScreen == null) { + continue; + } - ViewGroupManager stackViewGroupManager = - (ViewGroupManager) reanimatedNativeHierarchyManager.resolveViewManager(stack.getId()); - int screensCount = stackViewGroupManager.getChildCount(stack); + ViewGroup stack = (ViewGroup) findStack(viewSourceScreen); + if (stack == null) { + continue; + } - if (screensCount < 2) { - continue; - } + ViewGroupManager stackViewGroupManager = + (ViewGroupManager) reanimatedNativeHierarchyManager.resolveViewManager(stack.getId()); + int screensCount = stackViewGroupManager.getChildCount(stack); - View topScreen = stackViewGroupManager.getChildAt(stack, screensCount - 1); - View secondScreen = stackViewGroupManager.getChildAt(stack, screensCount - 2); - boolean isValidConfiguration; - if (addedNewScreen) { - isValidConfiguration = - secondScreen.getId() == viewSourceScreen.getId() - && topScreen.getId() == viewTargetScreen.getId(); - } else { - isValidConfiguration = - topScreen.getId() == viewSourceScreen.getId() - && secondScreen.getId() == viewTargetScreen.getId(); - } - if (!isValidConfiguration) { - continue; + if (screensCount < 2) { + continue; + } + + View topScreen = stackViewGroupManager.getChildAt(stack, screensCount - 1); + View secondScreen = stackViewGroupManager.getChildAt(stack, screensCount - 2); + boolean isValidConfiguration; + if (addedNewScreen) { + isValidConfiguration = + secondScreen.getId() == viewSourceScreen.getId() + && topScreen.getId() == viewTargetScreen.getId(); + } else { + isValidConfiguration = + topScreen.getId() == viewSourceScreen.getId() + && secondScreen.getId() == viewTargetScreen.getId(); + } + if (!isValidConfiguration) { + continue; + } } + Snapshot sourceViewSnapshot = null; if (addedNewScreen) { - makeSnapshot(viewSource); + if (isSourceViewInTransition) { + sourceViewSnapshot = new Snapshot(viewSource); + } else { + makeSnapshot(viewSource); + } makeSnapshot(viewTarget); + } else if (isSourceViewInTransition) { + makeSnapshot(viewSource); + } + if (sourceViewSnapshot == null) { + sourceViewSnapshot = mSnapshotRegistry.get(viewSource.getId()); } - Snapshot sourceViewSnapshot = mSnapshotRegistry.get(viewSource.getId()); Snapshot targetViewSnapshot = mSnapshotRegistry.get(viewTarget.getId()); - if (!mCurrentSharedTransitionViews.containsKey(viewSource.getId())) { - mCurrentSharedTransitionViews.put(viewSource.getId(), viewSource); - } - if (!mCurrentSharedTransitionViews.containsKey(viewTarget.getId())) { - mCurrentSharedTransitionViews.put(viewTarget.getId(), viewTarget); - } + newTransitionViews.add(viewSource); + newTransitionViews.add(viewTarget); + SharedElement sharedElement = new SharedElement(viewSource, sourceViewSnapshot, viewTarget, targetViewSnapshot); sharedElements.add(sharedElement); } + + if (!newTransitionViews.isEmpty()) { + for (View view : mCurrentSharedTransitionViews.values()) { + if (newTransitionViews.contains(view)) { + disableCleaningForViewTag(view.getId()); + } else { + mViewsWithCanceledAnimation.put(view.getId(), view); + } + } + mCurrentSharedTransitionViews.clear(); + for (View view : newTransitionViews) { + mCurrentSharedTransitionViews.put(view.getId(), view); + } + List viewsWithCanceledAnimation = new ArrayList<>(mViewsWithCanceledAnimation.values()); + for (View view : viewsWithCanceledAnimation) { + cancelAnimation(view); + finishSharedAnimation(view.getId()); + } + } + + mSharedElements = sharedElements; return sharedElements; } @@ -270,41 +390,55 @@ private void startSharedAnimationForView(View view, Snapshot before, Snapshot af } protected void finishSharedAnimation(int tag) { + if (mDisableCleaningForViewTag.containsKey(tag)) { + enableCleaningForViewTag(tag); + return; + } View view = mCurrentSharedTransitionViews.get(tag); + if (view == null) { + view = mViewsWithCanceledAnimation.get(tag); + if (view != null) { + mViewsWithCanceledAnimation.remove(view.getId()); + } + } if (view != null) { + int viewTag = view.getId(); ((ViewGroup) mTransitionContainer).removeView(view); - View parentView = mSharedTransitionParent.get(view.getId()); - int childIndex = mSharedTransitionInParentIndex.get(view.getId()); + View parentView = mSharedTransitionParent.get(viewTag); + int childIndex = mSharedTransitionInParentIndex.get(viewTag); ViewGroup parentViewGroup = ((ViewGroup) parentView); if (childIndex <= parentViewGroup.getChildCount()) { parentViewGroup.addView(view, childIndex); } else { parentViewGroup.addView(view); } - Snapshot viewSourcePreviousSnapshot = mSnapshotRegistry.get(view.getId()); - int originY = viewSourcePreviousSnapshot.originY; - if (findStack(view) == null) { - viewSourcePreviousSnapshot.originY = viewSourcePreviousSnapshot.topInsetFromParent; - } - Map snapshotMap = viewSourcePreviousSnapshot.toBasicMap(); - Map preparedValues = new HashMap<>(); - for (String key : snapshotMap.keySet()) { - Object value = snapshotMap.get(key); - preparedValues.put(key, (double) PixelUtil.toDIPFromPixel((int) value)); + Snapshot viewSourcePreviousSnapshot = mSnapshotRegistry.get(viewTag); + if (viewSourcePreviousSnapshot != null) { + int originY = viewSourcePreviousSnapshot.originY; + if (findStack(view) == null) { + viewSourcePreviousSnapshot.originY = viewSourcePreviousSnapshot.originYByParent; + } + Map snapshotMap = viewSourcePreviousSnapshot.toBasicMap(); + Map preparedValues = new HashMap<>(); + for (String key : snapshotMap.keySet()) { + Object value = snapshotMap.get(key); + preparedValues.put(key, (double) PixelUtil.toDIPFromPixel((int) value)); + } + mAnimationsManager.progressLayoutAnimation(viewTag, preparedValues, true); + viewSourcePreviousSnapshot.originY = originY; } - mAnimationsManager.progressLayoutAnimation(view.getId(), preparedValues, true); - viewSourcePreviousSnapshot.originY = originY; - mCurrentSharedTransitionViews.remove(view.getId()); + mCurrentSharedTransitionViews.remove(viewTag); + mSharedTransitionParent.remove(viewTag); + mSharedTransitionInParentIndex.remove(viewTag); } if (mCurrentSharedTransitionViews.isEmpty()) { - mSharedTransitionParent.clear(); - mSharedTransitionInParentIndex.clear(); if (mTransitionContainer != null) { ViewParent transitionContainerParent = mTransitionContainer.getParent(); if (transitionContainerParent != null) { ((ViewGroup) transitionContainerParent).removeView(mTransitionContainer); } } + mSharedElements.clear(); mIsSharedTransitionActive = false; } } @@ -418,4 +552,30 @@ private void clearAllSharedConfigsForView(View view) { mSnapshotRegistry.remove(viewTag); mNativeMethodsHolder.clearAnimationConfig(viewTag); } + + private void cancelAnimation(View view) { + int viewTag = view.getId(); + mNativeMethodsHolder.cancelAnimation(viewTag, "sharedTransition", true, true); + } + + private void disableCleaningForViewTag(int viewTag) { + Integer counter = mDisableCleaningForViewTag.get(viewTag); + if (counter != null) { + mDisableCleaningForViewTag.put(viewTag, counter + 1); + } else { + mDisableCleaningForViewTag.put(viewTag, 1); + } + } + + private void enableCleaningForViewTag(int viewTag) { + Integer counter = mDisableCleaningForViewTag.get(viewTag); + if (counter == null) { + return; + } + if (counter == 1) { + mDisableCleaningForViewTag.remove(viewTag); + } else { + mDisableCleaningForViewTag.put(viewTag, counter - 1); + } + } } diff --git a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java index 3e09b28ef49..19d77b1762d 100644 --- a/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java +++ b/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java @@ -41,7 +41,8 @@ public class Snapshot { public int originY; public int globalOriginX; public int globalOriginY; - public int topInsetFromParent; + public int originXByParent; + public int originYByParent; public static ArrayList targetKeysToTransform = new ArrayList<>( @@ -88,7 +89,8 @@ public Snapshot(View view) { originY = location[1]; width = view.getWidth(); height = view.getHeight(); - topInsetFromParent = view.getTop(); + originXByParent = view.getLeft(); + originYByParent = view.getTop(); } private void addTargetConfig(HashMap data) { diff --git a/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java b/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java index 2beb3977257..51890990cfb 100644 --- a/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java +++ b/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java @@ -94,6 +94,14 @@ public void clearAnimationConfig(int tag) { } } + @Override + public void cancelAnimation(int tag, String type, boolean cancelled, boolean removeView) { + LayoutAnimations layoutAnimations = weakLayoutAnimations.get(); + if (layoutAnimations != null) { + layoutAnimations.cancelAnimationForTag(tag, type, cancelled, removeView); + } + } + @Override public int findPrecedingViewTagForTransition(int tag) { LayoutAnimations layoutAnimations = weakLayoutAnimations.get(); diff --git a/android/src/reactNativeVersionPatch/nativeHierarchyManager/latest/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java b/android/src/reactNativeVersionPatch/nativeHierarchyManager/latest/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java index a05851efb14..d7dc5333ba8 100644 --- a/android/src/reactNativeVersionPatch/nativeHierarchyManager/latest/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java +++ b/android/src/reactNativeVersionPatch/nativeHierarchyManager/latest/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java @@ -256,9 +256,13 @@ public synchronized void updateLayout( && mReaLayoutAnimator != null) { boolean hasHeader = checkIfTopScreenHasHeader((ViewGroup) container); if (!hasHeader || !container.isLayoutRequested()) { - mReaLayoutAnimator.getAnimationsManager().viewsDidLayout(); + mReaLayoutAnimator.getAnimationsManager().screenDidLayout(); } } + View view = resolveView(tag); + if (view != null && mReaLayoutAnimator != null) { + mReaLayoutAnimator.getAnimationsManager().viewDidLayout(view); + } } catch (IllegalViewOperationException e) { // (IllegalViewOperationException) == (vm == null) e.printStackTrace(); diff --git a/ios/LayoutReanimation/REAAnimationsManager.h b/ios/LayoutReanimation/REAAnimationsManager.h index 59d7be47f4f..373b25dbc35 100644 --- a/ios/LayoutReanimation/REAAnimationsManager.h +++ b/ios/LayoutReanimation/REAAnimationsManager.h @@ -20,6 +20,8 @@ typedef void (^REAAnimationStartingBlock)( NSDictionary *_Nonnull yogaValues, NSNumber *_Nonnull depth); typedef void (^REAAnimationRemovingBlock)(NSNumber *_Nonnull tag); +typedef void ( + ^REACancelAnimationBlock)(NSNumber *_Nonnull tag, NSString *_Nonnull type, BOOL cancelled, BOOL removeView); typedef NSNumber *_Nullable (^REAFindPrecedingViewTagForTransitionBlock)(NSNumber *_Nonnull tag); typedef int (^REATreeVisitor)(id); @@ -36,10 +38,11 @@ BOOL REANodeFind(id view, int (^block)(id)); isSharedTransition:(BOOL)isSharedTransition; - (void)setFindPrecedingViewTagForTransitionBlock: (REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition; +- (void)setCancelAnimationBlock:(REACancelAnimationBlock)animationCancellingBlock; - (void)endLayoutAnimationForTag:(NSNumber *_Nonnull)tag cancelled:(BOOL)cancelled removeView:(BOOL)removeView; - (void)endAnimationsRecursive:(UIView *)view; - (void)invalidate; -- (void)viewDidMount:(UIView *)view withBeforeSnapshot:(REASnapshot *)snapshot; +- (void)viewDidMount:(UIView *)view withBeforeSnapshot:(REASnapshot *)snapshot withNewFrame:(CGRect)frame; - (REASnapshot *)prepareSnapshotBeforeMountForView:(UIView *)view; - (BOOL)wantsHandleRemovalOfView:(UIView *)view; - (void)removeAnimationsFromSubtree:(UIView *)view; diff --git a/ios/LayoutReanimation/REAAnimationsManager.m b/ios/LayoutReanimation/REAAnimationsManager.m index dd50206f733..8a057d8efc3 100644 --- a/ios/LayoutReanimation/REAAnimationsManager.m +++ b/ios/LayoutReanimation/REAAnimationsManager.m @@ -505,7 +505,7 @@ - (void)removeAnimationsFromSubtree:(UIView *)view }); } -- (void)viewDidMount:(UIView *)view withBeforeSnapshot:(nonnull REASnapshot *)before +- (void)viewDidMount:(UIView *)view withBeforeSnapshot:(nonnull REASnapshot *)before withNewFrame:(CGRect)frame { NSString *type = before == nil ? @"entering" : @"layout"; NSNumber *viewTag = view.reactTag; @@ -521,8 +521,12 @@ - (void)viewDidMount:(UIView *)view withBeforeSnapshot:(nonnull REASnapshot *)be [self setNewProps:before.values forView:view]; } - if (_hasAnimationForTag(viewTag, @"sharedElementTransition") && [type isEqual:@"entering"]) { - [_sharedTransitionManager notifyAboutNewView:view]; + if (_hasAnimationForTag(viewTag, @"sharedElementTransition")) { + if ([type isEqual:@"entering"]) { + [_sharedTransitionManager notifyAboutNewView:view]; + } else { + [_sharedTransitionManager notifyAboutViewLayout:view withViewFrame:frame]; + } } } @@ -537,8 +541,17 @@ - (void)setFindPrecedingViewTagForTransitionBlock: [_sharedTransitionManager setFindPrecedingViewTagForTransitionBlock:findPrecedingViewTagForTransition]; } +- (void)setCancelAnimationBlock:(REACancelAnimationBlock)animationCancellingBlock +{ + [_sharedTransitionManager setCancelAnimationBlock:animationCancellingBlock]; +} + - (BOOL)hasAnimationForTag:(NSNumber *)tag type:(NSString *)type { + if (!_hasAnimationForTag) { + // It can happen during reload. + return NO; + } return _hasAnimationForTag(tag, type); } diff --git a/ios/LayoutReanimation/REAFrame.h b/ios/LayoutReanimation/REAFrame.h new file mode 100644 index 00000000000..a5fbbaed30b --- /dev/null +++ b/ios/LayoutReanimation/REAFrame.h @@ -0,0 +1,10 @@ +@interface REAFrame : NSObject + +@property float x; +@property float y; +@property float width; +@property float height; + +- (instancetype)initWithX:(float)x y:(float)y width:(float)width height:(float)height; + +@end diff --git a/ios/LayoutReanimation/REAFrame.m b/ios/LayoutReanimation/REAFrame.m new file mode 100644 index 00000000000..b5815afdbdb --- /dev/null +++ b/ios/LayoutReanimation/REAFrame.m @@ -0,0 +1,15 @@ +#import + +@implementation REAFrame + +- (instancetype)initWithX:(float)x y:(float)y width:(float)width height:(float)height +{ + self = [super init]; + _x = x; + _y = y; + _width = width; + _height = height; + return self; +} + +@end diff --git a/ios/LayoutReanimation/REASharedTransitionManager.h b/ios/LayoutReanimation/REASharedTransitionManager.h index 8de4751e1f1..1c6e509e094 100644 --- a/ios/LayoutReanimation/REASharedTransitionManager.h +++ b/ios/LayoutReanimation/REASharedTransitionManager.h @@ -4,11 +4,13 @@ @interface REASharedTransitionManager : NSObject - (void)notifyAboutNewView:(UIView *)view; +- (void)notifyAboutViewLayout:(UIView *)view withViewFrame:(CGRect)frame; - (void)viewsDidLayout; - (BOOL)configureAndStartSharedTransitionForViews:(NSArray *)views; - (void)finishSharedAnimation:(UIView *)view; - (void)setFindPrecedingViewTagForTransitionBlock: (REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition; +- (void)setCancelAnimationBlock:(REACancelAnimationBlock)cancelAnimationBlock; - (instancetype)initWithAnimationsManager:(REAAnimationsManager *)animationManager; - (UIView *)getTransitioningView:(NSNumber *)tag; diff --git a/ios/LayoutReanimation/REASharedTransitionManager.m b/ios/LayoutReanimation/REASharedTransitionManager.m index d9c16aff37f..79c77f4ad5e 100644 --- a/ios/LayoutReanimation/REASharedTransitionManager.m +++ b/ios/LayoutReanimation/REASharedTransitionManager.m @@ -1,3 +1,4 @@ +#import #import #import #import @@ -15,11 +16,18 @@ @implementation REASharedTransitionManager { NSMutableDictionary *_snapshotRegistry; NSMutableDictionary *_currentSharedTransitionViews; REAFindPrecedingViewTagForTransitionBlock _findPrecedingViewTagForTransition; + REACancelAnimationBlock _cancelLayoutAnimation; UIView *_transitionContainer; NSMutableArray *_addedSharedViews; BOOL _isSharedTransitionActive; NSMutableArray *_sharedElements; REAAnimationsManager *_animationManager; + NSMutableArray *_removedViews; + NSMutableSet *_viewsWithCanceledAnimation; + NSMutableDictionary *_disableCleaningForView; + NSMutableSet *_layoutedSharedViewsTags; + NSMutableDictionary *_layoutedSharedViewsFrame; + BOOL _isAsyncSharedTransitionConfigured; } /* @@ -41,6 +49,11 @@ - (instancetype)initWithAnimationsManager:(REAAnimationsManager *)animationManag _sharedElements = [NSMutableArray new]; _animationManager = animationManager; _sharedTransitionManager = self; + _viewsWithCanceledAnimation = [NSMutableSet new]; + _disableCleaningForView = [NSMutableDictionary new]; + _layoutedSharedViewsTags = [NSMutableSet new]; + _layoutedSharedViewsFrame = [NSMutableDictionary new]; + _isAsyncSharedTransitionConfigured = NO; [self swizzleScreensMethods]; } return self; @@ -67,10 +80,23 @@ - (void)notifyAboutNewView:(UIView *)view [_addedSharedViews addObject:view]; } +- (void)notifyAboutViewLayout:(UIView *)view withViewFrame:(CGRect)frame +{ + [_layoutedSharedViewsTags addObject:view.reactTag]; + float x = frame.origin.x; + float y = frame.origin.y; + float width = frame.size.width; + float height = frame.size.height; + _layoutedSharedViewsFrame[view.reactTag] = [[REAFrame alloc] initWithX:x y:y width:width height:height]; +} + - (void)viewsDidLayout { [self configureAsyncSharedTransitionForViews:_addedSharedViews]; [_addedSharedViews removeAllObjects]; + [self maybeRestartAnimationWithNewLayout]; + [_layoutedSharedViewsTags removeAllObjects]; + [_layoutedSharedViewsFrame removeAllObjects]; } - (void)configureAsyncSharedTransitionForViews:(NSArray *)views @@ -78,7 +104,50 @@ - (void)configureAsyncSharedTransitionForViews:(NSArray *)views if ([views count] > 0) { NSArray *sharedViews = [self sortViewsByTags:views]; _sharedElements = [self getSharedElementForCurrentTransition:sharedViews withNewElements:YES]; + _isAsyncSharedTransitionConfigured = YES; + } +} + +- (void)maybeRestartAnimationWithNewLayout +{ + if ([_layoutedSharedViewsTags count] == 0 || [_currentSharedTransitionViews count] == 0) { + return; + } + NSMutableArray *sharedElementToRestart = [NSMutableArray new]; + for (REASharedElement *sharedElement in _sharedElements) { + NSNumber *viewTag = sharedElement.targetView.reactTag; + if ([_layoutedSharedViewsTags containsObject:viewTag] && _currentSharedTransitionViews[viewTag]) { + [sharedElementToRestart addObject:sharedElement]; + } } + + for (REASharedElement *sharedElement in sharedElementToRestart) { + UIView *sourceView = sharedElement.sourceView; + UIView *targetView = sharedElement.targetView; + + REASnapshot *newSourceViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:sourceView]; + REASnapshot *currentTargetViewSnapshot = _snapshotRegistry[targetView.reactTag]; + REAFrame *frameData = _layoutedSharedViewsFrame[targetView.reactTag]; + float currentOriginX = [currentTargetViewSnapshot.values[@"originX"] floatValue]; + float currentOriginY = [currentTargetViewSnapshot.values[@"originY"] floatValue]; + float currentOriginXByParent = [currentTargetViewSnapshot.values[@"originXByParent"] floatValue]; + float currentOriginYByParent = [currentTargetViewSnapshot.values[@"originYByParent"] floatValue]; + NSNumber *newOriginX = @(currentOriginX - currentOriginXByParent + frameData.x); + NSNumber *newOriginY = @(currentOriginY - currentOriginYByParent + frameData.y); + currentTargetViewSnapshot.values[@"width"] = @(frameData.width); + currentTargetViewSnapshot.values[@"height"] = @(frameData.height); + currentTargetViewSnapshot.values[@"originX"] = newOriginX; + currentTargetViewSnapshot.values[@"originY"] = newOriginY; + currentTargetViewSnapshot.values[@"globalOriginX"] = newOriginX; + currentTargetViewSnapshot.values[@"globalOriginY"] = newOriginY; + currentTargetViewSnapshot.values[@"originXByParent"] = @(frameData.x); + currentTargetViewSnapshot.values[@"originYByParent"] = @(frameData.y); + sharedElement.sourceViewSnapshot = newSourceViewSnapshot; + + [self disableCleaningForViewTag:sourceView.reactTag]; + [self disableCleaningForViewTag:targetView.reactTag]; + } + [self startSharedTransition:sharedElementToRestart]; } - (BOOL)configureAndStartSharedTransitionForViews:(NSArray *)views @@ -110,6 +179,7 @@ - (NSArray *)sortViewsByTags:(NSArray *)views - (NSMutableArray *)getSharedElementForCurrentTransition:(NSArray *)sharedViews withNewElements:(BOOL)addedNewScreen { + NSMutableArray *newTransitionViews = [NSMutableArray new]; NSMutableArray *sharedElements = [NSMutableArray new]; for (UIView *sharedView in sharedViews) { // add observers @@ -142,6 +212,18 @@ - (NSArray *)sortViewsByTags:(NSArray *)views viewTarget = siblingView; } + bool isInCurrentTransition = false; + if (_currentSharedTransitionViews[viewSource.reactTag] || _currentSharedTransitionViews[viewTarget.reactTag]) { + isInCurrentTransition = true; + if (addedNewScreen) { + siblingViewTag = _findPrecedingViewTagForTransition(siblingView.reactTag); + siblingView = [_animationManager viewForTag:siblingViewTag]; + + viewSource = siblingView; + viewTarget = sharedView; + } + } + // check valid target screen configuration int screensCount = [stack.reactSubviews count]; if (addedNewScreen) { @@ -151,7 +233,7 @@ - (NSArray *)sortViewsByTags:(NSArray *)views } UIView *viewSourceParentScreen = [self getScreenForView:viewSource]; UIView *screenUnderStackTop = stack.reactSubviews[screensCount - 2]; - if (![screenUnderStackTop.reactTag isEqual:viewSourceParentScreen.reactTag]) { + if (![screenUnderStackTop.reactTag isEqual:viewSourceParentScreen.reactTag] && !isInCurrentTransition) { continue; } } else { @@ -164,7 +246,9 @@ - (NSArray *)sortViewsByTags:(NSArray *)views } REASnapshot *sourceViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewSource]; - _snapshotRegistry[viewSource.reactTag] = sourceViewSnapshot; + if (addedNewScreen && !_currentSharedTransitionViews[viewSource.reactTag]) { + _snapshotRegistry[viewSource.reactTag] = sourceViewSnapshot; + } REASnapshot *targetViewSnapshot; if (addedNewScreen) { @@ -174,12 +258,8 @@ - (NSArray *)sortViewsByTags:(NSArray *)views targetViewSnapshot = _snapshotRegistry[viewTarget.reactTag]; } - if (!_currentSharedTransitionViews[viewSource.reactTag]) { - _currentSharedTransitionViews[viewSource.reactTag] = viewSource; - } - if (!_currentSharedTransitionViews[viewTarget.reactTag]) { - _currentSharedTransitionViews[viewTarget.reactTag] = viewTarget; - } + [newTransitionViews addObject:viewSource]; + [newTransitionViews addObject:viewTarget]; REASharedElement *sharedElement = [[REASharedElement alloc] initWithSourceView:viewSource sourceViewSnapshot:sourceViewSnapshot @@ -187,6 +267,26 @@ - (NSArray *)sortViewsByTags:(NSArray *)views targetViewSnapshot:targetViewSnapshot]; [sharedElements addObject:sharedElement]; } + if ([newTransitionViews count] > 0) { + for (NSNumber *viewTag in _currentSharedTransitionViews) { + UIView *view = _currentSharedTransitionViews[viewTag]; + if ([newTransitionViews containsObject:view]) { + [self disableCleaningForViewTag:viewTag]; + } else { + [_viewsWithCanceledAnimation addObject:view]; + } + } + [_currentSharedTransitionViews removeAllObjects]; + for (UIView *view in newTransitionViews) { + _currentSharedTransitionViews[view.reactTag] = view; + } + for (UIView *view in [_viewsWithCanceledAnimation copy]) { + [self cancelAnimation:view.reactTag]; + [self finishSharedAnimation:view]; + } + } + + _sharedElements = sharedElements; return sharedElements; } @@ -304,6 +404,9 @@ - (void)makeSnapshotForScreenViews:(UIView *)screen { REANodeFind(screen, ^int(id view) { NSNumber *viewTag = view.reactTag; + if (self->_currentSharedTransitionViews[viewTag]) { + return false; + } if ([self->_animationManager hasAnimationForTag:viewTag type:@"sharedElementTransition"]) { REASnapshot *snapshot = [[REASnapshot alloc] initWithAbsolutePosition:(UIView *)view]; self->_snapshotRegistry[viewTag] = snapshot; @@ -363,36 +466,29 @@ - (void)runSharedTransitionForSharedViewsOnScreen:(UIView *)screen } return false; }); - BOOL animationStarted = [self configureAndStartSharedTransitionForViews:removedViews]; - if (animationStarted) { - for (UIView *view in removedViews) { - [_animationManager clearAnimationConfigForTag:view.reactTag]; - } + BOOL startedAnimation = [self configureAndStartSharedTransitionForViews:removedViews]; + if (startedAnimation) { + _removedViews = removedViews; } } - (void)runAsyncSharedTransition { - if ([_sharedElements count] == 0) { + if ([_sharedElements count] == 0 || !_isAsyncSharedTransitionConfigured) { return; } - NSMutableArray *currentSharedElements = [NSMutableArray new]; for (REASharedElement *sharedElement in _sharedElements) { UIView *viewTarget = sharedElement.targetView; REASnapshot *targetViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewTarget]; _snapshotRegistry[viewTarget.reactTag] = targetViewSnapshot; sharedElement.targetViewSnapshot = targetViewSnapshot; - [currentSharedElements addObject:sharedElement]; } - if ([currentSharedElements count] == 0) { - return; - } [self configureTransitionContainer]; [self reparentSharedViewsForCurrentTransition:_sharedElements]; [self startSharedTransition:_sharedElements]; [_addedSharedViews removeAllObjects]; - [_sharedElements removeObjectsInArray:currentSharedElements]; + _isAsyncSharedTransitionConfigured = NO; } - (void)configureTransitionContainer @@ -456,29 +552,44 @@ - (void)onViewTransition:(UIView *)view before:(REASnapshot *)before after:(REAS - (void)finishSharedAnimation:(UIView *)view { - if (_currentSharedTransitionViews[view.reactTag]) { + NSNumber *viewTag = view.reactTag; + if (_disableCleaningForView[viewTag]) { + [self enableCleaningForViewTag:viewTag]; + return; + } + if (_currentSharedTransitionViews[viewTag] || [_viewsWithCanceledAnimation containsObject:view]) { [view removeFromSuperview]; - UIView *parent = _sharedTransitionParent[view.reactTag]; - int childIndex = [_sharedTransitionInParentIndex[view.reactTag] intValue]; - [parent insertSubview:view atIndex:childIndex]; - REASnapshot *viewSourcePreviousSnapshot = _snapshotRegistry[view.reactTag]; - BOOL isScreenDetached = [self getScreenForView:view].superview == nil; - NSNumber *originY = viewSourcePreviousSnapshot.values[@"originY"]; - if (isScreenDetached) { - float originYByParent = [viewSourcePreviousSnapshot.values[@"originYByParent"] floatValue]; - float headerHeight = [viewSourcePreviousSnapshot.values[@"headerHeight"] floatValue]; - viewSourcePreviousSnapshot.values[@"originY"] = @(originYByParent + headerHeight); + UIView *parent = _sharedTransitionParent[viewTag]; + int childIndex = [_sharedTransitionInParentIndex[viewTag] intValue]; + UIView *screen = [self getScreenForView:parent]; + bool isScreenInNativeTree = screen.superview != nil; + bool isScreenInReactTree = screen.reactSuperview != nil; + if (isScreenInReactTree) { + [parent insertSubview:view atIndex:childIndex]; + REASnapshot *viewSourcePreviousSnapshot = _snapshotRegistry[viewTag]; + NSNumber *originY = viewSourcePreviousSnapshot.values[@"originY"]; + if (!isScreenInNativeTree) { + float originYByParent = [viewSourcePreviousSnapshot.values[@"originYByParent"] floatValue]; + float headerHeight = [viewSourcePreviousSnapshot.values[@"headerHeight"] floatValue]; + viewSourcePreviousSnapshot.values[@"originY"] = @(originYByParent + headerHeight); + } + [_animationManager progressLayoutAnimationWithStyle:viewSourcePreviousSnapshot.values + forTag:viewTag + isSharedTransition:YES]; + viewSourcePreviousSnapshot.values[@"originY"] = originY; + } + [_currentSharedTransitionViews removeObjectForKey:viewTag]; + [_sharedTransitionParent removeObjectForKey:viewTag]; + [_sharedTransitionInParentIndex removeObjectForKey:viewTag]; + [_viewsWithCanceledAnimation removeObject:view]; + if ([_removedViews containsObject:view]) { + [_animationManager clearAnimationConfigForTag:viewTag]; } - [_animationManager progressLayoutAnimationWithStyle:viewSourcePreviousSnapshot.values - forTag:view.reactTag - isSharedTransition:YES]; - viewSourcePreviousSnapshot.values[@"originY"] = originY; - [_currentSharedTransitionViews removeObjectForKey:view.reactTag]; } if ([_currentSharedTransitionViews count] == 0) { - [_sharedTransitionParent removeAllObjects]; - [_sharedTransitionInParentIndex removeAllObjects]; [_transitionContainer removeFromSuperview]; + [_removedViews removeAllObjects]; + [_sharedElements removeAllObjects]; _isSharedTransitionActive = NO; } } @@ -489,6 +600,11 @@ - (void)setFindPrecedingViewTagForTransitionBlock: _findPrecedingViewTagForTransition = findPrecedingViewTagForTransition; } +- (void)setCancelAnimationBlock:(REACancelAnimationBlock)cancelAnimationBlock +{ + _cancelLayoutAnimation = cancelAnimationBlock; +} + - (void)clearAllSharedConfigsForViewTag:(NSNumber *)viewTag { if (viewTag != nil) { @@ -497,4 +613,33 @@ - (void)clearAllSharedConfigsForViewTag:(NSNumber *)viewTag } } +- (void)cancelAnimation:(NSNumber *)viewTag +{ + _cancelLayoutAnimation(viewTag, @"sharedTransition", YES, YES); +} + +- (void)disableCleaningForViewTag:(NSNumber *)viewTag +{ + NSNumber *counter = _disableCleaningForView[viewTag]; + if (counter != nil) { + _disableCleaningForView[viewTag] = @([counter intValue] + 1); + } else { + _disableCleaningForView[viewTag] = @(1); + } +} + +- (void)enableCleaningForViewTag:(NSNumber *)viewTag +{ + NSNumber *counter = _disableCleaningForView[viewTag]; + if (counter == nil) { + return; + } + int counterInt = [counter intValue]; + if (counterInt == 1) { + [_disableCleaningForView removeObjectForKey:viewTag]; + } else { + _disableCleaningForView[viewTag] = @(counterInt - 1); + } +} + @end diff --git a/ios/LayoutReanimation/REASnapshot.m b/ios/LayoutReanimation/REASnapshot.m index c9671ce650c..5e454befaa2 100644 --- a/ios/LayoutReanimation/REASnapshot.m +++ b/ios/LayoutReanimation/REASnapshot.m @@ -34,6 +34,7 @@ - (void)makeSnapshotForView:(UIView *)view useAbsolutePositionOnly:(BOOL)useAbso if (useAbsolutePositionOnly) { _values[@"originX"] = _values[@"globalOriginX"]; _values[@"originY"] = _values[@"globalOriginY"]; + _values[@"originXByParent"] = [NSNumber numberWithDouble:view.center.x - view.bounds.size.width / 2.0]; _values[@"originYByParent"] = [NSNumber numberWithDouble:view.center.y - view.bounds.size.height / 2.0]; UIView *navigationContainer = view.reactViewController.navigationController.view; diff --git a/ios/LayoutReanimation/REAUIManager.mm b/ios/LayoutReanimation/REAUIManager.mm index fec63118c4f..301cb0a3d60 100644 --- a/ios/LayoutReanimation/REAUIManager.mm +++ b/ios/LayoutReanimation/REAUIManager.mm @@ -308,7 +308,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * // Reanimated changes /start if (isNew || snapshotBefore != nil) { - [self->_animationsManager viewDidMount:view withBeforeSnapshot:snapshotBefore]; + [self->_animationsManager viewDidMount:view withBeforeSnapshot:snapshotBefore withNewFrame:frame]; } // Reanimated changes /end } diff --git a/ios/native/NativeProxy.mm b/ios/native/NativeProxy.mm index 7d05782d0a8..20b2ef45849 100644 --- a/ios/native/NativeProxy.mm +++ b/ios/native/NativeProxy.mm @@ -324,6 +324,17 @@ static CFTimeInterval calculateTimestampWithSlowAnimations(CFTimeInterval curren weakModule.lock()->layoutAnimationsManager().clearLayoutAnimationConfig([tag intValue]); }]; + [animationsManager + setCancelAnimationBlock:^(NSNumber *_Nonnull tag, NSString *_Nonnull type, BOOL cancelled, BOOL removeView) { + if (auto reaModule = weakModule.lock()) { + if (auto runtime = wrt.lock()) { + jsi::Runtime &rt = *runtime; + reaModule->layoutAnimationsManager().cancelLayoutAnimation( + rt, [tag intValue], std::string([type UTF8String]), cancelled == YES, removeView == YES); + } + } + }]; + [animationsManager setFindPrecedingViewTagForTransitionBlock:^NSNumber *_Nullable(NSNumber *_Nonnull tag) { if (auto reaModule = weakModule.lock()) { int resultTag = reaModule->layoutAnimationsManager().findPrecedingViewTagForTransition([tag intValue]); diff --git a/src/reanimated2/layoutReanimation/animationsManager.ts b/src/reanimated2/layoutReanimation/animationsManager.ts index cbb2281f2b4..93db9b64b97 100644 --- a/src/reanimated2/layoutReanimation/animationsManager.ts +++ b/src/reanimated2/layoutReanimation/animationsManager.ts @@ -64,12 +64,13 @@ function createLayoutAnimationManager() { value = makeUIMutable(style.initialValues); mutableValuesForTag.set(tag, value); } else { - if (!sharedTransitionForTag.get(tag)) { - stopObservingProgress(tag, value, false, false); - } value._value = style.initialValues; } + if (sharedTransitionForTag.get(tag)) { + stopObservingProgress(tag, value, true, false); + } + if (type === 'sharedElementTransition') { sharedTransitionForTag.set(tag, currentAnimation); } @@ -92,6 +93,10 @@ function createLayoutAnimationManager() { startObservingProgress(tag, value, type); value.value = animation; }, + stop(tag: number) { + const value = mutableValuesForTag.get(tag); + stopObservingProgress(tag, value, true, true); + }, }; } diff --git a/src/reanimated2/layoutReanimation/sharedTransitions/index.ts b/src/reanimated2/layoutReanimation/sharedTransitions/index.ts index 73c983ef866..4db9d78c845 100644 --- a/src/reanimated2/layoutReanimation/sharedTransitions/index.ts +++ b/src/reanimated2/layoutReanimation/sharedTransitions/index.ts @@ -53,7 +53,8 @@ export class SharedTransition implements ILayoutAnimationBuilder { const keyToTargetValue = 'target' + propName.charAt(0).toUpperCase() + propName.slice(1); animations[propName] = withTiming(values[keyToTargetValue], { - duration: 1000, + // native screen transition takes around 500ms + duration: 500, }); } }