Flutter的上滑面板插件中的ScrollController如何操作?

How to manipulate the ScrollController in Flutter's sliding up panel plugin?

我正在使用 Flutter 的 sliding_up_panel 插件。

我想在从我的应用程序抽屉中选择新项目时将面板滚动到顶部。目前选择一个新项目会关闭面板并刷新面板内容。然后将其打开到 200px 峰值,但不会将面板的滚动位置重置为顶部。

我一直在兜圈子,以略有不同的方式尝试相同的解决方案,但一无所获。

我尝试过的: 我有全球

  PanelController slidingPanelController = new PanelController();
  ScrollController slideUpPanelScrollController = new ScrollController();

我尝试将我的全局 slideUpPanelScrollController 附加到面板的列表视图,但是当向上滑动面板的 ListView 时,它同时开始关闭整个面板。如果您向上滚动以阅读您浏览过的内容,那么,您将无法阅读,因为它正在消失。

防止此错误很容易,您可以按照插件示例中的规范方式进行操作,从 SlidingPanel 传递 ScrollController,然后在面板的 Listview 中创建一个本地 ScrollController。

panelBuilder: (slideUpPanelScrollController) => _scrollingList(presentLocation, slideUpPanelScrollController)

问题是,您无法在新的应用程序抽屉选择上滚动面板,因为控制器现在是本地的。

我尝试在本地列表视图 ScrollController 上放置一个侦听器,_slideUpPanelScrollController 并测试 panelController.close():

if(slidingPanelController.isPanelClosed) _slideUpPanelScrollController.jumpTo(0);

但是侦听器阻止了面板滑动,触发了滑动事件但面板没有滑动,或者也非常不情愿。

在 ListView 中途显示内容的面板中打开新选择的内容是一种故障用户体验。我会喜欢一些想法或更好的解决方案。

我需要它,所以当面板关闭时,我可以 slideUpPanelScrollController.jumpTo(0);

我需要将全局控制器附加到面板 ListView 的本地控制器,或者我需要一种方法来访问本地控制器以从我的 _scrollingList() 函数中触发它的 Scroll。

