2022-02-24

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

I'm using Flutter's sliding_up_panel plugin.

I want to scroll the panel to the top when a new item is selected from my app drawer. Presently selecting a new item closes the panel and refreshes the panel content. Then opens it to a 200px peak, but it doesn't reset the panel's Scroll location to the top.

I've been going around in circles trying the same solutions in slightly different ways and getting nowhere.

What I've tried: I have global

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

I tried attaching my global slideUpPanelScrollController to my panel's listview, but when swiping up the panel's ListView it simultaneously starts closing the whole panel. If you were scrolling up to read the content you'd skimmed, well, you're not able to because it's disappearing.

Preventing this bug is easy, you do it the canonical way from the plugin's examples, pass the ScrollController through from SlidingPanel and therefore create a local ScrollController in the panel's Listview.

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

The problem then is, you can't scroll the panel on new App drawer selections, because the controller is now local.

I tried putting a listener on the local listview ScrollController, _slideUpPanelScrollController and testing for panelController.close():

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

But the listener blocked the panel from swiping, swipe events fired but the panel didn't swipe, or was extremely reluctant too.

Having freshly selected content open in the panel displaying content halfway down the ListView is a glitchy user experience. I would love some ideas or better solutions.

I need it so when the panel is closed, I can slideUpPanelScrollController.jumpTo(0);

I need the global controller to attach to the panel ListView's local controller, or I need a way to access the local controller to fire its Scroll from outwith my _scrollingList() function.

Here's the panel Widget:

  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: ...

Here's the _scrollingList Widget:

  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: [

This is my onTap from my Drawer ListView item:

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

Love help! Below is I think a minimum viable problem. I wrote it in Dartpad, but sharing from Dartpad is nontrivial, so I've copied and pasted it here. Dartpad doesn't support the plugin anyway so it's not like you could tweak it there.

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)])));
    }
}

OK, so the problem became that when I closed the sliding panel 'naturally', by scrolling back up the panel to it top then sliding the panel down, well both things happened at once.

I've found how to solve this, I need to set SlidingUpPanel's isDraggable property to false, till the user has scrolled to the top of the panel.

Like so...

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

The shortfall of this approach is the listener is running its test whenever the panel is Scrolled, could it jank the scroll? Is there a better/clearer/more performant way?

For completion I amended setScrollBehaviour to this:

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

So when the user can scroll they can't drag. When the panel closes, resetPos == true therefore the panel scrolls to the top AND it can be dragged (slid) once more.



No comments:

Post a Comment