过渡到另一条路线时如何使ListView保留其滚动?

问题描述 投票:-1回答:1

我想在我的flutter应用程序中完成从幻灯片到正确过渡的操作。问题是路由转换有点创建了我要从其转换的页面的新实例,因此ListView滚动重置。

See a video

这是包含ListView的页面代码:

import 'package:app/components/SingleTouchRecognizer.dart';
import 'package:app/components/albumArt.dart';
import 'package:app/components/bottomTrackPanel.dart';
import 'package:app/components/search.dart';
import 'package:app/player/permissions.dart';
import 'package:app/player/playerWidgets.dart';
import 'package:app/player/playlist.dart';
import 'package:app/player/song.dart';
import 'package:app/routes/playerRoute.dart';
import 'package:app/routes/settingsRoute.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:app/player/player.dart';
import 'scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:app/components/refresh_indicator.dart';


/// List of fetched tracks
class TrackList extends StatefulWidget {
  final EdgeInsets bottomPadding;
  TrackList({Key key, this.bottomPadding: const EdgeInsets.only(bottom: 0.0)})
      : super(key: key);

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

class _TrackListState extends State<TrackList> {
  // As you can see here's a page storage key, that works normally with
  // page transitions, that don't move exit route
  static final PageStorageKey _pageScrollKey = PageStorageKey('MainListView');

 // Other methods...

  Future<void> _handleClickSettings() async {
    Navigator.pop(context);
    await Future.delayed(Duration(
        milliseconds: 246 + 20)); // Wait before pop sidebar closes plus delay
    Navigator.of(context).push(createSettingsRoute(widget));
  }

