


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
