JONAS RODEHORST

Mastering Scroll Physics in Flutter Modal Bottom Sheets
15. August 2024

📱 Mastering Scroll Physics in Flutter Modal Bottom Sheets

Flutter

The Scroll Physics Dilemma in Flutter Modal Bottom Sheets

Have you ever struggled with scroll behavior in Flutter modal bottom sheets? If so, you're not alone. The default scroll physics can lead to a frustrating user experience, especially when trying to close the modal by scrolling.

The Problem

Modal bottom sheets in Flutter often contain scrollable content. The default BouncingScrollPhysics works well at the bottom of the content, providing a satisfying bounce effect. However, it causes issues at the top.

When a user scrolls down at the top of the content, instead of closing the modal, the content shifts downward due to the bouncing effect. This behavior is counterintuitive and can confuse users who expect the modal to close.

The Solution

To address this issue, we need custom scroll physics that behave differently at the top and bottom of the content. Luckily, a solution exists in the form of a custom ScrollPhysics class.

The BottomModalScrollPhysics class provides the desired behavior. It prevents bouncing at the top, allowing the modal to close when scrolled down. At the bottom, it maintains the usual bouncing effect for a smooth experience.

Implementing the Solution

Here's how you can use the BottomModalScrollPhysics in your Flutter project:

  1. First, create a new file named bottom_modal_scroll_physics.dart.
  2. Copy the BottomModalScrollPhysics class into this file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
 
class BottomModalScrollPhysics extends ScrollPhysics {
  /// Creates scroll physics that prevent the scroll offset from exceeding the
  /// top bound of the modal.
  const BottomModalScrollPhysics({ScrollPhysics? parent})
      : super(parent: parent);
 
  @override
  BottomModalScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return BottomModalScrollPhysics(parent: buildParent(ancestor));
  }
 
  @override
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    assert(() {
      if (value == position.pixels) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary(
              '$runtimeType.applyBoundaryConditions() was called redundantly.'),
          ErrorDescription(
              'The proposed new position, $value, is exactly equal to the current position of the '
              'given ${position.runtimeType}, ${position.pixels}.\n'
              'The applyBoundaryConditions method should only be called when the value is '
              'going to actually change the pixels, otherwise it is redundant.'),
          DiagnosticsProperty<ScrollPhysics>(
              'The physics object in question was', this,
              style: DiagnosticsTreeStyle.errorProperty),
          DiagnosticsProperty<ScrollMetrics>(
              'The position object in question was', position,
              style: DiagnosticsTreeStyle.errorProperty)
        ]);
      }
      return true;
    }());
    final direction = position.axisDirection;
    // Normal vertical scroll
    if (direction == AxisDirection.down) {
      if (value < position.pixels &&
          position.pixels <= position.minScrollExtent) {
        // underscroll
        return value - position.pixels;
      }
 
      if (value < position.minScrollExtent &&
          position.minScrollExtent < position.pixels) {
        // hit top edge
        return value - position.minScrollExtent;
      }
    }
    // Reversed vertical scroll
    else if (direction == AxisDirection.up) {
      if (position.maxScrollExtent <= position.pixels &&
          position.pixels < value) {
        // overscroll
        return value - position.pixels;
      }
 
      if (position.pixels < position.maxScrollExtent &&
          position.maxScrollExtent < value) {
        // hit bottom edge
        return value - position.maxScrollExtent;
      }
    }
    if (parent != null) return super.applyBoundaryConditions(position, value);
    return 0.0;
  }
}
 
  1. Import the file in your modal bottom sheet widget.
  2. Apply the custom physics to your ScrollView:
ListView(
  physics: const BottomModalScrollPhysics(),
  children: [
    // Your list items here
  ],
)
 
// or
 
SingleChildScrollView(
  physics: const BottomModalScrollPhysics(),
  child: Column(
    children: [
      // Your content here
    ],
  ),
)

Conclusion

By implementing this custom scroll physics, you can significantly improve the user experience of your modal bottom sheets. Users will be able to close the modal intuitively by scrolling down at the top, while still enjoying a smooth bouncing effect at the bottom.

Remember, small details like scroll behavior can make a big difference in how users perceive and interact with your app. Happy coding!