Skip to content

Instantly share code, notes, and snippets.

@kumamotone
Created September 2, 2024 02:16
Show Gist options
  • Save kumamotone/406ecd60bd3cd99574915d58a97dbc3e to your computer and use it in GitHub Desktop.
Save kumamotone/406ecd60bd3cd99574915d58a97dbc3e to your computer and use it in GitHub Desktop.
DartPad Gist
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