first implementation finished
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// An internal representation of a child widget subtree that is a child of
|
||||
/// the [AnimatedIndexedStack].
|
||||
///
|
||||
/// This keeps track of animation controllers, keys, and the child widget.
|
||||
class _ChildEntry {
|
||||
_ChildEntry({
|
||||
required this.key,
|
||||
required this.primaryController,
|
||||
required this.secondaryController,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// The key of this entry.
|
||||
/// This is usually a [GlobalKey] to ensure that children do not lose their state.
|
||||
final Key key;
|
||||
|
||||
/// The animation controller for the child's transition.
|
||||
final AnimationController primaryController;
|
||||
|
||||
/// The (curved) animation being used to drive the transition.
|
||||
final AnimationController secondaryController;
|
||||
Widget child;
|
||||
|
||||
/// Release the resources used by this object.
|
||||
///
|
||||
/// The object is no longer usable after this method is called.
|
||||
void dispose() {
|
||||
primaryController.dispose();
|
||||
secondaryController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'AnimatedIndexedStackEntry#${shortHash(this)}($child)';
|
||||
}
|
||||
|
||||
enum _ChildAnimationDirection {
|
||||
primaryForward,
|
||||
primaryReverse,
|
||||
secondaryForward,
|
||||
secondaryReverse,
|
||||
}
|
||||
|
||||
/// A Widget that shows a single child from a list of children.
|
||||
/// Changing the index will animate the change of widgets according to the [transitionBuilder].
|
||||
/// Removing the widget at the current index will also animate the change.
|
||||
///
|
||||
/// Widgets which are not currently visible will be kept alive until they are removed.
|
||||
class AnimatedIndexedStack extends StatefulWidget {
|
||||
const AnimatedIndexedStack({
|
||||
super.key,
|
||||
this.index = 0,
|
||||
this.duration = const Duration(milliseconds: 300),
|
||||
this.reverse = false,
|
||||
required this.transitionBuilder,
|
||||
this.layoutBuilder = defaultLayoutBuilder,
|
||||
this.children = const [],
|
||||
});
|
||||
|
||||
/// The index of the child to show.
|
||||
///
|
||||
/// If this is null, none of the children will be shown.
|
||||
final int? index;
|
||||
|
||||
/// The duration of the transition from the old [child] value to the new one.
|
||||
final Duration duration;
|
||||
|
||||
/// Indicates whether the new [child] will visually appear on top of or
|
||||
/// underneath the old child.
|
||||
final bool reverse;
|
||||
|
||||
/// A function that wraps a new [child] with a primary and secondary animation
|
||||
/// set define how the child appears and disappears.
|
||||
final Widget Function(
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) transitionBuilder;
|
||||
|
||||
/// A function that lays out all the children in this IndexedStack.
|
||||
/// This defaults to [PageTransitionSwitcher.defaultLayoutBuilder].
|
||||
final Widget Function(List<Widget> entries) layoutBuilder;
|
||||
|
||||
/// The child widgets of the stack.
|
||||
/// Only the child at index [index] will be shown.
|
||||
/// To correctly keep track of the state of child widgets, they must be given unique keys.
|
||||
final List<Widget> children;
|
||||
|
||||
/// The default layout builder for [AnimatedIndexedStack].
|
||||
/// Contains all the children in a [Stack].
|
||||
static Widget defaultLayoutBuilder(List<Widget> entries) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: entries,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<AnimatedIndexedStack> createState() => _AnimatedIndexedStackState();
|
||||
}
|
||||
|
||||
class _AnimatedIndexedStackState extends State<AnimatedIndexedStack>
|
||||
with TickerProviderStateMixin {
|
||||
/// All entries contained in this Stack.
|
||||
/// This is built from the children list, but may also contain entries which are animating out.
|
||||
List<_ChildEntry> _entries = [];
|
||||
|
||||
/// The entry which is currently at the top of the stack.
|
||||
_ChildEntry? _currentEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateEntriesList();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AnimatedIndexedStack oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateEntriesList();
|
||||
}
|
||||
|
||||
/// In place operation to shift a child entry to the end of the list (the visual front).
|
||||
///
|
||||
/// If entry is null, this is a no-op.
|
||||
void _moveToEnd(List<_ChildEntry> entries, _ChildEntry? entry) {
|
||||
if (entry == null) return;
|
||||
entries.remove(entry);
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
/// Inserts an entry as last place in the list and animates it.
|
||||
///
|
||||
/// If entry is null, this is a no-op.
|
||||
void _insertAndAnimate(
|
||||
List<_ChildEntry> entries,
|
||||
_ChildEntry? entry,
|
||||
_ChildAnimationDirection direction,
|
||||
) {
|
||||
if (entry == null) return;
|
||||
_moveToEnd(entries, entry);
|
||||
switch (direction) {
|
||||
case _ChildAnimationDirection.primaryForward:
|
||||
entry.primaryController.forward(from: 0);
|
||||
entry.secondaryController.value = 0;
|
||||
case _ChildAnimationDirection.primaryReverse:
|
||||
entry.primaryController.reverse(from: 1);
|
||||
entry.secondaryController.value = 0;
|
||||
case _ChildAnimationDirection.secondaryForward:
|
||||
entry.primaryController.value = 1;
|
||||
entry.secondaryController.forward(from: 0);
|
||||
case _ChildAnimationDirection.secondaryReverse:
|
||||
entry.primaryController.value = 1;
|
||||
entry.secondaryController.reverse(from: 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the list of child entries.
|
||||
/// Ensures to order the list appropriately and animate entries in and out.
|
||||
void _updateEntriesList() {
|
||||
final List<_ChildEntry> entries = [];
|
||||
|
||||
final _ChildEntry? previousEntry = _currentEntry;
|
||||
_ChildEntry? currentEntry;
|
||||
|
||||
Widget? currentChild;
|
||||
if (widget.index != null && widget.children.isNotEmpty) {
|
||||
currentChild = widget.children[widget.index!];
|
||||
}
|
||||
|
||||
for (final child in widget.children) {
|
||||
// We find the previous entry by looking for an identical child widget.
|
||||
// If the children of this Stack share widget types, they must be given unique keys.
|
||||
final int existingIndex =
|
||||
_entries.indexWhere((entry) => Widget.canUpdate(entry.child, child));
|
||||
|
||||
_ChildEntry? existingEntry;
|
||||
if (existingIndex != -1) {
|
||||
existingEntry = _entries[existingIndex];
|
||||
}
|
||||
|
||||
_ChildEntry entry;
|
||||
|
||||
if (existingEntry != null) {
|
||||
// If we find an existing entry, we update its child widget and reuse it.
|
||||
// This ensures it continues to use the same global key and animation controllers.
|
||||
existingEntry.child = child;
|
||||
existingEntry.primaryController.duration = widget.duration;
|
||||
existingEntry.secondaryController.duration = widget.duration;
|
||||
entry = existingEntry;
|
||||
} else {
|
||||
entry = _newEntry(child);
|
||||
}
|
||||
|
||||
if (currentChild == child) {
|
||||
currentEntry = entry;
|
||||
}
|
||||
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
final bool hasChanged = previousEntry != currentEntry;
|
||||
final bool previousWasRemoved =
|
||||
previousEntry != null && !entries.contains(previousEntry);
|
||||
|
||||
if (hasChanged) {
|
||||
if (widget.reverse) {
|
||||
// When reverse is true, the new child will transition in below the
|
||||
// old child while its secondary animation and the primary
|
||||
// animation of the old child are running in reverse. This is similar to
|
||||
// the transition associated with popping a [PageRoute] to reveal a new
|
||||
// [PageRoute] below it.
|
||||
_insertAndAnimate(
|
||||
entries,
|
||||
currentEntry,
|
||||
_ChildAnimationDirection.secondaryReverse,
|
||||
);
|
||||
_insertAndAnimate(
|
||||
entries,
|
||||
previousEntry,
|
||||
_ChildAnimationDirection.primaryReverse,
|
||||
);
|
||||
if (previousWasRemoved) {
|
||||
previousEntry.primaryController.addStatusListener((status) {
|
||||
if (status == AnimationStatus.dismissed) {
|
||||
setState(() {
|
||||
_entries.remove(previousEntry);
|
||||
previousEntry.dispose();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// When reverse is false, the new child will transition in on top of the
|
||||
// old child while its primary animation and the secondary
|
||||
// animation of the old child are running forward. This is similar to
|
||||
// the transition associated with pushing a new [PageRoute] on top of
|
||||
// another.
|
||||
_insertAndAnimate(
|
||||
entries,
|
||||
previousEntry,
|
||||
_ChildAnimationDirection.secondaryForward,
|
||||
);
|
||||
_insertAndAnimate(
|
||||
entries,
|
||||
currentEntry,
|
||||
_ChildAnimationDirection.primaryForward,
|
||||
);
|
||||
if (previousWasRemoved) {
|
||||
previousEntry.secondaryController.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
setState(() {
|
||||
_entries.remove(previousEntry);
|
||||
previousEntry.dispose();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (widget.reverse) {
|
||||
_moveToEnd(entries, currentEntry);
|
||||
_moveToEnd(entries, previousEntry);
|
||||
} else {
|
||||
_moveToEnd(entries, previousEntry);
|
||||
_moveToEnd(entries, currentEntry);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_entries = entries;
|
||||
_currentEntry = currentEntry;
|
||||
});
|
||||
}
|
||||
|
||||
_ChildEntry _newEntry(Widget child) => _ChildEntry(
|
||||
key: GlobalKey(),
|
||||
child: child,
|
||||
primaryController: AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
),
|
||||
secondaryController: AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final entry in _entries) {
|
||||
entry.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildChild(_ChildEntry entry) => AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
entry.primaryController,
|
||||
entry.secondaryController,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
final bool isVisible = entry.primaryController.isAnimating ||
|
||||
entry.secondaryController.isAnimating ||
|
||||
entry == _currentEntry;
|
||||
|
||||
return Visibility(
|
||||
visible: isVisible,
|
||||
maintainState: true,
|
||||
child: widget.transitionBuilder(
|
||||
KeyedSubtree(
|
||||
key: entry.key,
|
||||
child: child!,
|
||||
),
|
||||
entry.primaryController,
|
||||
entry.secondaryController,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: entry.child,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.layoutBuilder(_entries.map(_buildChild).toList());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user