Created
January 11, 2023 15:09
-
-
Save iapicca/9d30f8d10b91593409e139e436a8886e to your computer and use it in GitHub Desktop.
parallax demo
This file contains hidden or 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:flutter/material.dart'; | |
| void main() => runApp(const MaterialApp(home: ParallaxWidget())); | |
| class ParallaxWidget extends StatelessWidget { | |
| final List<LocationItem> locations; | |
| const ParallaxWidget({ | |
| super.key, | |
| this.locations = LocationItems.all, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return SingleChildScrollView( | |
| child: Column( | |
| children: [ | |
| for (final location in locations) | |
| LocationItemWidget( | |
| imageUrl: location.imageUrl, | |
| name: location.name, | |
| place: location.place, | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| @immutable | |
| class LocationItemWidget extends StatelessWidget { | |
| const LocationItemWidget({ | |
| super.key, | |
| required this.imageUrl, | |
| required this.name, | |
| required this.place, | |
| }); | |
| final String imageUrl; | |
| final String name; | |
| final String place; | |
| @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: Stack( | |
| children: [ | |
| ParallaxBackground(imageUrl: imageUrl), | |
| const ParallaxGradient(), | |
| ParallaxTitle( | |
| name: name, | |
| place: 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(); | |
| } | |
| class _ParallaxBackgroundState extends State<ParallaxBackground> { | |
| late GlobalKey _backgroundKey; | |
| @override | |
| void initState() { | |
| _backgroundKey = widget.backgroundKey ?? GlobalKey(); | |
| super.initState(); | |
| } | |
| @override | |
| Widget build(context) => Flow( | |
| delegate: ParallaxFlowDelegate( | |
| scrollable: Scrollable.of(context)!, | |
| 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