From 8ef0d87c9674e26673bba3c3526fefef911930b0 Mon Sep 17 00:00:00 2001 From: Shubham Soni Date: Mon, 24 Jan 2022 15:20:31 +0530 Subject: [PATCH] Initial code --- .gitignore | 75 +++++++++ .metadata | 10 ++ CHANGELOG.md | 3 + LICENSE | 1 + README.md | 39 +++++ analysis_options.yaml | 4 + lib/autoscale_tabbarview.dart | 5 + lib/src/autoscale_tabbar_widget.dart | 228 +++++++++++++++++++++++++++ lib/src/size_detector_widget.dart | 42 +++++ lib/src/sized_pageview.dart | 81 ++++++++++ pubspec.lock | 161 +++++++++++++++++++ pubspec.yaml | 54 +++++++ 12 files changed, 703 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 lib/autoscale_tabbarview.dart create mode 100644 lib/src/autoscale_tabbar_widget.dart create mode 100644 lib/src/size_detector_widget.dart create mode 100644 lib/src/sized_pageview.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a247422 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..690e6b3 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 18116933e77adc82f80866c928266a5b4f1ed645 + channel: unknown + +project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/autoscale_tabbarview.dart b/lib/autoscale_tabbarview.dart new file mode 100644 index 0000000..68501b3 --- /dev/null +++ b/lib/autoscale_tabbarview.dart @@ -0,0 +1,5 @@ +library autoscale_tabbarview; + +export 'src/autoscale_tabbar_widget.dart'; +export 'src/sized_pageview.dart'; +export 'src/size_detector_widget.dart'; diff --git a/lib/src/autoscale_tabbar_widget.dart b/lib/src/autoscale_tabbar_widget.dart new file mode 100644 index 0000000..12239f7 --- /dev/null +++ b/lib/src/autoscale_tabbar_widget.dart @@ -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 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 createState() => _AutoScaleTabBarViewState(); +} + +class _AutoScaleTabBarViewState extends State { + TabController? _controller; + late PageController _pageController; + late List _children; + late List _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 _warpToCurrentIndex() async { + if (!mounted) return Future.value(); + + if (_pageController.page == _currentIndex!.toDouble()) + return Future.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.value(); + } + + assert((_currentIndex! - previousIndex).abs() > 1); + final int initialPage = _currentIndex! > previousIndex + ? _currentIndex! - 1 + : _currentIndex! + 1; + final List originalChildren = _childrenWithKey; + setState(() { + _warpUnderwayCount += 1; + + _childrenWithKey = List.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.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( + 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, + ), + ); + } +} diff --git a/lib/src/size_detector_widget.dart b/lib/src/size_detector_widget.dart new file mode 100644 index 0000000..e4f7c09 --- /dev/null +++ b/lib/src/size_detector_widget.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class SizeDetectorWidget extends StatefulWidget { + final Widget child; + final ValueChanged onSizeDetect; + + const SizeDetectorWidget({ + Key? key, + required this.child, + required this.onSizeDetect, + }) : super(key: key); + + @override + _SizeDetectorWidgetState createState() => _SizeDetectorWidgetState(); +} + +class _SizeDetectorWidgetState extends State { + 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!); + } + } +} diff --git a/lib/src/sized_pageview.dart b/lib/src/sized_pageview.dart new file mode 100644 index 0000000..56a5b15 --- /dev/null +++ b/lib/src/sized_pageview.dart @@ -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 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 + with SingleTickerProviderStateMixin { + late List _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( + curve: Curves.easeInOutCubic, + duration: const Duration(milliseconds: 100), + tween: Tween(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(); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..816c48a --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,161 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.10" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" +sdks: + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..9e92d0d --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,54 @@ +name: autoscale_tabbarview +description: A new Flutter package project. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages