Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created January 8, 2020 21:31
Show Gist options
  • Save slightfoot/9e11a0edab37b61184a1f2a34a7b9807 to your computer and use it in GitHub Desktop.
Save slightfoot/9e11a0edab37b61184a1f2a34a7b9807 to your computer and use it in GitHub Desktop.
Fun with Route Animations - by Simon Lightfoot - #HumpDayQandA - 8th Janurary 2020
// MIT License
//
// Copyright (c) 2019 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:ui' show lerpDouble;
import 'package:flutter/material.dart';
void main() {
final List<MyData> data = [
MyData("1", 100),
MyData("2", 200),
MyData("3", 300),
MyData("4", 400),
];
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.blue,
canvasColor: Colors.blueGrey,
accentColor: Colors.orangeAccent[100],
),
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder(
settings: settings,
pageBuilder: (_, __, ___) => HomeScreen(data: data),
);
},
),
);
}
@immutable
class MyData {
const MyData(this.param1, this.param2);
final String param1;
final int param2;
}
class HomeScreen extends StatelessWidget {
const HomeScreen({
Key key,
@required this.data,
}) : super(key: key);
final List<MyData> data;
void navigateToDetails(BuildContext context, MyData item, String tag) {
Navigator.of(context).push(DetailScreen.route(item, tag));
}
@override
Widget build(BuildContext context) {
final route = ModalRoute.of(context);
return Material(
child: SafeArea(
child: FadeTransition(
opacity: ReverseAnimation(route.secondaryAnimation),
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
final item = data[index];
final heroTag = 'details-$index';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: HeadingWidget(
heroTag: heroTag,
value: 0.0,
fromTitle: item.param1,
toTitle: 'Details for ${item.param1}',
onTap: () => navigateToDetails(context, item, heroTag),
),
);
},
),
),
),
);
}
}
class DetailScreen extends StatefulWidget {
static Route<dynamic> route(MyData data, String heroTag) {
return PageRouteBuilder(
opaque: false,
transitionDuration: const Duration(milliseconds: 450),
pageBuilder: (BuildContext context, _, __) {
return DetailScreen._(
data: data,
heroTag: heroTag,
);
},
);
}
const DetailScreen._({
Key key,
@required this.data,
this.heroTag = 'tag',
}) : super(key: key);
final MyData data;
final String heroTag;
@override
_DetailScreenState createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
@override
Widget build(BuildContext context) {
final route = ModalRoute.of(context);
return Material(
type: MaterialType.transparency,
child: Column(
children: <Widget>[
HeadingWidget(
heroTag: widget.heroTag,
value: 1.0,
fromTitle: widget.data.param1,
toTitle: 'Details for ${widget.data.param1}',
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SlideTransition(
position: Tween<Offset>(
begin: Offset(0.0, 1.0),
end: Offset.zero,
).animate(route.animation),
child: Card(
child: Container(
alignment: Alignment.center,
child: Text(
'${widget.data.param2}',
style: TextStyle(
fontSize: 56.0,
),
),
),
),
),
),
),
],
),
);
}
}
class HeadingWidget extends StatelessWidget {
const HeadingWidget({
Key key,
@required this.heroTag,
@required this.value,
@required this.fromTitle,
@required this.toTitle,
this.onTap,
}) : super(key: key);
final String heroTag;
final double value;
final String fromTitle;
final String toTitle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Hero(
tag: heroTag,
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return _buildHeader(context, animation.value);
},
);
},
child: _buildHeader(context, value),
);
}
Widget _buildHeader(BuildContext context, double value) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final insetValue = lerpDouble(0.0, kMinInteractiveDimension, value);
return Theme(
data: theme.copyWith(
appBarTheme: theme.appBarTheme.copyWith(
textTheme: theme.accentTextTheme,
iconTheme: theme.accentIconTheme,
),
),
child: Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
color: theme.accentColor,
borderRadius: BorderRadius.circular(
8.0 * (1.0 - value),
),
boxShadow: kElevationToShadow[3],
),
padding: EdgeInsets.only(
top: mediaQuery.padding.top * value,
),
child: SizedBox(
width: double.infinity,
height: kToolbarHeight,
child: Stack(
children: <Widget>[
InkWell(
onTap: onTap,
child: SizedBox(
height: kToolbarHeight,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(
insetValue,
8.0,
8.0,
8.0,
),
child: DefaultTextStyle.merge(
style: theme.textTheme.title,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Opacity(
opacity: (1.0 - value),
child: Text(fromTitle),
),
Opacity(
opacity: value,
child: Text(toTitle),
),
],
),
),
),
),
),
Positioned(
top: 0.0,
left: insetValue - kMinInteractiveDimension,
child: const SizedBox(
height: kToolbarHeight,
child: BackButton(),
),
),
],
),
),
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment