Created
September 2, 2024 02:16
-
-
Save kumamotone/406ecd60bd3cd99574915d58a97dbc3e to your computer and use it in GitHub Desktop.
DartPad Gist
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter_hooks/flutter_hooks.dart'; | |
import 'package:hooks_riverpod/hooks_riverpod.dart'; | |
final scrollControllersProvider = Provider((ref) => [ | |
ScrollController(), | |
ScrollController(), | |
]); | |
void main() { | |
runApp(const ProviderScope(child: MyApp())); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp( | |
home: MainPage(), | |
); | |
} | |
} | |
class MainPage extends HookConsumerWidget { | |
const MainPage({super.key}); | |
@override | |
Widget build(BuildContext context, WidgetRef ref) { | |
final scrollControllers = ref.watch(scrollControllersProvider); | |
final currentIndex = useState(0); | |
void onTabTapped(int index) { | |
if (currentIndex.value == index) { | |
scrollControllers[index].animateTo( | |
0.0, | |
duration: const Duration(milliseconds: 300), | |
curve: Curves.easeOut, | |
); | |
} | |
currentIndex.value = index; | |
} | |
const commonSliverAppBar = SliverAppBar( | |
title: Text('Common AppBar'), | |
floating: true, | |
snap: true, | |
); | |
return Scaffold( | |
body: Stack( | |
children: [ | |
Offstage( | |
offstage: currentIndex.value != 0, | |
child: CustomScrollView( | |
controller: scrollControllers[0], | |
slivers: [ | |
commonSliverAppBar, | |
SliverList( | |
delegate: SliverChildBuilderDelegate( | |
(context, index) => | |
ListTile(title: Text('Tab 1 - Item $index')), | |
childCount: 30, | |
), | |
), | |
], | |
), | |
), | |
Offstage( | |
offstage: currentIndex.value != 1, | |
child: CustomScrollView( | |
controller: scrollControllers[1], | |
slivers: [ | |
commonSliverAppBar, | |
SliverList( | |
delegate: SliverChildBuilderDelegate( | |
(context, index) => | |
ListTile(title: Text('Tab 2 - Item $index')), | |
childCount: 30, | |
), | |
), | |
], | |
), | |
), | |
], | |
), | |
floatingActionButton: ScaleAndTranslateContainer( | |
scrollController: scrollControllers[currentIndex.value], | |
fabTranslationOffset: 36.0, | |
child: FloatingActionButton( | |
onPressed: () {}, | |
child: const Icon(Icons.add), | |
), | |
), | |
bottomNavigationBar: HeightContainer( | |
scrollController: scrollControllers[currentIndex.value], | |
bottomNavigationBarHeight: 56.0, | |
child: BottomNavigationBar( | |
currentIndex: currentIndex.value, | |
onTap: onTabTapped, | |
items: const <BottomNavigationBarItem>[ | |
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Tab 1'), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.text_snippet), label: 'Tab 2'), | |
], | |
), | |
), | |
); | |
} | |
} | |
abstract class ScrollHidableContainer extends HookWidget { | |
final ScrollController scrollController; | |
final Widget child; | |
const ScrollHidableContainer({ | |
required this.scrollController, | |
required this.child, | |
super.key, | |
}); | |
Animation<double> createAnimation(AnimationController controller); | |
@override | |
Widget build(BuildContext context) { | |
final animationController = useAnimationController( | |
duration: const Duration(milliseconds: 200), | |
); | |
final animation = createAnimation(animationController); | |
useEffect( | |
() { | |
void scrollListener() { | |
if (scrollController.position.userScrollDirection == | |
ScrollDirection.reverse) { | |
if (animationController.status != AnimationStatus.forward) { | |
animationController.forward(); | |
} | |
} else if (scrollController.position.userScrollDirection == | |
ScrollDirection.forward) { | |
if (animationController.status != AnimationStatus.reverse) { | |
animationController.reverse(); | |
} | |
} else if (scrollController.position.userScrollDirection == | |
ScrollDirection.idle) { | |
animationController.reverse(); | |
} | |
} | |
scrollController.addListener(scrollListener); | |
return () => scrollController.removeListener(scrollListener); | |
}, | |
[scrollController, animationController], | |
); | |
return AnimatedBuilder( | |
animation: animationController, | |
builder: (context, child) { | |
return buildAnimation(context, child, animation); | |
}, | |
child: child, | |
); | |
} | |
Widget buildAnimation( | |
BuildContext context, Widget? child, Animation<double> animation); | |
} | |
class ScaleAndTranslateContainer extends ScrollHidableContainer { | |
final double fabTranslationOffset; | |
const ScaleAndTranslateContainer({ | |
required super.scrollController, | |
required super.child, | |
this.fabTranslationOffset = 36.0, | |
super.key, | |
}); | |
@override | |
Animation<double> createAnimation(AnimationController controller) { | |
return CurvedAnimation( | |
parent: Tween<double>(begin: 1, end: 0).animate(controller), | |
curve: Curves.easeIn, | |
); | |
} | |
@override | |
Widget buildAnimation( | |
BuildContext context, Widget? child, Animation<double> animation) { | |
return Transform.translate( | |
offset: Offset(0, (1 - animation.value) * fabTranslationOffset), | |
child: ScaleTransition( | |
scale: animation, | |
child: child, | |
), | |
); | |
} | |
} | |
class HeightContainer extends ScrollHidableContainer { | |
final double bottomNavigationBarHeight; | |
const HeightContainer({ | |
required super.scrollController, | |
required super.child, | |
required this.bottomNavigationBarHeight, | |
super.key, | |
}); | |
@override | |
Animation<double> createAnimation(AnimationController controller) { | |
return Tween<double>(begin: bottomNavigationBarHeight, end: 0.0).animate( | |
CurvedAnimation( | |
parent: controller, | |
curve: Curves.easeIn, | |
), | |
); | |
} | |
@override | |
Widget buildAnimation( | |
BuildContext context, Widget? child, Animation<double> animation) { | |
return AnimatedContainer( | |
duration: const Duration(milliseconds: 200), | |
height: animation.value, | |
child: Wrap(children: [child!]), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment