Conversation
Complete port of the iOS Hero transition library to Flutter as flutter_hero_transitions package. Includes independent transition engine (not using Flutter's built-in Hero), 50+ modifiers, 14 built-in animation types, interactive gestures, cascade animations, arc motion paths, spring physics, and plugin system. - 35 library source files with zero external dependencies - 15 example screens (5 modern + 9 legacy + 1 tablet) - 8 test files with 516 passing tests - 66 image assets ported from iOS xcassets Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major animation quality improvements to match iOS Hero library: PHYSICS: - Add HeroSpringSimulation implementing real damped harmonic oscillator - Support underdamped, overdamped, and critically damped spring regimes - iOS defaults: mass=1.0, stiffness=230.0, dampingRatio=0.825 - Add spring presets: ios, bouncy, stiff CURVES: - Add iOS-accurate cubic-bezier curves (iosEaseInOut, iosEaseOut, iosEaseIn) - Replace Material Design curves with iOS Core Animation equivalents - iosEaseInOut: cubic-bezier(0.42, 0, 0.58, 1) TIMING: - Update duration calculation to match iOS (300ms-500ms range) - Formula: 0.3 + (distance/400) * 0.2 seconds - Previous: 208ms-700ms Material Design timing API CHANGES: - HeroSpringConfig: change 'damping' parameter to 'dampingRatio' - HeroModifier.spring: update signature to use dampingRatio - Update app_store_card_example to use new spring API FIXES: - Fix Stack layout constraints in main.dart (add StackFit.expand) - Fix Positioned widget parent data chain in hero_overlay.dart - Fix size change calculation in hero_default_animator.dart FILES ADDED: - lib/src/animator/hero_spring_simulation.dart FILES MODIFIED: - lib/src/extensions/curve_extensions.dart - lib/src/types/hero_target_state.dart - lib/src/animator/hero_default_animator.dart - lib/src/animator/hero_animation_entry.dart - lib/src/modifiers/hero_modifier.dart - lib/flutter_hero_transitions.dart - example/lib/main.dart - example/lib/screens/app_store_card_example.dart - lib/src/widgets/hero_overlay.dart Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- HeroView hides itself (opacity 0) when its ID has an active animation entry, so only the overlay proxy is visible during transitions - HeroPageRoute hides only the upper route (dest on push, source on pop) and sets opaque=false so the source route stays painted underneath - MatchPreprocessor clears opacity/transform for matched views so they morph visibly instead of fading in from invisible - HeroDefaultAnimator handles zero-dimension dest rects by computing a fallback from the source aspect ratio, and prefers the source widget as the snapshot for matched views (already loaded/rendered) - Expanded debug plugin with custom slider, 3D view, and arc overlays Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
HeroPageRoute uses opaque: false so the source route stays painted during hero transitions. However, when no transition is active, the source route's content was visible behind destination screens that lack explicit backgrounds (e.g., main menu text showing behind the Match in Collection grid). Wrap the child widget in a ColoredBox using the scaffold background color (or transitionBackgroundColor if set) so the destination route is visually opaque at rest while still allowing the source to be painted during active transitions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Resolve 'auto' animation type to push-left/pull-right instead of returning early with no modifiers (root cause of scrubber showing no animation when dragged) - Default debug plugin to OFF so transitions play normally - Add Debug ON/OFF toggle pill on main menu for easy switching Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The blur background (unmatched dest view with fade modifier) was being painted ON TOP of the matched card in the overlay Stack, causing the card to appear to fade out during the App Store Card transition. Reordered entry creation: unmatched source -> unmatched dest -> matched, so matched (morphing) views are always painted last and remain fully visible. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wrap overlay snapshot widgets in DefaultTextStyle to prevent yellow underlines that appear when Text widgets render outside Material ancestors. Update card title to 32pt and subtitle to 17pt to match iOS exactly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace binary route visibility (ValueListenableBuilder + Opacity 0/1) with Flutter's built-in route animations for smooth cross-dissolve. This matches iOS Hero behavior where both view controllers cross-fade while the overlay animates matched hero views on top. Also clear overlay modifier on matched views in MatchPreprocessor to prevent darkening from DefaultAnimationPreprocessor leaking through. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move Positioned.fill() outside HeroView so it remains a direct child of Stack. HeroView wraps its child in Opacity for hide/show during transitions, which breaks Positioned's requirement to be a direct Stack child. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Light blue background, "Adventure awaits in CANADA" header - Horizontal scrolling 200x298 city cards with photos and shadows - Full-screen detail pager with 50% dark overlay and city descriptions - Hero IDs on text elements with fade+translate modifiers - City data model updated with shortDescription matching iOS City.swift Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ero views Previously, matched hero transitions only showed the source widget during the entire animation, causing a visual "pop" when the destination widget appeared at the end. This adds iOS-matching cross-fade behavior where both source and destination snapshots are rendered simultaneously with opposing opacity during the morph animation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… size Previously, matched hero view snapshots were re-laid out at each intermediate animation size, causing distortion when source and destination had different aspect ratios (e.g., 200x298 card vs full-screen detail). Now each snapshot renders at its native captured size and uses FittedBox(fit: BoxFit.cover) to scale into the current animated rect, mimicking iOS CALayer bitmap snapshot behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BoxFit.cover caused clipping at intermediate animation sizes when source and destination had different aspect ratios. BoxFit.fill stretches the snapshot to exactly fill the animated rect, matching how iOS stretches CALayer bitmap snapshots during morph animations. The cross-fade between source and destination masks any intermediate distortion. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…rendering Source snapshots are bitmap-scaled via FittedBox.fill (like iOS CALayer snapshots) since they have fixed sizes and are fading out. Destination snapshots render directly at the animated rect size, allowing widgets with StackFit.expand to adapt naturally without FittedBox distortion. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The AnimationController's value listener and status listener both fire in the same animation tick. The value listener sets progress to 1.0 and notifies the overlay, but the status listener immediately calls _complete() which clears the overlay entries. Since Flutter only rebuilds once per frame with the latest value, the final frame at progress=1.0 was never rendered — causing the animation to appear to "zoom past" its target before snapping to the destination. Fix: defer _complete() via addPostFrameCallback so the overlay paints the final position before being cleared. Added a state guard to handle edge cases where the deferred callback fires after the engine resets. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
HeroPageRoute now implements complete iOS-parity route transitions: push, pull, slide, zoomSlide, cover, uncover, pageIn, pageOut, fade, zoom, zoomOut, none, selectBy, and auto. Each type drives both the appearing route's primary animation and the covered route's secondary animation with matched iOS curves. Fix a critical tap-blocking bug where after one push+pop cycle, all taps on the source screen became non-functional. Root cause was _coveredByAnimationType being set permanently by setCoveredByType() but never cleared, causing stale transition wrappers to remain in the widget tree. Fixed by returning plain opaqueChild when both animations are settled and skipping secondary wrappers when secondaryAnimation is dismissed. Also fixes List-to-Grid example crash and removes debug logging. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
What even is this? The .md's look AI generated. Care to provide any details on what I'm even looking at here? (obviously something with flutter, but I don't use flutter nor do I even know where to start with a review this large with an empty description) |
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2026-02-01.at.11.57.15.mp4Simulator.Screen.Recording.-.iPhone.16.Pro.-.2026-02-05.at.09.15.58.mp4I got claude 4.5 to read all code - and convert to flutter - in the first pass - it got all the fundamental animations wrong
the official flutter hero package is a botched code - this actually works using the same fundamental id tag cross fade between pages - i dont expect this to be merged to main - and you can dismiss - but consider merging it into an orphaned fork - and then others can benefit. (there's also a rough edge here and there that needs more work / help) |
There was a problem hiding this comment.
Pull request overview
This PR introduces a Flutter port of the iOS Hero transitions library, aiming for 100% example parity with the original iOS implementation. The library provides declarative custom view transitions with support for advanced features like spring animations, cascade effects, and interactive gestures.
Changes:
- Complete Flutter hero transitions library implementation with core widgets, transition engine, animators, and preprocessors
- Comprehensive example application demonstrating various transition types
- iOS project updates to support legacy examples
Reviewed changes
Copilot reviewed 113 out of 203 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| flutter_hero_transitions/pubspec.yaml | Package configuration with dependencies |
| flutter_hero_transitions/lib/ | Core library implementation (widgets, transitions, types, extensions) |
| flutter_hero_transitions/example/ | Example app demonstrating library features |
| Podfile | iOS deployment target updated to 15.0 |
| LegacyExamples/ | UIColor extension replacing ChameleonFramework dependency |
Files not reviewed (2)
- flutter_hero_transitions/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Language not supported
- flutter_hero_transitions/example/ios/Runner.xcworkspace/contents.xcworkspacedata: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| errorBuilder: (_, __, ___) => Container( | ||
| width: isGrid ? double.infinity : 80, | ||
| height: isGrid ? double.infinity : 80, | ||
| color: color.withValues(alpha: 0.7), |
There was a problem hiding this comment.
The Color.withValues method with named parameter 'alpha' is not available in Flutter versions before 3.27. Since the pubspec specifies 'flutter: ">=3.10.0"', this will fail on older Flutter versions. Use Color.withOpacity() instead.
| // --- Widgets --- | ||
| export 'src/widgets/hero_view.dart'; | ||
| export 'src/widgets/hero_overlay.dart'; | ||
| export 'src/widgets/hero_overlay_v2.dart'; // Optimized overlay |
There was a problem hiding this comment.
The export refers to 'hero_overlay_v2.dart' but the actual file in this PR is named 'hero_overlay_optimized.dart'. This will cause a compilation error.
| export 'src/transition/hero_page_route.dart'; | ||
| export 'src/transition/hero_navigator_observer.dart'; | ||
| export 'src/transition/hero_context.dart'; | ||
| export 'src/transition/hero_registry.dart'; | ||
|
|
||
| // --- Animator --- | ||
| export 'src/animator/hero_default_animator.dart'; | ||
| export 'src/animator/hero_animation_entry.dart'; | ||
| export 'src/animator/arc_tween.dart'; | ||
| export 'src/animator/hero_progress_runner.dart'; | ||
| export 'src/animator/hero_spring_simulation.dart'; | ||
| export 'src/animator/hero_spring_v2.dart'; // Improved spring with settling detection | ||
| export 'src/animator/hero_display_link.dart'; // CADisplayLink-style timing | ||
|
|
||
| // --- Plugins --- | ||
| export 'src/plugins/hero_plugin.dart'; | ||
| export 'src/plugins/hero_debug_plugin.dart'; |
There was a problem hiding this comment.
The exports reference files that don't exist in this PR: 'hero_default_animator.dart', 'hero_spring_v2.dart', 'hero_debug_plugin.dart', and 'hero_page_route.dart'. These missing exports will cause compilation failures.
| // (3D perspective, arc visualization, scrub slider). | ||
| // When HeroDebugPlugin.isEnabled is false, it behaves identically | ||
| // to a plain HeroOverlayV2. | ||
| const HeroDebugWrapper(), |
There was a problem hiding this comment.
The code references 'HeroDebugWrapper' which is imported but the corresponding file doesn't exist in this PR. This will cause a compilation error when trying to run the example app.
| void main() { | ||
| // Debug plugin is off by default. Toggle it from the main menu | ||
| // (bug icon) to inspect transitions with the scrub slider. | ||
| HeroDebugPlugin.isEnabled = false; |
There was a problem hiding this comment.
The code references 'HeroDebugPlugin.isEnabled' which is imported from a file that doesn't exist in this PR. This will cause a compilation error.
| ], | ||
| ); | ||
| }, | ||
| home: const MainMenuScreen(), |
There was a problem hiding this comment.
The code references 'MainMenuScreen' which is imported from 'screens/main_menu_screen.dart', but this file doesn't exist in this PR. This will cause a compilation error when building the example app.
| rrect.shift(entry.currentShadowOffset), | ||
| Paint() | ||
| ..color = entry.currentShadowColor | ||
| .withValues(alpha: entry.currentShadowOpacity * entry.currentOpacity) |
There was a problem hiding this comment.
The Color.withValues method with named parameter 'alpha' is not available in the current stable Flutter SDK. This method was introduced in Flutter 3.27. Since the pubspec specifies 'flutter: ">=3.10.0"', this code will fail on Flutter versions before 3.27. Use Color.withOpacity() instead for broader compatibility.
| if (t < 1.0) | ||
| Opacity( | ||
| opacity: (1.0 - t).clamp(0.0, 1.0), | ||
| child: _buildScaledSnapshot( | ||
| entry.snapshotWidget, entry.sourceSnapshotSize, w, h, | ||
| ), | ||
| ), | ||
| ], | ||
| ); | ||
| } else { | ||
| // Single snapshot (unmatched views) — bitmap-scale to fill animated rect | ||
| contentWidget = _buildScaledSnapshot( | ||
| entry.snapshotWidget, entry.sourceSnapshotSize, w, h, | ||
| ); | ||
| } | ||
|
|
||
| return Positioned( | ||
| left: entry.currentRect.left, | ||
| top: entry.currentRect.top, | ||
| width: w, | ||
| height: h, | ||
| child: Transform( | ||
| transform: entry.currentTransform, | ||
| alignment: Alignment.center, | ||
| child: Opacity( | ||
| opacity: entry.currentOpacity.clamp(0.0, 1.0), | ||
| child: Stack( | ||
| clipBehavior: Clip.none, | ||
| children: [ | ||
| // Main content | ||
| SizedBox( | ||
| width: w, | ||
| height: h, | ||
| child: ClipRRect( | ||
| borderRadius: borderRadius, | ||
| child: DecoratedBox( | ||
| decoration: BoxDecoration( | ||
| color: entry.currentBackgroundColor, | ||
| borderRadius: borderRadius, | ||
| boxShadow: entry.currentBoxShadow != null | ||
| ? [entry.currentBoxShadow!] | ||
| : null, | ||
| ), | ||
| child: contentWidget, | ||
| ), | ||
| ), | ||
| ), | ||
| // Overlay | ||
| if (entry.overlayOpacity != null && | ||
| entry.overlayOpacity! > 0 && | ||
| entry.overlayColor != null) | ||
| Positioned.fill( | ||
| child: ClipRRect( | ||
| borderRadius: borderRadius, | ||
| child: ColoredBox( | ||
| color: entry.overlayColor! | ||
| .withValues(alpha: entry.overlayOpacity!.clamp(0.0, 1.0)), |
There was a problem hiding this comment.
The Color.withValues method with named parameter 'alpha' is not available in Flutter versions before 3.27. Since the pubspec specifies 'flutter: ">=3.10.0"', this will cause compilation errors. Use Color.withOpacity() instead for compatibility with Flutter 3.10+.
| platform :ios, '10.0' | ||
| platform :ios, '15.0' | ||
| use_frameworks! | ||
| pod 'CollectionKit', :inhibit_warnings => true |
There was a problem hiding this comment.
The pod 'CollectionKit', :inhibit_warnings => true dependency is declared without a version constraint, so CocoaPods will always fetch the latest available release of this third-party library at build time. This mutable reference creates a supply-chain risk: if the CollectionKit pod or its distribution channel is compromised in the future, your app could silently pick up and execute malicious code during build or at runtime. Pin this pod to a specific, vetted version (or commit / vendored copy) to ensure builds only consume a known-good artifact.
No description provided.