Last active
January 11, 2023 19:50
-
-
Save iapicca/77fcad4876b963a95caff19bc9667254 to your computer and use it in GitHub Desktop.
issue 115705
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:animations/animations.dart'; | |
import 'package:flutter/material.dart'; | |
typedef WidgetBuilderWithCallback = Widget Function( | |
BuildContext context, | |
VoidCallback callback, | |
); | |
void main() => runApp(const MaterialApp(home: ParallaxWidget())); | |
/// OPEN CONTAINER | |
class OpenContainerWidget extends StatelessWidget { | |
final ContainerTransitionType transition; | |
final Duration duration; | |
final Widget open; | |
final WidgetBuilderWithCallback closed; | |
final double elevation; | |
const OpenContainerWidget({ | |
super.key, | |
this.transition = ContainerTransitionType.fade, | |
this.duration = kThemeAnimationDuration, | |
this.elevation = 0, | |
required this.open, | |
required this.closed, | |
}); | |
@override | |
Widget build(context) => OpenContainer( | |
transitionType: transition, | |
transitionDuration: duration, | |
openBuilder: (context, _) => open, | |
closedBuilder: closed, | |
closedElevation: elevation, | |
); | |
} | |
class LocationDetail extends StatelessWidget { | |
final LocationItem location; | |
const LocationDetail({ | |
super.key, | |
required this.location, | |
}); | |
@override | |
Widget build(context) => Scaffold( | |
appBar: AppBar(title: Text(location.name)), | |
body: Image.network(location.imageUrl), | |
); | |
} | |
/// PARALLAX [https://gist.github.com/iapicca/9d30f8d10b91593409e139e436a8886e] | |
class ParallaxWidget extends StatelessWidget { | |
final List<LocationItem> locations; | |
const ParallaxWidget({ | |
super.key, | |
this.locations = LocationItems.all, | |
}); | |
@override | |
Widget build(context) => Scaffold( | |
appBar: AppBar(), | |
body: SingleChildScrollView( | |
child: Column( | |
children: [ | |
for (final location in locations) | |
LocationItemWidget(location: location) | |
], | |
), | |
), | |
); | |
} | |
class LocationItemWidget extends StatelessWidget { | |
final LocationItem location; | |
const LocationItemWidget({ | |
super.key, | |
required this.location, | |
}); | |
@override | |
Widget build(context) => Padding( | |
padding: const EdgeInsets.symmetric( | |
horizontal: 24, | |
vertical: 16, | |
), | |
child: AspectRatio( | |
aspectRatio: 16 / 9, | |
child: ClipRRect( | |
borderRadius: BorderRadius.circular(16), | |
child: OpenContainerWidget( | |
open: LocationDetail(location: location), | |
closed: (context, callback) => Stack( | |
children: [ | |
ParallaxBackground( | |
imageUrl: location.imageUrl, | |
), | |
const ParallaxGradient(), | |
ParallaxTitle( | |
name: location.name, | |
place: location.place, | |
) | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
class ParallaxGradient extends StatelessWidget { | |
const ParallaxGradient({super.key}); | |
@override | |
Widget build(context) => Positioned.fill( | |
child: DecoratedBox( | |
decoration: BoxDecoration( | |
gradient: LinearGradient( | |
colors: [ | |
Colors.transparent, | |
Colors.black.withOpacity(0.7), | |
], | |
begin: Alignment.topCenter, | |
end: Alignment.bottomCenter, | |
stops: const [ | |
0.6, | |
0.95, | |
], | |
), | |
), | |
), | |
); | |
} | |
class ParallaxBackground extends StatefulWidget { | |
final String imageUrl; | |
final GlobalKey? backgroundKey; | |
const ParallaxBackground({ | |
super.key, | |
required this.imageUrl, | |
this.backgroundKey, | |
}); | |
@override | |
State<ParallaxBackground> createState() => _ParallaxBackgroundState(); | |
} | |
/// hack part 1 | |
ScrollableState scrollableStateHack( | |
BuildContext context, { | |
required void Function(ScrollableState) storeScrollable, | |
required ScrollableState Function() getStoredScrollable, | |
}) { | |
final state = Scrollable.of(context); | |
return state != null | |
? () { | |
storeScrollable(state); | |
return state; | |
}() | |
: getStoredScrollable(); | |
} | |
class _ParallaxBackgroundState extends State<ParallaxBackground> { | |
late GlobalKey _backgroundKey; | |
/// hack part 2 | |
late ScrollableState? scrollableState; | |
@override | |
void initState() { | |
_backgroundKey = widget.backgroundKey ?? GlobalKey(); | |
super.initState(); | |
} | |
@override | |
Widget build(context) => Flow( | |
delegate: ParallaxFlowDelegate( | |
/// hack part 3 | |
scrollable: scrollableStateHack( | |
context, | |
storeScrollable: (state) => scrollableState = state, | |
getStoredScrollable: () => scrollableState!, | |
), | |
listItemContext: context, | |
backgroundImageKey: _backgroundKey, | |
), | |
children: [ | |
Image.network( | |
widget.imageUrl, | |
fit: BoxFit.cover, | |
key: _backgroundKey, | |
), | |
], | |
); | |
} | |
class ParallaxTitle extends StatelessWidget { | |
final String name; | |
final String place; | |
const ParallaxTitle({ | |
super.key, | |
required this.name, | |
required this.place, | |
}); | |
@override | |
Widget build(context) => Positioned( | |
left: 20, | |
bottom: 20, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Text( | |
name, | |
style: const TextStyle( | |
color: Colors.white, | |
fontSize: 20, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
Text( | |
place, | |
style: const TextStyle( | |
color: Colors.white, | |
fontSize: 14, | |
), | |
), | |
], | |
), | |
); | |
} | |
class ParallaxFlowDelegate extends FlowDelegate { | |
ParallaxFlowDelegate({ | |
required this.scrollable, | |
required this.listItemContext, | |
required this.backgroundImageKey, | |
}) : super(repaint: scrollable.position); | |
final ScrollableState scrollable; | |
final BuildContext listItemContext; | |
final GlobalKey backgroundImageKey; | |
@override | |
BoxConstraints getConstraintsForChild(i, constraints) => | |
BoxConstraints.tightFor(width: constraints.maxWidth); | |
@override | |
void paintChildren(context) { | |
// Calculate the position of this list item within the viewport. | |
final scrollableBox = scrollable.context.findRenderObject() as RenderBox; | |
final listItemBox = listItemContext.findRenderObject() as RenderBox; | |
final listItemOffset = listItemBox.localToGlobal( | |
listItemBox.size.centerLeft(Offset.zero), | |
ancestor: scrollableBox, | |
); | |
// Determine the percent position of this list item within the | |
// scrollable area. | |
final viewportDimension = scrollable.position.viewportDimension; | |
final scrollFraction = | |
(listItemOffset.dy / viewportDimension).clamp(0.0, 1.0); | |
// Calculate the vertical alignment of the background | |
// based on the scroll percent. | |
final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1); | |
// Convert the background alignment into a pixel offset for | |
// painting purposes. | |
final backgroundSize = | |
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox) | |
.size; | |
final listItemSize = context.size; | |
final childRect = verticalAlignment.inscribe( | |
backgroundSize, | |
Offset.zero & listItemSize, | |
); | |
// Paint the background. | |
context.paintChild( | |
0, | |
transform: Transform.translate( | |
offset: Offset( | |
0.0, | |
childRect.top, | |
), | |
).transform, | |
); | |
} | |
@override | |
bool shouldRepaint(ParallaxFlowDelegate oldDelegate) => | |
scrollable != oldDelegate.scrollable || | |
listItemContext != oldDelegate.listItemContext || | |
backgroundImageKey != oldDelegate.backgroundImageKey; | |
} | |
abstract class LocationItems { | |
static const all = [ | |
egypt, | |
indonesia, | |
mexico, | |
peru, | |
singapore, | |
switzerland, | |
usa, | |
]; | |
static const _urlPrefix = | |
'https://docs.flutter.dev/cookbook/img-files/effects/parallax'; | |
static const usa = LocationItem( | |
name: 'Mount Rushmore', | |
place: 'U.S.A', | |
imageUrl: '$_urlPrefix/01-mount-rushmore.jpg', | |
); | |
static const singapore = LocationItem( | |
name: 'Gardens By The Bay', | |
place: 'Singapore', | |
imageUrl: '$_urlPrefix/02-singapore.jpg', | |
); | |
static const peru = LocationItem( | |
name: 'Machu Picchu', | |
place: 'Peru', | |
imageUrl: '$_urlPrefix/03-machu-picchu.jpg', | |
); | |
static const switzerland = LocationItem( | |
name: 'Vitznau', | |
place: 'Switzerland', | |
imageUrl: '$_urlPrefix/04-vitznau.jpg', | |
); | |
static const indonesia = LocationItem( | |
name: 'Bali', | |
place: 'Indonesia', | |
imageUrl: '$_urlPrefix/05-bali.jpg', | |
); | |
static const mexico = LocationItem( | |
name: 'Mexico City', | |
place: 'Mexico', | |
imageUrl: '$_urlPrefix/06-mexico-city.jpg', | |
); | |
static const egypt = LocationItem( | |
name: 'Cairo', | |
place: 'Egypt', | |
imageUrl: '$_urlPrefix/07-cairo.jpg', | |
); | |
} | |
@immutable | |
class LocationItem { | |
const LocationItem({ | |
required this.imageUrl, | |
required this.name, | |
required this.place, | |
}); | |
final String imageUrl; | |
final String name; | |
final String place; | |
@override | |
bool operator ==(Object other) => | |
other is LocationItem && other.hashCode == hashCode; | |
@override | |
int get hashCode => Object.hashAll([imageUrl, name, place]); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment