Nate Wilson 9ee00ae168
Fix NestedScrollView inner position logic (#157756)
closes #40740

<p align="center">
  <img 
    src="https://github.com/user-attachments/assets/8a48abd0-466b-4e06-90f3-dd05869bac27"
    alt="NestedScrollController fix"
    height="720px"
  />
</p>

<details> <summary><h4>Demo source code</h4> (click to expand)</summary>

(this is a slightly more concise version of the code sample from #40740)

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(body: NewsScreen()),
    ),
  );
}

class NewsScreen extends StatelessWidget {
  const NewsScreen({super.key});

  static const List<String> _tabs = <String>['Featured', 'Popular', 'Latest'];

  static final List<Widget> _tabViews = <Widget>[
    for (final String name in _tabs)
      SafeArea(
        top: false,
        bottom: false,
        child: Builder(builder: (BuildContext context) {
          final handle = NestedScrollView.sliverOverlapAbsorberHandleFor(context);

          return NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification notification) => true,
            child: CustomScrollView(
              key: PageStorageKey<String>(name),
              slivers: <Widget>[
                SliverOverlapInjector(handle: handle),
                SliverPadding(
                  padding: const EdgeInsets.all(8.0),
                  sliver: SliverList(
                    delegate: SliverChildBuilderDelegate(
                      childCount: 30,
                      (BuildContext context, int index) => Container(
                        margin: const EdgeInsets.only(bottom: 8),
                        width: double.infinity,
                        height: 150,
                        color: const Color(0xFFB0A4C8),
                        alignment: Alignment.center,
                        child: Text(
                          '$name $index',
                          style: const TextStyle(fontWeight: FontWeight.w600),
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          );
        }),
      ),
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => <Widget>[
          SliverOverlapAbsorber(
            handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
            sliver: SliverSafeArea(
              top: false,
              sliver: SliverAppBar(
                title: const Text('Tab Demo'),
                floating: true,
                pinned: true,
                snap: true,
                forceElevated: innerBoxIsScrolled,
                bottom: TabBar(
                  tabs: _tabs.map((String name) => Tab(text: name)).toList(),
                ),
              ),
            ),
          ),
        ],
        body: TabBarView(children: _tabViews),
      ),
    );
  }
}
```

<br>

</details>

<br>

This bug can be traced to a return statement inside `_NestedScrollPosition`:

```dart
double applyClampedDragUpdate(double delta) {
  // ...
  return delta + offset;
}
```

Thanks to some quirks of floating-point arithmetic, `applyClampedDragUpdate` would sometimes return a tiny non-zero value, which ends up ruining one of the scroll coordinator's equality checks.

8990ed6538/packages/flutter/lib/src/widgets/nested_scroll_view.dart (L658-L664)
2024-11-04 16:42:06 +00:00
..