main.dart
และ detail_page.dart
แล้วให้ตัง tag ของทั้งคู่ให้เหมือนกัน ซึ่งเราก็ตั้งให้เป็นชื่อ title ของไอคอนแต่ละอันimport 'package:flutter/material.dart'; | |
import 'package:icon_showcase_animation_part/color_palette.dart'; | |
import 'package:icon_showcase_animation_part/icon_data.dart'; | |
import 'package:icon_showcase_animation_part/detail_page.dart'; | |
void main() => runApp(MyApp()); | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Icon Showcase', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: MyHomePage(title: 'Icon Showcase'), | |
debugShowCheckedModeBanner: false, | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
MyHomePage({Key key, this.title}) : super(key: key); | |
final String title; | |
@override | |
_MyHomePageState createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: ColorPalette.grey90, | |
appBar: AppBar( | |
backgroundColor: Colors.transparent, | |
elevation: 0.0, | |
title: Text( | |
widget.title, | |
style: TextStyle(color: ColorPalette.grey10), | |
), | |
leading: Icon( | |
Icons.menu, | |
color: ColorPalette.grey10, | |
), | |
), | |
body: ListView.builder( | |
itemCount: iconList.length, | |
itemBuilder: (context, index) => InkWell( | |
onTap: () => Navigator.of(context).push(MaterialPageRoute( | |
builder: (context) => DetailPage( | |
iconData: iconList[index], | |
))), | |
child: Stack(children: <Widget>[ | |
Hero( | |
tag: iconList[index].title, | |
child: Card( | |
color: ColorPalette.grey10, | |
margin: EdgeInsets.all(10), | |
elevation: 10.0, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(25.0)), | |
child: Padding( | |
padding: const EdgeInsets.all(30.0), | |
child: ListTile( | |
leading: Icon( | |
iconList[index].icon, | |
size: 45.0, | |
color: Colors.transparent, | |
), | |
), | |
)), | |
), | |
Padding( | |
padding: const EdgeInsets.all(40.0), | |
child: ListTile( | |
title: Hero( | |
tag: 'title\${iconList[index].title}', | |
child: Text( | |
iconList[index].title, | |
style: TextStyle( | |
color: Colors.black87, fontSize: 20.0), | |
), | |
), | |
leading: Hero( | |
tag: 'icon\${iconList[index].title}', | |
child: Icon( | |
iconList[index].icon, | |
size: 45.0, | |
color: ColorPalette.grey60, | |
), | |
)), | |
), | |
]), | |
), | |
)); | |
} | |
} |
import 'package:icon_showcase_animation_part/color_palette.dart'; | |
import 'package:icon_showcase_animation_part/icon_data.dart'; | |
import 'package:flutter/material.dart'; | |
class DetailPage extends StatefulWidget { | |
final IconModel iconData; | |
DetailPage({Key key, @required this.iconData}) : super(key: key); | |
@override | |
_DetailPageState createState() => _DetailPageState(); | |
} | |
class _DetailPageState extends State<DetailPage> { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: ColorPalette.grey90, | |
body: SafeArea( | |
child: Stack(children: <Widget>[ | |
Hero( | |
tag: widget.iconData.title, | |
child: Card( | |
margin: EdgeInsets.all(10), | |
elevation: 10.0, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(25.0)), | |
child: Stack(children: <Widget>[ | |
Container( | |
width: MediaQuery.of(context).size.width, | |
height: 500, | |
child: SizedBox()), | |
])), | |
), | |
Hero( | |
tag: 'icon\${widget.iconData.title}', | |
child: Card( | |
margin: EdgeInsets.all(10), | |
clipBehavior: Clip.antiAlias, | |
elevation: 0.0, | |
color: Colors.transparent, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(25.0)), | |
child: Stack(children: <Widget>[ | |
Positioned.fill( | |
bottom: -90, | |
right: -90, | |
child: Align( | |
alignment: Alignment.bottomRight, | |
child: Icon( | |
widget.iconData.icon, | |
size: 400, | |
color: ColorPalette.grey30, | |
))), | |
Container( | |
width: MediaQuery.of(context).size.width, | |
height: 500, | |
child: Padding( | |
padding: const EdgeInsets.only(left: 20.0, top: 20.0), | |
child: SizedBox())), | |
])), | |
), | |
Card( | |
margin: EdgeInsets.all(10), | |
clipBehavior: Clip.antiAlias, | |
elevation: 0.0, | |
color: Colors.transparent, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(25.0)), | |
child: Stack(children: <Widget>[ | |
Container( | |
width: MediaQuery.of(context).size.width, | |
height: 500, | |
child: Padding( | |
padding: const EdgeInsets.only(left: 20.0, top: 20.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
InkWell( | |
onTap: () { | |
Navigator.of(context).pop(true); | |
return Future.value(false); | |
}, | |
child: Icon( | |
Icons.arrow_back, | |
), | |
), | |
Hero( | |
tag: 'title\${widget.iconData.title}', | |
child: Text( | |
widget.iconData.title, | |
style: TextStyle( | |
color: Colors.black87, fontSize: 60.0), | |
), | |
), | |
], | |
), | |
)), | |
])), | |
]), | |
), | |
); | |
} | |
} |
main.dart
และ detail_page.dart
ไปคลุมด้วย Widget Material และแทรกพารามีเตอร์ type: MaterialType.transparency
เพิ่มเข้าไป ก็จะพอแก้ขัดไปคร่าวๆได้view_state.dart
enum ViewState { | |
enlarge, | |
enlarged, | |
shrink, | |
shrunk, | |
} |
title_hero_flight.dart
import 'package:flutter/material.dart'; | |
import 'package:icon_showcase_animation_part/view_state.dart'; | |
class DestinationTitleContent extends StatelessWidget { | |
final String text; | |
final double fontSize; | |
final int maxLines; | |
final TextOverflow overflow; | |
final bool isOverflow; | |
final Color color; | |
const DestinationTitleContent({ | |
Key key, | |
this.text, | |
this.fontSize, | |
this.maxLines, | |
this.overflow, | |
this.isOverflow, | |
this.color, | |
}) : super(key: key); | |
Widget _buildTitleText() => Text( | |
text, | |
maxLines: maxLines, | |
overflow: overflow, | |
style: TextStyle( | |
color: color, | |
fontSize: fontSize, | |
), | |
); | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
color: Colors.transparent, | |
child: isOverflow | |
? OverflowBox( | |
alignment: Alignment.topLeft, | |
maxWidth: double.infinity, | |
maxHeight: double.infinity, | |
child: _buildTitleText(), | |
) | |
: _buildTitleText(), | |
); | |
} | |
} | |
class DestinationTitle extends StatefulWidget { | |
final String title; | |
final ViewState viewState; | |
final double smallFontSize; | |
final double largeFontSize; | |
final int maxLines; | |
final TextOverflow textOverflow; | |
final bool isOverflow; | |
final Color color; | |
const DestinationTitle( | |
{Key key, | |
@required this.title, | |
@required this.viewState, | |
this.smallFontSize = 15.0, | |
this.largeFontSize = 48.0, | |
this.maxLines = 2, | |
this.textOverflow = TextOverflow.ellipsis, | |
this.isOverflow = false, | |
this.color = Colors.black}) | |
: super(key: key); | |
@override | |
_DestinationTitleState createState() => _DestinationTitleState(); | |
} | |
class _DestinationTitleState extends State<DestinationTitle> | |
with SingleTickerProviderStateMixin { | |
AnimationController _animationController; | |
Animation<double> _fontSizeTween; | |
double fontSize; | |
@override | |
void initState() { | |
super.initState(); | |
_animationController = AnimationController( | |
vsync: this, | |
duration: Duration(milliseconds: 500), | |
)..addListener(() { | |
setState(() { | |
fontSize = _fontSizeTween.value; | |
}); | |
}); | |
switch (widget.viewState) { | |
case ViewState.enlarge: | |
_fontSizeTween = Tween<double>( | |
begin: widget.smallFontSize, | |
end: widget.largeFontSize, | |
).animate( | |
CurvedAnimation( | |
parent: _animationController, | |
curve: Curves.easeInOutSine, | |
), | |
); | |
_animationController.forward(from: 0.0); | |
break; | |
case ViewState.enlarged: | |
fontSize = widget.largeFontSize; | |
break; | |
case ViewState.shrink: | |
_fontSizeTween = Tween<double>( | |
begin: widget.largeFontSize, | |
end: widget.smallFontSize, | |
).animate( | |
CurvedAnimation( | |
parent: _animationController, | |
curve: Curves.easeInOutSine, | |
), | |
); | |
_animationController.forward(from: 0.0); | |
break; | |
case ViewState.shrunk: | |
fontSize = widget.smallFontSize; | |
break; | |
} | |
} | |
@override | |
void dispose() { | |
_animationController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return DestinationTitleContent( | |
text: widget.title, | |
fontSize: fontSize, | |
maxLines: widget.maxLines, | |
overflow: widget.textOverflow, | |
isOverflow: widget.isOverflow, | |
color: widget.color); | |
} | |
} |
title_hero_flight.dart
นี้ไปใช้ก่อนก็ได้main.dart
กับ detail_page.dart
กันต่อ เอาของใหม่ของเรามาใช้งานmain.dart
_initAnimationController()
จะทำหน้าที่ประกาศค่าแรกเริ่มให้ _animationController ของเรา โดยเราตั้งเวลาเล่นไว้ที่ 500 ms เมื่อสร้างเสร็จก็เอาฟังก์ชั้นนี้ไปใส่ใน initState()
และสุดท้ายอย่าลืม dispose _animationController ของเราที่ ฟังก์ชั่น _dispose()_
ด้วย_animationController.forward(from: 0.0);
ตอนก่อนกำลังเปลี่ยนไปหน้า DetailPage
และ _animationController.reverse(from: 1.0);
เมื่อกลับมาจากหน้าก่อนหน้า แต่ก่อนจะรัน ก็ต้องมีหลักการเช็คเล็กน้อยdetail_page.dart
ด้วยinitState()
เราจะ _animationController.forward()
ไปเลยfade_page_route.dart
แล้วก็อปแปะเลยimport 'package:flutter/material.dart'; | |
class FadePageRoute<T> extends PageRoute<T> { | |
FadePageRoute({ | |
@required this.builder, | |
RouteSettings settings, | |
this.maintainState = true, | |
bool fullscreenDialog = false, | |
}) : assert(builder != null), | |
assert(maintainState != null), | |
assert(fullscreenDialog != null), | |
assert(opaque), | |
super(settings: settings, fullscreenDialog: fullscreenDialog); | |
/// Builds the primary contents of the route. | |
final WidgetBuilder builder; | |
@override | |
final bool maintainState; | |
@override | |
Duration get transitionDuration => const Duration(milliseconds: 550); | |
@override | |
Color get barrierColor => null; | |
@override | |
String get barrierLabel => null; | |
@override | |
bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) { | |
return previousRoute is FadePageRoute; | |
} | |
@override | |
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) { | |
// Don't perform outgoing animation if the next route is a fullscreen dialog. | |
return (nextRoute is FadePageRoute && !nextRoute.fullscreenDialog); | |
} | |
@override | |
Widget buildPage( | |
BuildContext context, | |
Animation<double> animation, | |
Animation<double> secondaryAnimation, | |
) { | |
final Widget result = builder(context); | |
assert(() { | |
if (result == null) { | |
throw FlutterError( | |
'The builder for route "\${settings.name}" returned null.\ ' | |
'Route builders must never return null.'); | |
} | |
return true; | |
}()); | |
return Semantics( | |
scopesRoute: true, | |
explicitChildNodes: true, | |
child: result, | |
); | |
} | |
@override | |
Widget buildTransitions(BuildContext context, Animation<double> animation, | |
Animation<double> secondaryAnimation, Widget child) { | |
// final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme; | |
return _FadeInPageTransition(routeAnimation: animation, child: child); | |
} | |
@override | |
String get debugLabel => '\${super.debugLabel}(\${settings.name})'; | |
} | |
class _FadeInPageTransition extends StatelessWidget { | |
_FadeInPageTransition({ | |
Key key, | |
@required | |
Animation<double> | |
routeAnimation, // The route's linear 0.0 - 1.0 animation. | |
@required this.child, | |
}) : _opacityAnimation = routeAnimation.drive(_easeInTween), | |
super(key: key); | |
static final Animatable<double> _easeInTween = | |
CurveTween(curve: Curves.easeIn); | |
final Animation<double> _opacityAnimation; | |
final Widget child; | |
@override | |
Widget build(BuildContext context) { | |
return FadeTransition( | |
opacity: _opacityAnimation, | |
child: child, | |
); | |
} | |
} |
main.dart
import เข้ามาแล้วเปลี่ยน MaterialPageRoute เป็น FadePageRoute