TDLR:
I want to change the appearance of a custom modal sheet based on its route's animation position (i.e. change as the user slides it up and down). However, I don't want to have to convert any UI widget that shows the modal sheet to StatefulWidget
in order to manage that controller. Instead, I want to abstract the logic that gets the sheet's animation position inside of the sheet.
Full Explanation:
I have a widget I'm using as a modal bottom sheet that incorporates a sheet handle, and I want to animate the handle's width when the user changes the position of the bottom sheet. From what I can tell, the only way to get access to the scroll position of an active bottom sheet is to access an AnimationController
passed to the transitionAnimationController
property of showModalBottomSheet
.
However, I don't want to have to convert any widget that calls showModalBottomSheet
and shows this modal widget into a StatefulWidget
in order to create this AnimationController
and then pass the controller to both showModalBottomSheet
and the modal widget. I don't believe the state of the modal sheet is mounted when showModalBottomSheet
is called, so I don't think it's possible to abstract the creation of an AnimationController
inside of the sheet's state. This would mean that I need another way to access the route's animation position without explicitly passing an AnimationController
created in a parent.
Sheet handle:
class _ModalSheetHandleState extends State<_ModalSheetHandle>
with SingleTickerProviderStateMixin {
final _handelHeight = Insets.xs;
late final _controller = AnimationController(
duration: Timings.short,
vsync: this,
);
late final _animation =
Tween<double>(begin: 35, end: 50).animate(_controller);
@override
void initState() {
widget.modalSheetTransitionController.addListener(updateHandle);
super.initState();
}
@override
void dispose() {
widget.modalSheetTransitionController.removeListener(updateHandle);
_controller.dispose();
super.dispose();
}
updateHandle() {
if (widget.modalSheetTransitionController.isCompleted) {
_controller.forward();
} else if (!widget.modalSheetTransitionController.isCompleted &&
_controller.isCompleted) {
_controller.reverse();
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (_, __) => Container(
width: _animation.value,
height: _handelHeight,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(_handelHeight),
),
),
);
}
}
What I'm doing right now:
Currently, I'm creating the AnimationController
passed to showModalBottomSheet
in the widget that uses the modal bottom sheet. This is the opposite of what I want to do, since I want to abstract the stateful management of this controller inside of the modal sheet. Here's what I'm doing currently:
// I *don't* want to convert any UI widget using the modal
// sheet to a stateful widget.
class _SomeUIElementState extends State<SomeUIElement>
with SingleTickerProviderStateMixin {
late final _modalSheetTransitionController = AnimationController(
duration: Timings.med,
vsync: this,
);
@override
Widget build(BuildContext context) {
return SomeWidget(
child: Button(
onTap: () {
showModalBottomSheet(
context: context,
builder: (_) => ContextMenu(
modalSheetTransitionAnimationController:
_modalSheetTransitionController,
actions: /*actions*/,
),
transitionAnimationController:
_modalSheetTransitionController,
);
},
),
);
}
}
Figured out how to do this. Instead of accessing the route animation through an AnimationController, access it through:
ModalRoute.of(context)!.animation!
Updated modal handle:
class _ModalSheetHandleState extends State<_ModalSheetHandle>
with SingleTickerProviderStateMixin {
final _handelHeight = Insets.xs;
final double _collapsedHandleWidth = 35;
final double _expandedHandleWidth = 50;
bool _isExpanded = false;
@override
void didChangeDependencies() {
// Listen to the modal's route animation here. This calls
// dependOnInheritedWidgetOfExactType, which can't be called in initState.
// It is safe to call in didChangeDependencies, which is called immediately
// after initState.
final routeAnimation = ModalRoute.of(context)!.animation!;
routeAnimation.addListener(() => updateHandle(routeAnimation));
super.didChangeDependencies();
}
void updateHandle(Animation<double> routeAnimation) {
if (routeAnimation.isCompleted) {
setState(() => _isExpanded = true);
} else if (!routeAnimation.isCompleted && _isExpanded) {
setState(() => _isExpanded = false);
}
}
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Timings.short,
width: _isExpanded ? _expandedHandleWidth : _collapsedHandleWidth,
height: _handelHeight,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(_handelHeight),
),
);
}
}