Repository: flutter
Issue: DraggableScrollableSheet snap animations conflict with multiple ScrollPositions
Labels: framework,f: scrolling,has reproducible steps,P2,found in release: 3.7,found in release: 3.8,team-framework,triaged-framework

<!-- Thank you for using Flutter!

     If you are looking for support, please check out our documentation
     or consider asking a question on Stack Overflow:
      * https://flutter.dev/
      * https://api.flutter.dev/
      * https://stackoverflow.com/questions/tagged/flutter?sort=frequent

     If you have found a bug or if our documentation doesn't have an answer
     to what you're looking for, then fill out the template below. Please read
     our guide to filing a bug first: https://flutter.dev/docs/resources/bug-reports
-->

<a href="https://dartpad.dev/?id=65bb94d872c5f8f9d5b47b07b5944317"><img src="https://user-images.githubusercontent.com/13222934/216722630-eb84e8d9-dd35-42b7-9bbb-a14bd13f6c49.png" width="512" alt="layout screenshot"></a>

## Steps to Reproduce

<!-- Please include full steps to reproduce so that we can reproduce the problem. -->

1. https://dartpad.dev/?id=65bb94d872c5f8f9d5b47b07b5944317
2. Select "Snap".
3. Scroll the bottom sheet using a touch interface.

**Expected results:** <!-- what did you want to see? -->

Bottom sheet snaps to expanded or collapsed position.

For reference, here's the expected snap behavior (with the scroll controller only hooked up to the outer scrollable covering the tab bar):
https://dartpad.dev/?id=7cdb15f7b00dc440053e5c54112cbc45

**Actual results:** <!-- what did you see? -->

Bottom sheet oscillates between animated and initial snapped positions before possibly eventually settling.

This seems to be due to the [IdleScrollActivity](https://api.flutter.dev/flutter/widgets/IdleScrollActivity-class.html) on the idle scroll positions calling `goBallistic(0)` in response to the resizing sheet. (It may be worth noting that the inner scroll position may indeed need to adjust position, for example in https://dartpad.dev/?id=7cdb15f7b00dc440053e5c54112cbc45 if the first tab is scrolled before fully expanding.)

Ostensibly this might be something to solve using a [NestedScrollView](https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html), but then we run into incompatible `ScrollPosition` types as the `DraggableScrollableSheet`'s scroll controller only supports its own scroll positions while `NestedScrollView` attempts to attach its own subclass into the outer scroll controller:

https://dartpad.dev/?id=b7650fc7f0590af8a2885b0ea8817469
(may need to open web debug for the errors to show up)

```
The following TypeErrorImpl was thrown during a scheduler callback:
Expected a value of type '_DraggableScrollableSheetScrollPosition', but got one of type
'_NestedScrollPosition'
When the exception was thrown, this was the stack:
...
packages/flutter/src/widgets/draggable_scrollable_sheet.dart 747:58                                                            <fn>
```

Related issue re: `NestedScrollView`: #64157

<details>
<summary>Code sample</summary>

<!--
      Please create a minimal reproducible sample that shows the problem
      and attach it below between the lines with the backticks.

      To create it you can use `flutter create bug` command and update the `main.dart` file.

      Alternatively, you can use https://dartpad.dev/
      which is capable of creating and running small Flutter apps.

      Without this we will unlikely be able to progress on the issue, and because of that
      we regretfully will have to close it.
-->

```dart
import 'dart:math';

import 'package:flutter/material.dart';

void main() => runApp(const ScrollDemo());

class ScrollDemo extends StatefulWidget {
  const ScrollDemo({super.key});
  @override
  State<ScrollDemo> createState() => ScrollDemoState();
}

class ScrollDemoState extends State<ScrollDemo> {
  bool snap = false;

  @override
  Widget build(BuildContext context) => MaterialApp(
        theme: ThemeData(
          tabBarTheme: const TabBarTheme(labelColor: Colors.black),
        ),
        home: Scaffold(
          body: LayoutBuilder(
            builder: (context, constraints) {
              final foldPosition = min(
                1.0,
                (Sheet.tabBarHeight + FoldTab.pageHeight) /
                    constraints.maxHeight,
              );

              return Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Expanded(
                    child: Stack(
                      alignment: Alignment.center,
                      children: [
                        InteractiveViewer(
                          constrained: false,
                          child: const GridPaper(
                            color: Colors.red,
                            child: SizedBox(
                              width: 2000,
                              height: 2000,
                              child: Text('Maps'),
                            ),
                          ),
                        ),
                        const Positioned(
                          left: 0,
                          bottom: 0,
                          child: Text('Watermark'),
                        ),
                        ElevatedButton(
                          onPressed: () => setState(() => snap = !snap),
                          child: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Checkbox(
                                value: snap,
                                onChanged: (value) =>
                                    setState(() => snap = value!),
                              ),
                              const Text('snap'),
                              const SizedBox(width: 8),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                  ConstrainedBox(
                    constraints: constraints,
                    child: DraggableScrollableSheet(
                      expand: false,
                      snap: snap,
                      initialChildSize: foldPosition,
                      snapSizes: [foldPosition],
                      minChildSize: Sheet.tabBarHeight / constraints.maxHeight,
                      builder: (context, scrollController) => CustomScrollView(
                        controller: scrollController,
                        slivers: [
                          SliverFillRemaining(
                            child: Sheet(
                              scrollController: scrollController,
                            ),
                          )
                        ],
                      ),
                    ),
                  )
                ],
              );
            },
          ),
        ),
      );
}

/// A scroll controller subordinate to a parent controller, which
/// [createScrollPosition]s via the parent and attaches/detaches its positions
/// from the parent. This is useful for creating nested scroll controllers
/// for widgets with scrollbars that can actuate a parent scroll controller.
class SubordinateScrollController extends ScrollController {
  SubordinateScrollController(
    this.parent, {
    String subordinateDebugLabel = 'subordinate',
  }) : super(
          debugLabel: parent.debugLabel == null
              ? null
              : '${parent.debugLabel}/$subordinateDebugLabel',
          initialScrollOffset: parent.initialScrollOffset,
          keepScrollOffset: parent.keepScrollOffset,
        );
  final ScrollController parent;
  // Although some use cases might seem to be simplified if parent were made
  // settable, we can't really do this because scroll positions are owned by
  // Scrollables rather than the scroll controller, so the scroll view is
  // responsible for transferring positions from one controller to another. If
  // we were to try to do the transfer here, we would end up trying to dispose
  // of positions that Scrollables may still be holding on to.

  @override
  ScrollPosition createScrollPosition(
    ScrollPhysics physics,
    ScrollContext context,
    ScrollPosition? oldPosition,
  ) =>
      parent.createScrollPosition(physics, context, oldPosition);

  @override
  void attach(ScrollPosition position) {
    super.attach(position);
    parent.attach(position);
  }

  @override
  void detach(ScrollPosition position) {
    parent.detach(position);
    super.detach(position);
  }

  @override
  void dispose() {
    for (final position in positions) {
      parent.detach(position);
    }

    super.dispose();
  }
}

class Sheet extends StatefulWidget {
  static const tabBarHeight = 18.0;

  const Sheet({super.key, this.scrollController});
  final ScrollController? scrollController;

  @override
  State<Sheet> createState() => SheetState();
}

class SheetState extends State<Sheet> with SingleTickerProviderStateMixin {
  late final tabController = TabController(length: 3, vsync: this);

  List<ScrollController>? _scrollControllers;

  void _updateScrollControllers() {
    _scrollControllers = widget.scrollController == null
        ? null
        : [
            for (int i = 0; i < 3; ++i)
              SubordinateScrollController(widget.scrollController!)
          ];
  }

  void _disposeScrollControllers() {
    if (_scrollControllers != null) {
      for (final scrollController in _scrollControllers!) {
        scrollController.dispose();
      }
    }
  }

  @override
  void initState() {
    super.initState();
    _updateScrollControllers();
  }

  @override
  void didUpdateWidget(covariant Sheet oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.scrollController != oldWidget.scrollController) {
      _disposeScrollControllers();
      _updateScrollControllers();
    }
  }

  @override
  void dispose() {
    _disposeScrollControllers();
    tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Column(
        children: [
          TabBar(
            controller: tabController,
            tabs: const [
              Text('Above/below the fold'),
              Text('Scrollable content'),
              Text('With a list'),
            ],
          ),
          Expanded(
            child: TabBarView(
              controller: tabController,
              children: [
                FoldTab(scrollController: _scrollControllers?[0]),
                ContentTab(scrollController: _scrollControllers?[1]),
                ListTab(scrollController: _scrollControllers?[2]),
              ],
            ),
          ),
        ],
      );
}

class FoldTab extends StatefulWidget {
  static const pageHeight = 256.0;

  const FoldTab({super.key, this.scrollController});
  final ScrollController? scrollController;

  @override
  State<FoldTab> createState() => FoldTabState();
}

class FoldTabState extends State<FoldTab> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return SingleChildScrollView(
      controller: widget.scrollController,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Container(
            color: Colors.green,
            height: FoldTab.pageHeight,
            child: const Text('Above the fold'),
          ),
          Container(
            color: Colors.blue,
            height: FoldTab.pageHeight,
            child: const Text('Below the fold'),
          ),
        ],
      ),
    );
  }
}

class ContentTab extends StatefulWidget {
  const ContentTab({super.key, this.scrollController});
  final ScrollController? scrollController;

  @override
  State<ContentTab> createState() => ContentTabState();
}

class ContentTabState extends State<ContentTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);

    return SingleChildScrollView(
      controller: widget.scrollController,
      child: Text([for (int i = 0; i < 1000; ++i) 'content'].join()),
    );
  }
}

class ListTab extends StatefulWidget {
  const ListTab({super.key, this.scrollController});
  final ScrollController? scrollController;

  @override
  State<ListTab> createState() => ListTabState();
}

class ListTabState extends State<ListTab> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);

    return Scaffold(
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(128.0),
        child: Container(
          alignment: Alignment.center,
          color: Colors.grey,
          child: const Text('Header content'),
        ),
      ),
      body: ListView.builder(
        controller: widget.scrollController,
        itemCount: 50,
        itemExtent: 48.0,
        itemBuilder: (context, index) => Text('Item $index'),
      ),
    );
  }
}
```

</details>

<details>
  <summary>Logs</summary>

<!--
     Run `flutter analyze` and attach any output of that command below.
     If there are any analysis errors, try resolving them before filing this issue.
-->

```
PS C:\Users\imagi\Documents\projects\trip_planner_aquamarine> flutter analyze
Analyzing trip_planner_aquamarine...                                    
No issues found! (ran in 3.6s)
```

<!-- Finally, paste the output of running `flutter doctor -v` here. -->

```
PS C:\Users\imagi\Documents\projects\trip_planner_aquamarine> flutter doctor -v
[!] Flutter (Channel rosswang, 3.7.0-2.0.pre.15, on Microsoft Windows [Version 10.0.22621.819], locale en-US)
    • Flutter version 3.7.0-2.0.pre.15 on channel rosswang at C:\Users\imagi\flutter
    ! Upstream repository git@github.com:AsturaPhoenix/flutter.git is not a standard remote.
      Set environment variable "FLUTTER_GIT_URL" to git@github.com:AsturaPhoenix/flutter.git to dismiss this error.
    • Framework revision b189260d41 (4 weeks ago), 2023-01-03 17:20:39 -0800
    • Engine revision 025aefc7af
    • Dart version 2.19.0 (build 2.19.0-444.0.dev)
    • DevTools version 2.20.0
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.

[√] Windows Version (Installed version of Windows is version 10 or higher)

[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    • Android SDK at C:\Users\imagi\AppData\Local\Android\sdk
    • Platform android-33, build-tools 33.0.0
    • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[!] Visual Studio - develop for Windows (Visual Studio Community 2019 16.11.16)
    • Visual Studio at C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
    • Visual Studio Community 2019 version 16.11.32602.291
    • Windows 10 SDK version 10.0.16299.0
    X Visual Studio is missing necessary components. Please re-run the Visual Studio installer for the "Desktop development with C++" workload, and include these components:      
        MSVC v142 - VS 2019 C++ x64/x86 build tools
         - If there are multiple build tool versions available, install the latest
        C++ CMake tools for Windows
        Windows 10 SDK

[√] Android Studio (version 2021.3)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[√] VS Code (version 1.74.3)
    • VS Code at C:\Users\imagi\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.57.20221221

[√] Connected device (3 available)
    • Windows (desktop) • windows • windows-x64    • Microsoft Windows [Version 10.0.22621.819]
    • Chrome (web)      • chrome  • web-javascript • Google Chrome 109.0.5414.76
    • Edge (web)        • edge    • web-javascript • Microsoft Edge 109.0.1518.61

[√] HTTP Host Availability
    • All required HTTP hosts are available

! Doctor found issues in 2 categories.
```

</details>


<!--
      Consider also attaching screenshots and/or videos to better
      illustrate the issue.

      You can upload them directly on GitHub.
      Beware that video file size is limited to 10MB.
-->