flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
165.04k stars 27.2k forks source link

Multiple Nested Scroll View. #107022

Closed 623637646 closed 2 years ago

623637646 commented 2 years ago

Use case

Supporting multiple nested scroll views on a page.

Currently, we can add only one scroll view in NestedScrollView's body. If we add two scroll views into the body of NestedScrollView. It will look like this (I am using an Android simulator).

https://user-images.githubusercontent.com/5275802/177024064-bbbd0353-c9dd-4ff6-8788-447579013a4c.mov

Code sample ```dart import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); static const String _title = 'Flutter Code Sample'; @override Widget build(BuildContext context) { return const MaterialApp( title: _title, home: MyStatelessWidget(), ); } } class MyStatelessWidget extends StatelessWidget { const MyStatelessWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ const SliverToBoxAdapter( child: ColorfulContainer( child: SizedBox( height: 200, ), ), ) ]; }, body: Column( children: [ SizedBox( height: 300, child: ColorfulScrollView(key: UniqueKey()), ), SizedBox( height: 300, child: ColorfulScrollView(key: UniqueKey()), ), ], ), ), ); } } extension ColorUtils on Color { static Color random() { return Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0); } Color invert() { final r = 255 - red; final g = 255 - green; final b = 255 - blue; return Color.fromARGB((opacity * 255).round(), r, g, b); } } class ColorfulContainer extends StatelessWidget { final Widget? child; const ColorfulContainer({this.child}); @override Widget build(BuildContext context) { final color = ColorUtils.random(); return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ color, color.invert(), ], ), ), child: child, ); } } class ColorfulScrollView extends StatelessWidget { const ColorfulScrollView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return SingleChildScrollView( physics: const ClampingScrollPhysics(), child: ColorfulContainer( child: Column( children: List.generate( 20, (index) => ListTile( title: Text(index.toString()), ), ), ), ), ); } } ```

Proposal

In this case, we have two options to implement this feature.

  1. Adding two or more scroll views into NestedScrollView's body. For my understanding, this is not possible due to the PrimaryScrollController of the current Flutter structure. All scroll views in the body will use the same PrimaryScrollController.

  2. Add NestedScrollView to NestedScrollView's body. This is the possible solution. But currently, it seems doesn't support it. Below is the example code and the error I encountered.

Code sample ```dart import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); static const String _title = 'Flutter Code Sample'; @override Widget build(BuildContext context) { return const MaterialApp( title: _title, home: MyStatelessWidget(), ); } } class MyStatelessWidget extends StatelessWidget { const MyStatelessWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( key: const ValueKey(1), headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ const SliverToBoxAdapter( child: ColorfulContainer( child: SizedBox( height: 200, ), ), ) ]; }, body: Builder( builder: (context) { return NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ const SliverToBoxAdapter( child: ColorfulContainer( child: SizedBox( height: 200, ), ), ) ]; }, body: ColorfulScrollView(key: UniqueKey()), ); }, ), ), ); } } extension ColorUtils on Color { static Color random() { return Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0); } Color invert() { final r = 255 - red; final g = 255 - green; final b = 255 - blue; return Color.fromARGB((opacity * 255).round(), r, g, b); } } class ColorfulContainer extends StatelessWidget { final Widget? child; const ColorfulContainer({this.child}); @override Widget build(BuildContext context) { final color = ColorUtils.random(); return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ color, color.invert(), ], ), ), child: child, ); } } class ColorfulScrollView extends StatelessWidget { const ColorfulScrollView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return SingleChildScrollView( physics: const ClampingScrollPhysics(), child: ColorfulContainer( child: Column( children: List.generate( 20, (index) => ListTile( title: Text(index.toString()), ), ), ), ), ); } } ```
Error ``` Restarted application in 462ms. ════════ Exception caught by widgets library ═══════════════════════════════════ The following StackOverflowError was thrown building PrimaryScrollController(no controller): Stack Overflow The relevant error-causing widget was SingleChildScrollView When the exception was thrown, this was the stack #0 Object._simpleInstanceOfTrue (dart:core-patch/object_patch.dart:64:3) #1 _NestedScrollController.detach #2 _NestedScrollPosition.setParent #3 _NestedScrollController.detach #4 _NestedScrollPosition.setParent #5 _NestedScrollController.detach #6 _NestedScrollPosition.setParent #7 _NestedScrollController.detach #8 _NestedScrollPosition.setParent #9 _NestedScrollController.detach #10 _NestedScrollPosition.setParent #11 _NestedScrollController.detach #12 _NestedScrollPosition.setParent #13 _NestedScrollController.detach #14 _NestedScrollPosition.setParent #15 _NestedScrollController.detach #16 _NestedScrollPosition.setParent #17 _NestedScrollController.detach #18 _NestedScrollPosition.setParent #19 _NestedScrollController.detach #20 _NestedScrollPosition.setParent #21 _NestedScrollController.detach #22 _NestedScrollPosition.setParent #23 _NestedScrollController.detach #24 _NestedScrollPosition.setParent #25 _NestedScrollController.detach #26 _NestedScrollPosition.setParent #27 _NestedScrollController.detach #28 _NestedScrollPosition.setParent #29 _NestedScrollController.detach #30 _NestedScrollPosition.setParent #31 _NestedScrollController.detach #32 _NestedScrollPosition.setParent #33 _NestedScrollController.detach #34 _NestedScrollPosition.setParent #35 _NestedScrollController.detach #36 _NestedScrollPosition.setParent #37 _NestedScrollController.detach #38 _NestedScrollPosition.setParent #39 _NestedScrollController.detach #40 _NestedScrollPosition.setParent #41 _NestedScrollController.detach #42 _NestedScrollPosition.setParent #43 _NestedScrollController.detach ... ... #12026 ComponentElement.mount ... Normal element mounting (21 frames) #12047 Element.inflateWidget #12048 Element.updateChild #12049 RenderObjectToWidgetElement._rebuild #12050 RenderObjectToWidgetElement.mount #12051 RenderObjectToWidgetAdapter.attachToRenderTree. #12052 BuildOwner.buildScope #12053 RenderObjectToWidgetAdapter.attachToRenderTree #12054 WidgetsBinding.attachRootWidget #12055 WidgetsBinding.scheduleAttachRootWidget. (elided 11 frames from class _RawReceivePortImpl, class _Timer, dart:async, and dart:async-patch) ════════════════════════════════════════════════════════════════════════════════ ```
flutter doctor -v ``` [✓] Flutter (Channel stable, 3.0.4, on macOS 12.4 21F79 darwin-arm, locale en-SG) • Flutter version 3.0.4 at /Users/wangyy/.flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 85684f9300 (2 days ago), 2022-06-30 13:22:47 -0700 • Engine revision 6ba2af10bb • Dart version 2.17.5 • DevTools version 2.12.2 [✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1) • Android SDK at /Users/wangyy/Library/Android/sdk • Platform android-32, build-tools 32.1.0-rc1 • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.11+0-b60-7772763) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 13.2.1) • Xcode at /Applications/Xcode.app/Contents/Developer • CocoaPods version 1.11.3 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2021.1) • Android Studio at /Applications/Android Studio.app/Contents • 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.11+0-b60-7772763) [✓] VS Code (version 1.68.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.34.0 [✓] Connected device (4 available) • sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 12 (API 32) (emulator) • Yanni’s iPhone 8 plus (mobile) • 3be87361c4457fbc9a39b5fe73a980755cd94568 • ios • iOS 14.4 18D52 • macOS (desktop) • macos • darwin-arm64 • macOS 12.4 21F79 darwin-arm • Chrome (web) • chrome • web-javascript • Google Chrome 103.0.5060.53 [✓] HTTP Host Availability • All required HTTP hosts are available • No issues found! ```
exaby73 commented 2 years ago

