Skip to content

Instantly share code, notes, and snippets.

@iapicca
Last active January 11, 2023 19:50
Show Gist options
  • Save iapicca/77fcad4876b963a95caff19bc9667254 to your computer and use it in GitHub Desktop.
Save iapicca/77fcad4876b963a95caff19bc9667254 to your computer and use it in GitHub Desktop.
issue 115705
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