mirror of
https://github.com/mzelldev/autoscale_tabbarview.git
synced 2024-09-21 16:56:27 +00:00
Initial code
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'sized_pageview.dart';
|
||||
|
||||
class AutoScaleTabBarView extends StatefulWidget {
|
||||
/// Creates a page view with one child per tab.
|
||||
///
|
||||
/// The length of [children] must be the same as the [controller]'s length.
|
||||
const AutoScaleTabBarView({
|
||||
Key? key,
|
||||
required this.children,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
}) : assert(children != null),
|
||||
assert(dragStartBehavior != null),
|
||||
super(key: key);
|
||||
|
||||
/// This widget's selection and animation state.
|
||||
///
|
||||
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
|
||||
/// will be used.
|
||||
final TabController? controller;
|
||||
|
||||
/// One widget per tab.
|
||||
///
|
||||
/// Its length must match the length of the [TabBar.tabs]
|
||||
/// list, as well as the [controller]'s [TabController.length].
|
||||
final List<Widget> children;
|
||||
|
||||
/// How the page view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the page view continues to animate after the
|
||||
/// user stops dragging the page view.
|
||||
///
|
||||
/// The physics are modified to snap to page boundaries using
|
||||
/// [PageScrollPhysics] prior to being used.
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
@override
|
||||
State<AutoScaleTabBarView> createState() => _AutoScaleTabBarViewState();
|
||||
}
|
||||
|
||||
class _AutoScaleTabBarViewState extends State<AutoScaleTabBarView> {
|
||||
TabController? _controller;
|
||||
late PageController _pageController;
|
||||
late List<Widget> _children;
|
||||
late List<Widget> _childrenWithKey;
|
||||
int? _currentIndex;
|
||||
int _warpUnderwayCount = 0;
|
||||
|
||||
// If the TabBarView is rebuilt with a new tab controller, the caller should
|
||||
// dispose the old one. In that case the old controller's animation will be
|
||||
// null and should not be accessed.
|
||||
bool get _controllerIsValid => _controller?.animation != null;
|
||||
|
||||
void _updateTabController() {
|
||||
final TabController? newController =
|
||||
widget.controller ?? DefaultTabController.of(context);
|
||||
assert(() {
|
||||
if (newController == null) {
|
||||
throw FlutterError(
|
||||
'No TabController for ${widget.runtimeType}.\n'
|
||||
'When creating a ${widget.runtimeType}, you must either provide an explicit '
|
||||
'TabController using the "controller" property, or you must ensure that there '
|
||||
'is a DefaultTabController above the ${widget.runtimeType}.\n'
|
||||
'In this case, there was neither an explicit controller nor a default controller.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (newController == _controller) return;
|
||||
|
||||
if (_controllerIsValid)
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
_controller = newController;
|
||||
if (_controller != null)
|
||||
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_updateTabController();
|
||||
_currentIndex = _controller?.index;
|
||||
_pageController = PageController(initialPage: _currentIndex ?? 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AutoScaleTabBarView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) _updateTabController();
|
||||
if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
|
||||
_updateChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_controllerIsValid)
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
_controller = null;
|
||||
// We don't own the _controller Animation, so it's not disposed here.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_children = widget.children;
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
if (_warpUnderwayCount > 0 || !_controller!.indexIsChanging)
|
||||
return; // This widget is driving the controller's animation.
|
||||
|
||||
if (_controller!.index != _currentIndex) {
|
||||
_currentIndex = _controller!.index;
|
||||
_warpToCurrentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warpToCurrentIndex() async {
|
||||
if (!mounted) return Future<void>.value();
|
||||
|
||||
if (_pageController.page == _currentIndex!.toDouble())
|
||||
return Future<void>.value();
|
||||
|
||||
final int previousIndex = _controller!.previousIndex;
|
||||
if ((_currentIndex! - previousIndex).abs() == 1) {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController.animateToPage(_currentIndex!,
|
||||
duration: kTabScrollDuration, curve: Curves.ease);
|
||||
_warpUnderwayCount -= 1;
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
assert((_currentIndex! - previousIndex).abs() > 1);
|
||||
final int initialPage = _currentIndex! > previousIndex
|
||||
? _currentIndex! - 1
|
||||
: _currentIndex! + 1;
|
||||
final List<Widget> originalChildren = _childrenWithKey;
|
||||
setState(() {
|
||||
_warpUnderwayCount += 1;
|
||||
|
||||
_childrenWithKey = List<Widget>.from(_childrenWithKey, growable: false);
|
||||
final Widget temp = _childrenWithKey[initialPage];
|
||||
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
|
||||
_childrenWithKey[previousIndex] = temp;
|
||||
});
|
||||
_pageController.jumpToPage(initialPage);
|
||||
|
||||
await _pageController.animateToPage(_currentIndex!,
|
||||
duration: kTabScrollDuration, curve: Curves.ease);
|
||||
if (!mounted) return Future<void>.value();
|
||||
setState(() {
|
||||
_warpUnderwayCount -= 1;
|
||||
if (widget.children != _children) {
|
||||
_updateChildren();
|
||||
} else {
|
||||
_childrenWithKey = originalChildren;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called when the PageView scrolls
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (_warpUnderwayCount > 0) return false;
|
||||
|
||||
if (notification.depth != 0) return false;
|
||||
|
||||
_warpUnderwayCount += 1;
|
||||
if (notification is ScrollUpdateNotification &&
|
||||
!_controller!.indexIsChanging) {
|
||||
if ((_pageController.page! - _controller!.index).abs() > 1.0) {
|
||||
_controller!.index = _pageController.page!.floor();
|
||||
_currentIndex = _controller!.index;
|
||||
}
|
||||
_controller!.offset =
|
||||
(_pageController.page! - _controller!.index).clamp(-1.0, 1.0);
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_controller!.index = _pageController.page!.round();
|
||||
_currentIndex = _controller!.index;
|
||||
if (!_controller!.indexIsChanging) {
|
||||
_controller!.offset =
|
||||
(_pageController.page! - _controller!.index).clamp(-1.0, 1.0);
|
||||
}
|
||||
}
|
||||
_warpUnderwayCount -= 1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(() {
|
||||
if (_controller!.length != widget.children.length) {
|
||||
throw FlutterError(
|
||||
"Controller's length property (${_controller!.length}) does not match the "
|
||||
"number of tabs (${widget.children.length}) present in TabBar's tabs property.",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: SizedPageView(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
pageController: _pageController,
|
||||
physics: widget.physics == null
|
||||
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
|
||||
: const PageScrollPhysics().applyTo(widget.physics),
|
||||
children: _childrenWithKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class SizeDetectorWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
final ValueChanged<Size> onSizeDetect;
|
||||
|
||||
const SizeDetectorWidget({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.onSizeDetect,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SizeDetectorWidgetState createState() => _SizeDetectorWidgetState();
|
||||
}
|
||||
|
||||
class _SizeDetectorWidgetState extends State<SizeDetectorWidget> {
|
||||
Size? _oldSize;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SchedulerBinding.instance?.addPostFrameCallback((_) => _detectSize());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
void _detectSize() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final size = context.size;
|
||||
if (_oldSize != size) {
|
||||
_oldSize = size;
|
||||
widget.onSizeDetect(size!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'size_detector_widget.dart';
|
||||
|
||||
class SizedPageView extends StatefulWidget {
|
||||
final List<Widget> children;
|
||||
final PageController pageController;
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
final ScrollPhysics physics;
|
||||
|
||||
const SizedPageView({
|
||||
Key? key,
|
||||
required this.children,
|
||||
required this.pageController,
|
||||
required this.dragStartBehavior,
|
||||
required this.physics,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SizedPageViewState createState() => _SizedPageViewState();
|
||||
}
|
||||
|
||||
class _SizedPageViewState extends State<SizedPageView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late List<double> _heights;
|
||||
int _currentIndex = 0;
|
||||
|
||||
double get _currentHeight => _heights[_currentIndex];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_heights = List.generate(widget.children.length, (index) => 0.0);
|
||||
|
||||
widget.pageController.addListener(() {
|
||||
final _newIndex = widget.pageController.page?.round();
|
||||
if (_currentIndex != _newIndex) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _currentIndex = _newIndex!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
curve: Curves.easeInOutCubic,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
tween: Tween<double>(begin: _heights[0], end: _currentHeight),
|
||||
builder: (context, value, child) => SizedBox(height: value, child: child),
|
||||
child: PageView(
|
||||
controller: widget.pageController,
|
||||
physics: widget.physics,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
children: List.generate(widget.children.length, (index) {
|
||||
return OverflowBox(
|
||||
maxHeight: double.infinity,
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizeDetectorWidget(
|
||||
onSizeDetect: (size) {
|
||||
if (mounted) {
|
||||
setState(() => _heights[index] = size.height);
|
||||
}
|
||||
},
|
||||
child: Align(child: widget.children[index]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user