这是面板小部件:

  SlidingUpPanel(
    key: Key("slidingUpPanelKey"),
    borderRadius: slidingPanelBorderRadius,
    parallaxEnabled: false,
    controller: slidingPanelController,
    isDraggable: isDraggableBool,
    onPanelOpened: () {
    },
    onPanelSlide: (value) {
      if (value >= 0.98)
        setState(() {
          slidingPanelBorderRadius =
              BorderRadius.vertical(top: Radius.circular(16));
        });
    },
    onPanelClosed: () async {
      setState(() {
        listViewScrollingOff = true;
      });
      imageZoomController.value =
          Matrix4.identity(); // so next Panel doesn't have zoomed in image

      slidingPanelBorderRadius =
          BorderRadius.vertical(top: Radius.circular(16));
    },
    minHeight: panelMinHeight,
    maxHeight:
        MediaQuery.of(context).size.height - AppBar().preferredSize.height,
    panelBuilder: (slideUpPanelScrollController) => _scrollingList(presentLocation, slideUpPanelScrollController),
    body: ...

这是 _scrollingList 小部件:

  Widget _scrollingList(LocationDetails presentLocation, ScrollController _slideUpPanelScrollController ) {
return Center(
    child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 600),
        child: ListView(
            controller: _slideUpPanelScrollController,
            physics: listViewScrollingOff
                ? const NeverScrollableScrollPhysics()
                : const AlwaysScrollableScrollPhysics(),
            key: Key("scrollingPanelListView"),
            children: [

这是我的 Drawer ListView 项目中的 onTap:

onTap: () {
  if(slidingPanelController.isPanelShown) {
  //slideUpPanelScrollController.jumpTo(0);
  slidingPanelController.close();
}

大爱帮忙!下面是我认为的一个最小可行问题。我是用 Dartpad 写的,但是从 Dartpad 分享是很重要的,所以我把它复制并粘贴在这里。 Dartpad 无论如何都不支持该插件,因此您无法在那里进行调整。

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

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  PanelController slidingPanelController = new PanelController();
  ScrollController slideUpPanelScrollController = new ScrollController();
  
  final String title = "sliding panel";
  
  String panelContent = "";
  String stupidText = "";
  String stupidText2 = ""
    
  int panelMinHeight = 0;
  int teaserPanelHeight = 77;
  
  bool listViewScrollingOff = false;
  
  initState() {
    super.initState();
    for(int i = 0; i < 500; i++) {
      stupidText += "More stupid text. ";
    }
    
     for(int i = 0; i < 500; i++) {
      stupidText2 += "More dumb, dumbest text. ";
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
              appBar: AppBar(title: Text(title)),
                drawer: Drawer(
  child: ListView(
    padding: EdgeInsets.zero,
    children: [
      const DrawerHeader(
        decoration: BoxDecoration(
          color: Colors.blue,
        ),
        child: Text('Drawer Header'),
      ),
      ListTile(
        title: const Text('Item 1'),
        onTap: () {
          if(slidingPanelController.isPanelShown) {
            print('attempting to scroll to top and close panel');
            //slideUpPanelScrollController.jumpTo(0);
            slidingPanelController.close();
          }
          Navigator.of(context).pop();
          setState() {
            panelContent = stupidText1;
            panelMinHeight = teaserPanelHeight;
          }
        },
      ),
      ListTile(
        title: const Text('Item 2'),
        onTap: () {
          if(slidingPanelController.isPanelShown) {
            //slideUpPanelScrollController.jumpTo(0);
            slidingPanelController.close();
          }
          Navigator.of(context).pop();
          setState() {
            panelContent = stupidText2;
            panelMinHeight = teaserPanelHeight;
          }
        }
      ),
    ],
  ),
),
        body: SlidingUpPanel(
        key: Key("slidingUpPanelKey"),
        borderRadius: 8,
        parallaxEnabled: false,
        controller: slidingPanelController,
        isDraggable: true,
        onPanelOpened: () async {
          setState(() {
            listViewScrollingOff = false;
            panelMinHeight = 0;
            animatedMarkerMap;
            //slideUpPanelScrollController.jumpTo(0);
          });
        },
        onPanelSlide: (value) {
          print("onPanelSlide: attempting to scroll panel");
        },
        onPanelClosed: () async {
          setState(() {
            //slideUpPanelScrollController.jumpTo(0);
            listViewScrollingOff = true;
          });
        },
        minHeight: panelMinHeight,
        maxHeight:
            MediaQuery.of(context).size.height - AppBar().preferredSize.height,
        // TODO BUG
        // SAM, IF I USE PANELBUILDER's ScrollController attached to the panel's ListView, then, when closing, the ListView will move to the top first, then the panel closes,
        // however ListView's controller is set to a globalController, this causes a bug when closing the panel, but means you can open/peek the panel from the App drawer,
        panelBuilder: (slideUpPanelScrollController) => _scrollingList(panelContent, slideUpPanelScrollController),
        
        body: Center(
          child: Text(
      'Hello, World!',
      style: Theme.of(context).textTheme.headline4,
    ),
        ),
          ),
      ),
    );
  }
  
    Widget _scrollingList(String panelContent, ScrollController _slideUpPanelScrollController ) {
    return Center(
        child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 600),
            child: ListView(
                controller: _slideUpPanelScrollController,
                physics: listViewScrollingOff
                    ? const NeverScrollableScrollPhysics()
                    : const AlwaysScrollableScrollPhysics(),
                key: Key("scrollingPanelListView"),
                children: [Text(panelContent)])));
    }
}

好的,所以问题就变成了当我关闭滑动面板时 'naturally',通过将面板向上滚动到顶部然后向下滑动面板,两件事同时发生了。

我找到了解决方法,我需要将 SlidingUpPanel 的 isDraggable 属性 设置为 false,直到用户滚动到面板顶部。

像这样...

      @override
      void initState() {
        super.initState();
    
        slideUpPanelScrollController.addListener(() {
          if(slideUpPanelScrollController.offset == 0) {
            setState(() {
              isDraggableBool = true;
            });
          }
        });
}

这种方法的缺点是侦听器是 运行 每当面板滚动时它的测试,它会卡住滚动吗?有 better/clearer/more 高效的方法吗?

为了完成,我将 setScrollBehaviour 修改为:

      void setScrollBehavior(bool _canScroll, {resetPos = false}) {
    setState(() {
      canScroll = _canScroll;
      isDraggableBool = !_canScroll;
      if (resetPos) {
        slideUpPanelScrollController.jumpTo(0);
        isDraggableBool = true;
      }
    });
  }

所以当用户可以滚动时他们不能拖动。 当面板关闭时,resetPos == true 因此面板会滚动到顶部并且可以再次拖动(滑动)它。

ListView might be the issue here. You could try to jump to the top before closing the panel instead of after. You could also try to provide a key to your ListView so it forces flutter to re-render. Eventually, if it doesn't work wrap the body of the slide panel inside a SingleChildScrollview and disable ListView scrolling using physics. It's a bit difficult to visualize a fix though. If you can provide a link to DartPad这样就容易多了

====更新====

您可以克隆面板构建器的滚动行为并在其他地方自由使用控制器。这样,您就不必担心列表视图滚动物理或面板状态。

进行了一些清理工作,添加了评论,并修复了 tap/scrolling 个问题。

此方法的唯一缺点是用户必须滚动回顶部才能关闭面板(您始终可以用手势包裹 body 并在需要时点击调用 resetPanel)

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final PanelController panelController = PanelController();
  ScrollController? _scrollController;

  /// reset the content position of your listview
  void resetScrollBehavior() {
    // We make sure that our scroll exist and it is attached before reset
    if (_scrollController != null && _scrollController!.hasClients) {
      _scrollController!.jumpTo(0);
    }
  }

  /// close the panel and reset the scroll behavior
  void resetPanel() {
    // We make sure our panel is attached and open before executing
    if (panelController.isAttached && panelController.isPanelOpen) {
      panelController.close();
      // Remove this line if you wish to not reset the scrollExtent on panel reset.
      resetScrollBehavior();
    }
  }

  void onDrawerItemTap() {
    // Reset the panel and pop the screen
    resetPanel();
    Navigator.of(context).pop();
  }

  @override
  void dispose() {
    // we make sure to dispose our controller(s)
    _scrollController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              child: Text('Drawer Header'),
            ),
            ListTile(
              title: const Text('Item 1'),
              onTap: () {
                onDrawerItemTap();
              },
            ),
            ListTile(
              title: const Text('Item 2'),
              onTap: () {
                onDrawerItemTap();
              },
            ),
          ],
        ),
      ),
      body: SlidingUpPanel(
        controller: panelController,
        borderRadius: BorderRadius.circular(8),
        minHeight: 80,
        maxHeight:
            MediaQuery.of(context).size.height - AppBar().preferredSize.height,
        panelBuilder: (ScrollController sc) {
          // ! So we can use local scrollcontroller outside the panel builder
          _scrollController ??= sc;
          return ScrollingList(
            scrollController: _scrollController!,
          );
        },
        body: Center(
          child: Text(
            'Hello, World!',
            style: Theme.of(context).textTheme.headline4,
          ),
        ),
      ),
    );
  }
}

