Initial code

This commit is contained in:
Shubham Soni
2022-01-24 15:20:31 +05:30
commit 8ef0d87c96
12 changed files with 703 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
library autoscale_tabbarview;
export 'src/autoscale_tabbar_widget.dart';
export 'src/sized_pageview.dart';
export 'src/size_detector_widget.dart';
+228
View File
@@ -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,
),
);
}
}
+42
View File
@@ -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!);
}
}
}
+81
View File
@@ -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();
}
}