Hello @623637646. What is the expected behavior here? That each scroll view scolls on its own? Because that can be achieved by giving the inner scroll views a controller. See modified code sample

Code sample ```dart import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); static const String _title = 'Flutter Code Sample'; @override Widget build(BuildContext context) { return const MaterialApp( title: _title, home: MyStatelessWidget(), ); } } class MyStatelessWidget extends StatelessWidget { const MyStatelessWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ const SliverToBoxAdapter( child: ColorfulContainer( child: SizedBox( height: 200, ), ), ) ]; }, body: Column( children: [ SizedBox( height: 300, child: ColorfulScrollView(key: UniqueKey()), ), SizedBox( height: 300, child: ColorfulScrollView(key: UniqueKey()), ), ], ), ), ); } } extension ColorUtils on Color { static Color random() { return Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0); } Color invert() { final r = 255 - red; final g = 255 - green; final b = 255 - blue; return Color.fromARGB((opacity * 255).round(), r, g, b); } } class ColorfulContainer extends StatelessWidget { final Widget? child; const ColorfulContainer({this.child}); @override Widget build(BuildContext context) { final color = ColorUtils.random(); return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ color, color.invert(), ], ), ), child: child, ); } } class ColorfulScrollView extends StatefulWidget { const ColorfulScrollView({Key? key}) : super(key: key); @override State createState() => _ColorfulScrollViewState(); } class _ColorfulScrollViewState extends State { final controller = ScrollController(); @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SingleChildScrollView( controller: controller, physics: const ClampingScrollPhysics(), child: ColorfulContainer( child: Column( children: List.generate( 20, (index) => ListTile( title: Text(index.toString()), ), ), ), ), ); } } ```
623637646 commented 2 years ago

Thanks, @exaby73 , I tried your code. It's not my expected behavior. What I want is that let users feel like there is only one scroll view. For example in my video. One NestedScrollView wraps two ScrollView.

  1. When users scroll down in the beginning, The NestedScrollView will scroll first.
  2. When the first ScrollView's top reached the top of the screen, then the NestedScrollView stops scrolling and the first ScrollView starts to scroll.
  3. When the first ScrollView's bottom reached the top of the screen, then the first ScrollView stops scrolling and the second ScrollView starts to scroll.
  4. When the second ScrollView's bottom reached the top of the screen. then the second ScrollView stops scrolling and the NestedScrollView starts to scroll.
exaby73 commented 2 years ago

Maybe what you need is to implement this with CustomScrollView. A similar issue (https://github.com/flutter/flutter/issues/91348) here could maybe help you.

623637646 commented 2 years ago

Thanks, @exaby73 , I know how to use SliverList in CustomScrollView. But what should I do if I can't use SliverList in some cases?

For example, I need to add two SingleChildScrollView which are from third part libraries into NestedScrollView. In this case, there is no SliverList, so I can't use CustomScrollView.

exaby73 commented 2 years ago

Not sure how you would approach this. I'll keep this issue open for insights from the team.

goderbauer commented 2 years ago

@Piinks

Piinks commented 2 years ago

For example, I need to add two SingleChildScrollView which are from third part libraries into NestedScrollView. In this case, there is no SliverList, so I can't use CustomScrollView.

This is likely not something that would be very performant, since SingleChildScrollView does not offer an optimized lazy building of children.

I do not think we are going to support this. The NestedScrollView is a very complex widget, and intended to serve a very specific use case (TabBar view with internal scrolling lists).

It sounds like what you are looking for is composing multiple slivers within a custom scroll view. You can put a CustomScrollView inside of the body of a NestedScrollView if you wrap the CSV in a builder. Then you can add as many slivers as you like. There are examples of this in https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.