Skip to content

Instantly share code, notes, and snippets.

@miguelpruivo
Last active April 10, 2019 11:32
Show Gist options
  • Save miguelpruivo/f7993a742b606cba00b8c0a73e283d13 to your computer and use it in GitHub Desktop.
Save miguelpruivo/f7993a742b606cba00b8c0a73e283d13 to your computer and use it in GitHub Desktop.
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