UPD2:
Я обнаружил secondaryAnimation
свойство в PageRouteBuilder
, которое делает именно то, что я хочу. И в итоге мы создали именованные маршруты и обработали их через onGenerateRoute
следующим образом:
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
/// Needed to disable animations on some routes
String _currentRoute = "/";
// Other fields and methods...
/// Changes the value of `_currentRoute`
void _setCurrentRoute(String newValue) {
_currentRoute = newValue;
}
/// Check the equality of `_currentRoute` to some value
bool _currentRouteEquals(String value) {
return _currentRoute == value;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
// Other parameters...
initialRoute: "/",
onGenerateRoute: (settings) {
// Note that `onGenerateRoute` doesn't get called when user pops from some screen
_setCurrentRoute(settings.name);
if (settings.isInitialRoute) {
return createRouteTransition<WillPopScope>(
checkExitAnimationEnabled: () => _currentRouteEquals("/settings"),
checkEntAnimationEnabled: () => false,
exitCurve: Curves.linearToEaseOut,
exitReverseCurve: Curves.easeInToLinear,
maintainState: true,
route: WillPopScope(child: MainRoute(), onWillPop: _handleHomePop),
);
} else if (settings.name == "/player") {
return createRouteTransition<PlayerRoute>(
entCurve: Curves.linearToEaseOut,
entReverseCurve: Curves.fastOutSlowIn,
exitCurve: Curves.linearToEaseOut,
exitReverseCurve: Curves.easeInToLinear,
entBegin: Offset(0.0, 1.0),
entIgnoreEvents: true,
checkExitAnimationEnabled: () => _currentRouteEquals("/exif"),
transitionDuration: const Duration(milliseconds: 500),
route: PlayerRoute(),
);
} else if (settings.name == "/settings") {
return createRouteTransition<SettingsRoute>(
entCurve: Curves.linearToEaseOut,
entReverseCurve: Curves.easeInToLinear,
route: SettingsRoute(),
);
} else if (settings.name == "/exif") {
return createRouteTransition<ExifRoute>(
entCurve: Curves.linearToEaseOut,
entReverseCurve: Curves.easeInToLinear,
route: ExifRoute(),
);
} else if (settings.name == "/search") {
return (settings.arguments as Map<String, Route>)["route"];
}
// FIXME: add unknown route
return null;
},
);
}
}
А теперь взглянем на createRouteTransition
function
import 'package:flutter/material.dart';
/// Type for function that returns boolean
typedef BoolFunction = bool Function();
/// Returns `PageRouteBuilder` that performs slide to right animation
PageRouteBuilder<T> createRouteTransition<T extends Widget>({
@required final T route,
/// Function that checks whether to play enter animation or not
///
/// E.G disable exit animation for main route
BoolFunction checkEntAnimationEnabled,
/// Function that checks whether to play exit animation or not
///
/// E.G disable exit animation for particular route pushes
BoolFunction checkExitAnimationEnabled,
/// Begin offset for enter animation
///
/// Defaults to `const Offset(1.0, 0.0)`
final Offset entBegin: const Offset(1.0, 0.0),
/// End offset for enter animation
///
/// Defaults to `Offset.zero`
final Offset entEnd = Offset.zero,
/// Begin offset for exit animation
///
/// Defaults to `Offset.zero`
final Offset exitBegin: Offset.zero,
/// End offset for exit animation
///
/// Defaults to `const Offset(-0.3, 0.0)`
final Offset exitEnd: const Offset(-0.3, 0.0),
/// A curve for enter animation
///
/// Defaults to `Curves.linearToEaseOut`
final Curve entCurve: Curves.linearToEaseOut,
/// A curve for reverse enter animation
///
/// Defaults to `Curves.easeInToLinear`
final Curve entReverseCurve: Curves.easeInToLinear,
/// A curve for exit animation
///
/// Defaults to `Curves.linearToEaseOut`
final Curve exitCurve: Curves.linearToEaseOut,
/// A curve for reverse exit animation
///
/// Defaults to `Curves.easeInToLinear`
final Curve exitReverseCurve: Curves.easeInToLinear,
/// A duration of transition
///
/// Defaults to `const Duration(milliseconds: 430)`
final Duration transitionDuration: const Duration(milliseconds: 430),
/// Field to pass `RouteSettings`
final RouteSettings settings,
///Whether the route obscures previous routes when the transition is complete.
///
/// When an opaque route's entrance transition is complete, the routes behind the opaque route will not be built to save resources.
///
/// Copied from `TransitionRoute`.
///
/// Defaults to true
final bool opaque: true,
/// Whether the route should remain in memory when it is inactive.
///
/// If this is true, then the route is maintained, so that any futures it is holding from the next route will properly resolve when the next route pops. If this is not necessary, this can be set to false to allow the framework to entirely discard the route's widget hierarchy when it is not visible.
///
/// The value of this getter should not change during the lifetime of the object. It is used by [createOverlayEntries], which is called by [install] near the beginning of the route lifecycle.
///
/// Copied from `ModalRoute`.
///
/// Defaults to false
final bool maintainState: false,
/// Whether to ignore touch events while enter animation
///
/// Defaults to false
final bool entIgnoreEvents: false,
}) {
checkEntAnimationEnabled ??= () => true;
checkExitAnimationEnabled ??= () => true;
return PageRouteBuilder<T>(
transitionDuration: Duration(milliseconds: 500),
settings: settings,
opaque: opaque,
maintainState: maintainState,
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) =>
route,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
bool entEnabled = checkEntAnimationEnabled();
bool exitEnabled = checkExitAnimationEnabled();
return TurnableSlideTransition(
enabled: entEnabled,
position: Tween(begin: entBegin, end: entEnd).animate(CurvedAnimation(
parent: animation,
curve: entCurve,
reverseCurve: entReverseCurve)),
child: TurnableSlideTransition(
enabled: exitEnabled,
position: Tween(begin: exitBegin, end: exitEnd).animate(
CurvedAnimation(
parent: secondaryAnimation,
curve: exitCurve,
reverseCurve: exitReverseCurve)),
child: Container(
foregroundDecoration: BoxDecoration(
color: // Dim exit page from 0 to 0.9
Colors.black.withOpacity(
exitEnabled ? secondaryAnimation.value / 1.1 : 0),
),
child: IgnorePointer(
// Disable any touch events on fake exit route only while transitioning
ignoring: entIgnoreEvents &&
(animation.status == AnimationStatus.forward ||
animation.status == AnimationStatus.reverse),
child: child,
),
),
),
);
});
}
/// `SlideTransition` class, but with `enabled` parameter
class TurnableSlideTransition extends SlideTransition {
TurnableSlideTransition(
{Key key,
@required Animation<Offset> position,
bool transformHitTests: true,
TextDirection textDirection,
Widget child,
this.enabled: true})
: super(
key: key,
position: position,
transformHitTests: transformHitTests,
textDirection: textDirection,
child: child,
);
/// If false, animation won't be played
final bool enabled;
@override
Widget build(BuildContext context) {
if (enabled) {
Offset offset = position.value;
if (textDirection == TextDirection.rtl)
offset = Offset(-offset.dx, offset.dy);
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
return child;
}
}
Originalответ:
Следуя совету Marc , я добавил ScrollController
к ListView.builder
и извлек код, который создает список дорожек, в отдельный метод, чтобы иметь возможность создать его поддельную копию.
Но, конечно, такое решение может привести к некоторым проблемам с производительностью, поскольку оно в любом случае создает новый ListView
экземпляр
здесь был удален код
UPD1:
Как я изначально предполагал, простое использование списка приведет к огромной производительности по мере увеличения длины списка.
Чтобы исправить это, вы должны забыть о списке и использовать ScrollablePositinedList
, который еще не доступен в самой библиотеке флаттера, но присутствует в репозитории флаттеров виджетов Google . Этот виджет позволяет вам переходить к элементу в списке без каких-либо проблем перфоманса (на самом деле, если вы посмотрите исходный код, он вообще не использует ListView
внутри него). ИМХО, это идеальное решение и лучшее решение для перехода по списку на данный момент, и я надеюсь, что команда flutter добавит этот виджет в свою библиотеку в будущем.
Так что вам нужно скопировать / установить его в проектзатем выполните следующие шаги:
- Expose
frontScrollController
свойство состояния ScrollablePositinedList
, есть также backScrollController
, но front - главный контроллер прокрутки в этом виджете, если я правильно понял, потому чтодля меня смещение назад всегда равно 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,
),
)
],
),
);
}