diff --git a/lib/common/extensions/scroll_controller_extensions.dart b/lib/common/extensions/scroll_controller_extensions.dart new file mode 100644 index 000000000..23a1ea535 --- /dev/null +++ b/lib/common/extensions/scroll_controller_extensions.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; + +extension ScrollControllerExtensions on ScrollController { + void handleScrollForFab(AnimationController hideFabController) { + print(position.userScrollDirection); + print(position.pixels); + print(position.atEdge); + switch (position.userScrollDirection) { + case ScrollDirection.idle: + break; + case ScrollDirection.forward: + hideFabController.forward(); + break; + case ScrollDirection.reverse: + hideFabController.reverse(); + break; + } + + if (position.pixels == 0 && position.atEdge) { + //User is at the top, so lets hide the fab + hideFabController.reverse(); + } + } + + void goToTheTop() => animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); +} diff --git a/lib/ui/pages/artifacts_page.dart b/lib/ui/pages/artifacts_page.dart index 70901ad05..2fc46b55e 100644 --- a/lib/ui/pages/artifacts_page.dart +++ b/lib/ui/pages/artifacts_page.dart @@ -12,6 +12,7 @@ import '../widgets/artifacts/artifact_info_card.dart'; import '../widgets/common/loading.dart'; import '../widgets/common/sliver_nothing_found.dart'; import '../widgets/common/sliver_page_filter.dart'; +import '../widgets/common/sliver_scaffold_with_fab.dart'; class ArtifactsPage extends StatefulWidget { @override @@ -31,7 +32,7 @@ class _ArtifactsPageState extends State with AutomaticKeepAliveCl builder: (context, state) { return state.map( loading: (_) => const Loading(), - loaded: (state) => CustomScrollView( + loaded: (state) => SliverScaffoldWithFab( slivers: [ SliverPageFilter( search: state.search, diff --git a/lib/ui/pages/character_page.dart b/lib/ui/pages/character_page.dart index 6da4f6a20..049c71055 100644 --- a/lib/ui/pages/character_page.dart +++ b/lib/ui/pages/character_page.dart @@ -1,21 +1,57 @@ import 'package:flutter/material.dart'; +import '../../common/extensions/scroll_controller_extensions.dart'; import '../widgets/characters/character_detail.dart'; +import '../widgets/common/app_fab.dart'; -class CharacterPage extends StatelessWidget { +class CharacterPage extends StatefulWidget { const CharacterPage({Key key}) : super(key: key); + @override + _CharacterPageState createState() => _CharacterPageState(); +} + +class _CharacterPageState extends State with SingleTickerProviderStateMixin { + ScrollController _scrollController; + AnimationController _hideFabAnimController; + + @override + void initState() { + super.initState(); + + _scrollController = ScrollController(); + _hideFabAnimController = AnimationController( + vsync: this, + duration: kThemeAnimationDuration, + value: 0, // initially not visible + ); + _scrollController.addListener(() => _scrollController.handleScrollForFab(_hideFabAnimController)); + } + @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: SingleChildScrollView( - child: Stack( - fit: StackFit.passthrough, - clipBehavior: Clip.none, - children: const [CharacterDetailTop(), CharacterDetailBottom()], - )), + controller: _scrollController, + child: Stack( + fit: StackFit.passthrough, + clipBehavior: Clip.none, + children: const [CharacterDetailTop(), CharacterDetailBottom()], + ), + ), + ), + floatingActionButton: AppFab( + hideFabAnimController: _hideFabAnimController, + scrollController: _scrollController, ), ); } + + @override + void dispose() { + _scrollController.dispose(); + _hideFabAnimController.dispose(); + super.dispose(); + } } diff --git a/lib/ui/pages/characters_page.dart b/lib/ui/pages/characters_page.dart index 3e825ea01..ffeee3d09 100644 --- a/lib/ui/pages/characters_page.dart +++ b/lib/ui/pages/characters_page.dart @@ -11,6 +11,7 @@ import '../widgets/characters/character_card.dart'; import '../widgets/common/loading.dart'; import '../widgets/common/sliver_nothing_found.dart'; import '../widgets/common/sliver_page_filter.dart'; +import '../widgets/common/sliver_scaffold_with_fab.dart'; class CharactersPage extends StatefulWidget { @override @@ -30,7 +31,7 @@ class _CharactersPageState extends State with AutomaticKeepAlive builder: (context, state) { return state.map( loading: (_) => const Loading(), - loaded: (state) => CustomScrollView( + loaded: (state) => SliverScaffoldWithFab( slivers: [ SliverPageFilter( search: state.search, diff --git a/lib/ui/pages/weapons_page.dart b/lib/ui/pages/weapons_page.dart index 0c41b8f5e..3616d4051 100644 --- a/lib/ui/pages/weapons_page.dart +++ b/lib/ui/pages/weapons_page.dart @@ -10,6 +10,7 @@ import '../../models/weapons/weapon_card_model.dart'; import '../widgets/common/loading.dart'; import '../widgets/common/sliver_nothing_found.dart'; import '../widgets/common/sliver_page_filter.dart'; +import '../widgets/common/sliver_scaffold_with_fab.dart'; import '../widgets/weapons/weapon_bottom_sheet.dart'; import '../widgets/weapons/weapon_card.dart'; @@ -30,7 +31,7 @@ class _WeaponsPageState extends State with AutomaticKeepAliveClient builder: (context, state) { return state.map( loading: (_) => const Loading(), - loaded: (state) => CustomScrollView( + loaded: (state) => SliverScaffoldWithFab( slivers: [ SliverPageFilter( search: state.search, diff --git a/lib/ui/widgets/common/app_fab.dart b/lib/ui/widgets/common/app_fab.dart new file mode 100644 index 000000000..549db113e --- /dev/null +++ b/lib/ui/widgets/common/app_fab.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../../common/extensions/scroll_controller_extensions.dart'; + +class AppFab extends StatelessWidget { + final ScrollController scrollController; + final AnimationController hideFabAnimController; + + const AppFab({ + Key key, + @required this.scrollController, + @required this.hideFabAnimController, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: hideFabAnimController, + child: ScaleTransition( + scale: hideFabAnimController, + child: FloatingActionButton( + backgroundColor: Theme.of(context).primaryColor, + mini: true, + onPressed: () => scrollController.goToTheTop(), + heroTag: null, + child: const Icon(Icons.arrow_upward), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/common/sliver_scaffold_with_fab.dart b/lib/ui/widgets/common/sliver_scaffold_with_fab.dart new file mode 100644 index 000000000..8eaf35392 --- /dev/null +++ b/lib/ui/widgets/common/sliver_scaffold_with_fab.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import '../../../common/extensions/scroll_controller_extensions.dart'; +import 'app_fab.dart'; + +class SliverScaffoldWithFab extends StatefulWidget { + final List slivers; + + const SliverScaffoldWithFab({ + Key key, + @required this.slivers, + }) : super(key: key); + + @override + _SliverScaffoldWithFabState createState() => _SliverScaffoldWithFabState(); +} + +class _SliverScaffoldWithFabState extends State with SingleTickerProviderStateMixin { + ScrollController _scrollController; + AnimationController _hideFabAnimController; + + @override + void initState() { + super.initState(); + + _scrollController = ScrollController(); + _hideFabAnimController = AnimationController( + vsync: this, + duration: kThemeAnimationDuration, + value: 0, // initially not visible + ); + _scrollController.addListener(() => _scrollController.handleScrollForFab(_hideFabAnimController)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + controller: _scrollController, + slivers: widget.slivers, + ), + floatingActionButton: AppFab( + hideFabAnimController: _hideFabAnimController, + scrollController: _scrollController, + ), + ); + } + + @override + void dispose() { + _scrollController.dispose(); + _hideFabAnimController.dispose(); + super.dispose(); + } +}