I designed a controller that allows for having an image at the top and a scrolling container below. When the user scroll up, the image reduces its size, while scrolling the rest .
The problem I have is that the container is clipped out when we scroll up (see photos).
How can I go thru that?
Scrolling down is OK
Scrolling up cuts off my rounded container: how to address this?
My widget code:
/// A Scrolling widget that have an image at the top and a csolling container
/// below
class ScrollingImageWidget extends StatefulWidget {
final String imagePath;
final List<Widget> children;
final double imageCoverRate;
const ScrollingImageWidget({
super.key,
required this.imagePath,
required this.children,
this.imageCoverRate = 0.9,
});
@override
State<ScrollingImageWidget> createState() => _ScrollingImageWidgetState();
}
class _ScrollingImageWidgetState extends State<ScrollingImageWidget> {
late final size = MediaQuery.of(context).size;
late final normalImageHeight = size.height / 4 * widget.imageCoverRate;
late double imageHeight = size.height / 4;
final _scrollController = ScrollController();
double? scrollDelta;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
// color: Colors.pink,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(60),
topRight: Radius.circular(60),
),
),
width: size.width,
height: size.height,
child: Stack(
children: [
// The image
// AnimatedContainer(
// duration: const Duration(milliseconds: 20),
// curve: Curves.easeInOut,
SizedBox(
height: imageHeight,
width: double.infinity,
child: Image.asset(
widget.imagePath,
height: imageHeight,
width: double.infinity,
fit: BoxFit.cover,
),
),
// Text("image height: $imageHeight vs ${size.height / 4}\nscrol delta: ${scrollDelta}"),
// The scrolling content
NotificationListener<ScrollNotification>(
// Listen for scroll events
onNotification: (notification) {
if (notification is ScrollUpdateNotification) {
final rate =
_scrollController.position.pixels < 0 ? 4.0 : 1.0;
setState(() {
scrollDelta = notification.scrollDelta;
imageHeight = normalImageHeight -
_scrollController.position.pixels * rate;
// Clamp imageHeight to prevent negative values
imageHeight = imageHeight.clamp(0.0, size.height * .9);
});
}
return true; // Allow further notification propagation
},
child: Positioned(
top: imageHeight * widget.imageCoverRate,
child: SizedBox(
width: size.width,
height: size.height * 0.9 - imageHeight,
// child: Scrollbar(
// controller: _scrollController,
// thumbVisibility: true,
// trackVisibility: true,
child: ListView.builder(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
itemCount: widget.children.length,
itemBuilder: (context, index) => widget.children[index],
),
// ),
),
),
),
],
),
),
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
How I instantiate the scroller:
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return ScrollingImageWidget(
imagePath: "./lib/assets/images/speakbetter.main.jpg",
children: [
Container(
decoration: const BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(64),
topRight: Radius.circular(64),
),
boxShadow: [
BoxShadow(
color: Colors.grey, //.withOpacity(0.5),
spreadRadius: 5,
blurRadius: 7,
offset: Offset(
0, 3), // Adjust the offset to control the shadow direction
),
],
),
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Read the following",
style: Theme.of(context)
.textTheme
.headlineLarge
?.copyWith(fontWeight: ui.FontWeight.w900),
),
Container(
color: Colors.green,
width: double.infinity,
height: 350,
child: const Text("Container 2/3")),
Container(
color: Colors.blue,
width: double.infinity,
height: 550,
child: const Text("Container 3/3")),
],
),
),
),
],
);
}
}
To fix the clipping issue, move the outer Container
(the amber-colored one) inside the ScrollingImageWidget
and wrap it around the ListView.builder
. Also, make sure to set the clipBehavior
property to Clip.hardEdge
, so the children are clipped when they go beyond the container's top borders.
Container(
width: size.width,
height: size.height * 0.9 - imageHeight,
clipBehavior: Clip.hardEdge, // Ensures content is clipped at the container's border
decoration: const BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(64),
topRight: Radius.circular(64),
),
boxShadow: [
BoxShadow(
color: Colors.grey,
spreadRadius: 5,
blurRadius: 7,
offset: Offset(0, 3),
),
],
),
child: ListView.builder(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
itemCount: widget.children.length,
itemBuilder: (context, index) => widget.children[index],
),
)
Bonus tip:
It's better to use MediaQuery.sizeOf(context)
instead of MediaQuery.of(context).size
to prevent unnecessary rebuilds. This minor change can improve performance, especially in widgets that rely heavily on layout adjustments during scrolling.