flutterscroll

How to make my scroller over the image not cutting off its rounded container top?


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 down is OK

Scrolling up cuts off my rounded container: how to address this?

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")),
              ],
            ),
          ),
        ),
      ],
    );
  }
}


Solution

  • 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.