  @override
  Widget build(BuildContext context) {
    if (Permissions.permissionStorageStatus != MyPermissionStatus.granted)
      // Code that displays to user button to re-request permissions
    if (PlaylistControl.songsEmpty(PlaylistType.global))
      // Code that displays to user a message that there're not songs on his device
    return Scaffold(
      drawer: Theme(
        data: Theme.of(context).copyWith(
          canvasColor:
              Color(0xff070707), //This will change the drawer background
        ),
        child: Drawer(
          child: ListView(
            physics: NeverScrollableScrollPhysics(),
            // Important: Remove any padding from the ListView.
            padding: EdgeInsets.zero,
            children: <Widget>[
              Container(
                // height: 100.0,
                padding:
                    const EdgeInsets.only(left: 15.0, top: 40.0, bottom: 20.0),
                child: Text('Меню', style: TextStyle(fontSize: 35.0)),
              ),
              ListTile(
                  title: Text('Настройки',
                      style: TextStyle(
                          fontSize: 17.0, color: Colors.deepPurple.shade300)),
                  onTap: _handleClickSettings),
            ],
          ),
        ),
      ),
      appBar: AppBar(
        // automaticallyImplyLeading: false,
        // leading: IconButton(
        //     icon: Icon(Icons.menu),
        //   ),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.sort),
            onPressed: () {
              _showSortModal();
            },
          ),
        ],
        titleSpacing: 0.0,
        title: Padding(
          padding: const EdgeInsets.only(left: 0.0),
          child: ClipRRect(
            // FIXME: cliprrect doesn't work for material for some reason
            borderRadius: BorderRadius.circular(10),
            child: GestureDetector(
              onTap: _showSearch,
              child: FractionallySizedBox(
                // heightFactor: 1,
                widthFactor: 1,
                child: Container(
                  padding: const EdgeInsets.only(
                      left: 12.0, top: 10.0, bottom: 10.0),
                  decoration: BoxDecoration(
                    color: Colors.white.withOpacity(0.05),
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text(
                        'Поиск треков на устройстве',
                        style: TextStyle(
                            color: Theme.of(context).hintColor, fontSize: 17),
                      )
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
      body: Stack(
        children: <Widget>[
          Padding(
            padding: widget.bottomPadding,
            child: Container(
              child: CustomRefreshIndicator(
                color: Colors.white,
                strokeWidth: 2.5,
                key: _refreshIndicatorKey,
                onRefresh: _refreshHandler,
                child: SingleTouchRecognizerWidget(
                  child: Container(
                    child: ListView.builder(
                      key: _pageScrollKey, // Here's key!
                      itemCount: PlaylistControl.globalPlaylist.length,
                      padding: EdgeInsets.only(bottom: 10, top: 5),
                      itemBuilder: (context, index) {
                        return StreamBuilder(
                            stream: PlaylistControl.onSongChange,
                            builder: (context, snapshot) {
                              return TrackTile(
                                index,
                                key: UniqueKey(),
                                playing: index ==
                                    PlaylistControl.currentSongIndex(
                                        PlaylistType.global),
                                additionalClickCallback: () {
                                  PlaylistControl.resetPlaylists();
                                },
                              );
                            });
                      },
                    ),
                  ),
                ),
              ),
            ),
          ),
          BottomTrackPanel(),
        ],
      ),
    );
  }
}

这就是我创建新路线的方式

/// @oldRoute needed cause this route transition utilizes `SlideStackRightRoute`
Route createSettingsRoute(Widget oldRoute) {
  return SlideStackRightRoute(exitPage: oldRoute, enterPage: SettingsRoute());
}

最后滑到正确的过渡类本身

import 'package:flutter/material.dart';

/// Creates cupertino-like route transition, where new route pushes old from right to left
class SlideStackRightRoute extends PageRouteBuilder {
  final Widget enterPage;
  final Widget exitPage;
  static var exBegin = Offset(0.0, 0.0);
  static var exEnd = Offset(-0.5, 0.0);
  static var entBegin = Offset(1.0, 0.0);
  static var entEnd = Offset.zero;
  static var curveIn = Curves.easeOutSine;
  static var curveOut = Curves.easeInSine;

  SlideStackRightRoute({@required this.exitPage, @required this.enterPage})
      : super(
          transitionDuration: Duration(milliseconds: 400),
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              enterPage,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              Stack(
            children: <Widget>[
              SlideTransition(
                position: Tween(begin: exBegin, end: exEnd)
                    .chain(CurveTween(curve: curveIn))
                    .chain(CurveTween(curve: curveOut))
                    .animate(animation),
                child: Container(
                    foregroundDecoration: BoxDecoration(
                      color: Colors.black.withOpacity(animation.value / 2),
                    ),
                    child: exitPage),
              ),
              SlideTransition(
                position: Tween(begin: entBegin, end: entEnd)
                    .chain(CurveTween(curve: curveIn))
                    .chain(CurveTween(curve: curveOut))
                    .animate(animation),
                child: enterPage,
              )
            ],
          ),
        );
}

listview flutter scroll routes transition
1个回答
0
投票

原始答案:

按照Marc的建议,我在ScrollController上添加了ListView.builder,并将提取出创建曲目列表的代码提取到单独的方法中,以便能够创建其假副本。

但是,这种解决方案在创建新的ListView实例时可能会导致一些性能问题

此处已删除代码


UPD:

正如我最初所预期的,随着列表长度的增加,仅使用列表将带来巨大的性能。

要解决此问题,您必须忘记列表并使用flutter的库本身尚不可用的ScrollablePositinedList,但它存在于google's flutter widgets repository中。这个小部件可让您跳到列表中的元素而不会出现性能问题(实际上,如果您查看源代码,则根本不使用其中的ListView)。恕我直言,这是一个完美的解决方案,也是暂时跳过列表的最佳解决方案,我希望flutter团队将来能够将此小部件添加到他们的库中。

因此您必须将其复制/安装到项目中,然后执行后续步骤:

  1. 公开frontScrollController状态的ScrollablePositinedList属性,还有backScrollController,但是如果我正确理解,front是此小部件中的主滚动控制器,因为对我来说back的偏移量始终等于0。
  2. 接下来检查列表的单个元素占用了多少空间
  3. 创建用于处理新路线的功能
bool didTapDrawerTile = false;

 Future<void> _handleClickSettings() async {
    if (!didTapDrawerTile) {
      setState(() {
// Make sure that user won't be able to click drawer twice
        didTapDrawerTile = true;
      });
      Navigator.pop(context);
      await Future.delayed(Duration(
          milliseconds: 246)); // Default drawer close time
      await 
      Navigator.of(context).push(createSettingsRoute(_buildTracks(true)));
      setState(() {
        didTapDrawerTile = false;
      });
    }
  }
  1. 创建函数,用于构建页面的内容
static final GlobalKey trackListGlobalKey = GlobalKey();

Widget _buildTracks([bool isFake = false]) {
     var indexOffset;
     var additionalScrollOffset ;
     if (isFake) {
// Stop possible scrolling
     listScrollController.jumpTo(listScrollController.offset);

// Calc init offsets

// Index offset to fake list (jumps to index in list)
// In my case tile is dense, so its height is 64
      indexOffset = listScrollController.offset ~/ 64;

// Additional offset to list (specified `initialScrollIndex`, the `frontScrollController` offset anyways will be zero, so we just add additional offset in range of 0 to <yourTileHeight> - 1)
      additionalScrollOffset = listScrollController.offset % 64;
    }
    return IgnorePointer(

// Just to be sure that our widgets won't dispose after transition add global key
      key: isFake ? null : trackListGlobalKey,

// Disable entire fake touch events
      ignoring: didTapDrawerTile,

      child: Scaffold(
        drawer: Theme(
          data: Theme.of(context).copyWith(
            canvasColor:
                Color(0xff070707), //This will change the drawer background
          ),
          child: Drawer(
            child: ListView(
              physics: NeverScrollableScrollPhysics(),
              // Important: Remove any padding from the ListView.
              padding: EdgeInsets.zero,
              children: <Widget>[
                Container(
                  // height: 100.0,
                  padding: const EdgeInsets.only(
                      left: 15.0, top: 40.0, bottom: 20.0),
                  child: Text('Меню', style: TextStyle(fontSize: 35.0)),
                ),
                ListTile(
                    title: Text('Настройки',
                        style: TextStyle(
                            fontSize: 17.0, color: Colors.deepPurple.shade300)),

                  // Function that opens new route
                    onTap: _handleClickSettings

                ),
              ],
            ),
          ),
        ),
        appBar: AppBar(
          // automaticallyImplyLeading: false,
          leading: DrawerButton(),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.sort),
              onPressed: () {
                _showSortModal();
              },
            ),
          ],
          titleSpacing: 0.0,
          title: Padding(
            padding: const EdgeInsets.only(left: 0.0),
            child: ClipRRect(
              borderRadius: BorderRadius.circular(10),
              child: GestureDetector(
                onTap: _showSearch,
                child: FractionallySizedBox(
                  // heightFactor: 1,
                  widthFactor: 1,
                  child: Container(
                    padding: const EdgeInsets.only(
                        left: 12.0, top: 10.0, bottom: 10.0),
                    decoration: BoxDecoration(
                      color: Colors.white.withOpacity(0.05),
                    ),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text(
                          'Поиск треков на устройстве',
                          style: TextStyle(
                              color: Theme.of(context).hintColor, fontSize: 17),
                        )
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
        body: Stack(
          children: <Widget>[
            Padding(
              padding: widget.bottomPadding,
              child: Container(
                child: CustomRefreshIndicator(
                  color: Colors.white,
                  strokeWidth: 2.5,
                  key: isFake ? null : _refreshIndicatorKey,
                  onRefresh: _refreshHandler,
                  child: SingleTouchRecognizerWidget(
                    child: Container(
                      child: ScrollablePositionedList.builder(

// Pass index offset
                        initialScrollIndex: isFake ? indexOffset : 0,

// Pass additional offset
                        frontScrollController: isFake
                            ? ScrollController(
                                initialScrollOffset: additionalScrollOffset )
                            : listScrollController,
                        itemCount: PlaylistControl.globalPlaylist.length,
                        padding: EdgeInsets.only(bottom: 10, top: 0),
                        itemBuilder: (context, index) {
                          return StreamBuilder(
                              stream: PlaylistControl.onSongChange,
                              builder: (context, snapshot) {
                                return TrackTile(
                                  index,
                                  key: UniqueKey(),
                                  playing: index ==
                                      PlaylistControl.currentSongIndex(
                                          PlaylistType.global),
                                  additionalClickCallback: () {
                                    PlaylistControl.resetPlaylists();
                                  },
                                );
                              });
                        },
                      ),
                    ),
                  ),
                ),
              ),
            ),
            BottomTrackPanel(),
          ],
        ),
      ),
    );
  }

我也更改了过渡小部件本身

     import 'package:flutter/material.dart';

/// Creates cupertino-like route transition, where new route pushes old from right to left
     class SlideStackRightRoute extends PageRouteBuilder {
     final Widget enterPage;
     final Widget exitPage;
     static var exBegin = Offset(0.0, 0.0);
     static var exEnd = Offset(-0.3, 0.0);
     static var entBegin = Offset(1.0, 0.0);
     static var entEnd = Offset.zero;
     static var curveIn = Curves.linearToEaseOut;
     static var curveOut = Curves.easeInToLinear;

    SlideStackRightRoute({@required this.exitPage, @required this.enterPage})
      : super(
          transitionDuration: Duration(milliseconds: 1400),
          maintainState: true,
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              enterPage,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              Stack(
            children: <Widget>[
              SlideTransition(
                position: Tween(begin: exBegin, end: exEnd)
                    .chain(CurveTween(curve: curveIn))
                    .chain(CurveTween(curve: curveOut))
                    .animate(animation),
                child: Container(
                  foregroundDecoration: BoxDecoration(
                    color: Colors.black.withOpacity(animation.value / 1.1),
                  ),
                  child: IgnorePointer(

// Disable any touch events on fake exit route
                    ignoring: true,
                    child: exitPage,
                  ),
                ),
              ),
              SlideTransition(
                position: Tween(begin: entBegin, end: entEnd)
                    .chain(CurveTween(curve: curveIn))
                    .chain(CurveTween(curve: curveOut))
                    .animate(animation),
                child: IgnorePointer(

// Disable any touch events on fake exit route only while transitioning
                  ignoring: animation.status != AnimationStatus.completed,
                  child: enterPage,
                ),
              )
            ],
          ),
        );
    }
© www.soinside.com 2019 - 2024. All rights reserved.