class ScrollingList extends StatefulWidget {
  const ScrollingList({
    Key? key,
    required this.scrollController,
  }) : super(key: key);
  final ScrollController scrollController;

  @override
  _ScrollingListState createState() => _ScrollingListState();
}

class _ScrollingListState extends State<ScrollingList> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 600),
        child: ListView(
          controller: widget.scrollController,
          children: List.generate(200, (index) => Text('Text #$index')),
        ),
      ),
    );
  }
}

===旧===

运行 测试了一下,好像是panelController.close(); 也会触发 onPanelClosed(); 所以你可以利用它来处理你的滚动行为。

以下示例演示了如何通过点击抽屉项目来重置面板 and/or 列表视图。

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  PanelController panelController = PanelController();
  ScrollController scrollController = ScrollController();

  bool canScroll = false;

  void onDrawerItemTap() {
    if (panelController.isAttached && panelController.isPanelOpen) {
      panelController.close();
    }
    Navigator.of(context).pop();
  }

  void setScrollBehavior(bool _canScroll, {resetPos = false}) {
    setState(() {
      canScroll = _canScroll;
      if (resetPos) {
        scrollController.jumpTo(0);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              child: Text('Drawer Header'),
            ),
            ListTile(
              title: const Text('Item 1'),
              onTap: () {
                onDrawerItemTap();
              },
            ),
            ListTile(
              title: const Text('Item 2'),
              onTap: () {
                onDrawerItemTap();
              },
            ),
          ],
        ),
      ),
      body: SlidingUpPanel(
        controller: panelController,
        borderRadius: BorderRadius.circular(8),
        parallaxEnabled: false,
        isDraggable: true,
        onPanelOpened: () {
          setScrollBehavior(true);
        },
        onPanelSlide: (_) {
          // panel slide
        },
        onPanelClosed: () {
          setScrollBehavior(false, resetPos: true);
        },
        minHeight: 80,
        maxHeight:
            MediaQuery.of(context).size.height - AppBar().preferredSize.height,
        panelBuilder: (_) => ScrollingList(
          canScroll: canScroll,
          scrollController: scrollController,
        ),
        body: Center(
          child: Text(
            'Hello, World!',
            style: Theme.of(context).textTheme.headline4,
          ),
        ),
      ),
    );
  }
}

class ScrollingList extends StatefulWidget {
  const ScrollingList({
    Key? key,
    this.canScroll = false,
    required this.scrollController,
  }) : super(key: key);
  final bool canScroll;
  final ScrollController scrollController;

  @override
  _ScrollingListState createState() => _ScrollingListState();
}

class _ScrollingListState extends State<ScrollingList> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 600),
        child: ListView(
          controller: widget.scrollController,
          physics: !widget.canScroll
              ? const NeverScrollableScrollPhysics()
              : const AlwaysScrollableScrollPhysics(),
          children: List.generate(200, (index) => Text('Text #$index')),
        ),
      ),
    );
  }
}