Last active
April 10, 2019 11:32
-
-
Save miguelpruivo/f7993a742b606cba00b8c0a73e283d13 to your computer and use it in GitHub Desktop.
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 'dart:math'; | |
import 'package:cached_network_image/cached_network_image.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:jnation/resources/constants.dart'; | |
const double _kPercentageOfScreenAppBar = 0.25; | |
const double _kListViewTopBottomPadding = 25.0; | |
const double _kAppbarScrollOffset = 75.0; | |
class ParallaxLayout extends StatefulWidget { | |
final String title; | |
final String subtitle; | |
final String backgroundAsset; | |
final String thumbnailAvatarUrl; | |
final bool flipHeaderBackground; | |
final bool flexible; | |
final bool scrollable; | |
final List<Widget> body; | |
final EdgeInsets padding; | |
ParallaxLayout({ | |
Key key, | |
@required this.title, | |
@required this.backgroundAsset, | |
this.subtitle, | |
this.body, | |
this.padding, | |
this.flipHeaderBackground = false, | |
this.thumbnailAvatarUrl, | |
this.scrollable = false, | |
this.flexible = false, | |
}) : super(key: key); | |
_ParallaxLayoutState createState() => _ParallaxLayoutState(); | |
} | |
class _ParallaxLayoutState extends State<ParallaxLayout> { | |
final ScrollController _scrollController = ScrollController(); | |
final GlobalKey _headerKey = GlobalKey(); | |
final Tween<double> _parallaxTween = Tween(begin: 1.0, end: 1.3); | |
final Tween<double> _positionTween = Tween(begin: 75.0, end: 0.0); | |
final Tween<double> _textScaleTween = Tween(begin: 0.8, end: 1.0); | |
final AlignmentTween _textAlignTween = AlignmentTween(begin: Alignment(0.0, -0.975), end: Alignment(0.0, -0.80)); | |
List<Widget> _flexElements; | |
double _currentOffset = 0.0; | |
bool _markToNotRebuild = false; | |
@override | |
void initState() { | |
super.initState(); | |
_scrollController.addListener(_updateLayout); | |
if (widget.thumbnailAvatarUrl != null) { | |
WidgetsBinding.instance.addPostFrameCallback((_) => _insertRenderAvatarInTree()); | |
} | |
} | |
void _insertRenderAvatarInTree() { | |
final RenderBox headerRenderBox = _headerKey.currentContext.findRenderObject(); | |
final Size renderBoxSize = headerRenderBox.size; | |
_flexElements.add(_buildThumbnailAvatar(renderBoxSize.height, renderBoxSize.width / 2)); | |
setState(() => _markToNotRebuild = true); | |
} | |
void _updateLayout() => setState(() => _currentOffset = _scrollController.offset); | |
Widget _buildHeader(double topOffset, double scale, double maxWidth) { | |
return Positioned( | |
top: topOffset, | |
child: Transform.scale( | |
key: _headerKey, | |
scale: scale, | |
child: Transform( | |
transform: widget.flipHeaderBackground ? (Matrix4.identity()..rotateY(pi)) : Matrix4.identity(), | |
alignment: FractionalOffset.center, | |
child: Image.asset( | |
widget.backgroundAsset, | |
fit: BoxFit.fitWidth, | |
width: maxWidth, | |
), | |
), | |
), | |
); | |
} | |
Widget _buildTitle(Alignment currentTextAlignment, double currentTextScale, bool isBigScreen, BuildContext context) { | |
return SafeArea( | |
child: Align( | |
alignment: widget.subtitle != null && isBigScreen | |
? Alignment(currentTextAlignment.x, currentTextAlignment.y - 0.075) | |
: widget.thumbnailAvatarUrl != null | |
? Alignment(currentTextAlignment.x, currentTextAlignment.y - (isBigScreen ? 0.1 : 0.0)) | |
: currentTextAlignment, | |
child: Transform.scale( | |
scale: currentTextScale, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Padding( | |
padding: const EdgeInsets.only(bottom: 10.0, left: 20.0, right: 20.0), | |
child: FittedBox( | |
child: Text( | |
widget.title, | |
maxLines: 1, | |
textAlign: TextAlign.center, | |
style: Theme.of(context).textTheme.display3, | |
textScaleFactor: 1.0, | |
), | |
), | |
), | |
widget.subtitle != null | |
? Text( | |
widget.subtitle, | |
maxLines: 1, | |
textAlign: TextAlign.center, | |
style: Theme.of(context).textTheme.display3.copyWith(fontSize: 12.0), | |
textScaleFactor: 1.0, | |
) | |
: Container(), | |
], | |
), | |
), | |
), | |
); | |
} | |
Widget _buildThumbnailAvatar(double topOffset, double leftOffset) { | |
final isNarrowScreen = MediaQuery.of(context).size.height < 600; | |
final double radius = isNarrowScreen ? 40.0 : 50.0; | |
return Positioned( | |
top: topOffset - radius - 10.0, | |
left: leftOffset - radius, | |
child: CircleAvatar( | |
radius: radius + 3.0, | |
backgroundColor: Colors.white, | |
child: Hero( | |
tag: 'avatar' + widget.title, | |
child: CircleAvatar( | |
radius: radius, | |
backgroundColor: Theme.of(context).disabledColor, | |
backgroundImage: AssetImage(IMG_JNATION_OWL), | |
child: ClipOval( | |
child: CachedNetworkImage( | |
imageUrl: widget.thumbnailAvatarUrl, | |
errorWidget: (_, __, ___) => Image.asset(IMG_JNATION_OWL), | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final double maxHeight = MediaQuery.of(context).size.height; | |
final double maxWidth = MediaQuery.of(context).size.width; | |
final bool isBigDisplay = maxHeight >= 700.0; | |
final double sizeFactor = isBigDisplay ? 1.3 : 1.5; | |
final double maxAppBarHeight = _kPercentageOfScreenAppBar * maxHeight; | |
final double progress = max(0.0, (maxAppBarHeight - _currentOffset) / maxAppBarHeight); | |
final double currentParallaxEffect = widget.flexible ? _parallaxTween.transform(progress) : _parallaxTween.end; | |
final double currentPosition = widget.flexible ? max(0.0, _positionTween.transform(progress)) : _positionTween.end; | |
final double currentTextScale = widget.flexible ? isBigDisplay ? 1.0 : _textScaleTween.transform(progress) : _textScaleTween.end; | |
final double bodyTopPadding = (maxAppBarHeight - currentPosition) * sizeFactor; | |
final Alignment currentTextAlignment = widget.flexible ? _textAlignTween.lerp(progress) : _textAlignTween.end; | |
List<Widget> children; | |
if (!widget.flexible && !widget.scrollable) { | |
children = [ | |
Container( | |
height: maxAppBarHeight * (isBigDisplay ? 1.3 : 1.5), | |
child: Stack( | |
children: <Widget>[ | |
_buildHeader(0.0, _parallaxTween.end, maxWidth), | |
_buildTitle(Alignment(0.0, -0.35), 1.0, isBigDisplay, context), | |
], | |
), | |
) | |
]..addAll(widget.body); | |
} else if (!_markToNotRebuild) { | |
_flexElements = [ | |
Scrollbar( | |
child: ListView( | |
padding: EdgeInsets.only( | |
left: widget.padding?.left ?? 0.0, | |
right: widget.padding?.right ?? 0.0, | |
bottom: _kListViewTopBottomPadding, | |
top: _kListViewTopBottomPadding + bodyTopPadding, | |
), | |
controller: _scrollController, | |
children: widget.body, | |
), | |
), | |
_buildHeader(-min(_kAppbarScrollOffset, currentPosition), max(1.0, currentParallaxEffect), maxWidth), | |
_buildTitle(currentTextAlignment, currentTextScale, isBigDisplay, context), | |
buildCloseElement(context), | |
]; | |
} | |
return widget.scrollable || widget.flexible | |
? Stack( | |
alignment: Alignment.center, | |
children: _flexElements, | |
) | |
: Column( | |
children: children, | |
); | |
} | |
} | |
Widget buildCloseElement(BuildContext context) { | |
if (Navigator.of(context).canPop()) { | |
return Align( | |
alignment: const Alignment(-0.975, -1.0), | |
child: SafeArea( | |
child: IconButton( | |
icon: Icon(Icons.close), | |
color: Colors.white, | |
iconSize: 30.0, | |
splashColor: Colors.white, | |
onPressed: () => Navigator.of(context).pop(), | |
), | |
), | |
); | |
} | |
return Container(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment