我想在我的flutter应用程序中完成从幻灯片到正确过渡的操作。问题是路由转换有点创建了我要从其转换的页面的新实例,因此ListView
滚动重置。
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,
)
],
),
);
}
按照Marc的建议,我在ScrollController
上添加了ListView.builder
,并将提取出创建曲目列表的代码提取到单独的方法中,以便能够创建其假副本。
但是,这种解决方案在创建新的ListView
实例时可能会导致一些性能问题
此处已删除代码
正如我最初所预期的,随着列表长度的增加,仅使用列表将带来巨大的性能。
要解决此问题,您必须忘记列表并使用flutter的库本身尚不可用的ScrollablePositinedList
,但它存在于google's flutter widgets repository中。这个小部件可让您跳到列表中的元素而不会出现性能问题(实际上,如果您查看源代码,则根本不使用其中的ListView
)。恕我直言,这是一个完美的解决方案,也是暂时跳过列表的最佳解决方案,我希望flutter团队将来能够将此小部件添加到他们的库中。
因此您必须将其复制/安装到项目中,然后执行后续步骤:
frontScrollController
状态的ScrollablePositinedList
属性,还有backScrollController
,但是如果我正确理解,front是此小部件中的主滚动控制器,因为对我来说back的偏移量始终等于0。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;
});
}
}
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,
),
)
],
),
);
}