diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cbed76..d1b3440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,4 +59,8 @@ * Addition of 'obscureText' parameter to 'FlutterFormInputPlainText' ## 2.7.0 -* Addition of 'decoration' parameter to 'FlutterFormInputPassword' \ No newline at end of file +* Addition of 'decoration' parameter to 'FlutterFormInputPassword' + +## 2.7.1 + +* Added Iconica CI and Iconica Linter \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..31b4b51 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,9 @@ -include: package:flutter_lints/flutter.yaml +include: package:flutter_iconica_analysis/analysis_options.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: diff --git a/example/.metadata b/example/.metadata index 8245e38..f41bb42 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,11 +1,11 @@ # 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. +# This file should be version controlled and should not be manually edited. version: - revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - channel: stable + revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4" + channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - - platform: web - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + - platform: ios + create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 # User provided section diff --git a/example/pubspec.lock b/example/pubspec.lock index 2b34ecf..4365e71 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" cupertino_icons: dependency: "direct main" description: @@ -68,7 +68,7 @@ packages: path: ".." relative: true source: path - version: "2.4.0" + version: "2.7.0" flutter_lints: dependency: "direct dev" description: @@ -118,10 +118,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" path: dependency: transitive description: @@ -147,18 +147,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -179,10 +179,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" vector_math: dependency: transitive description: @@ -195,10 +195,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=1.17.0" diff --git a/lib/flutter_input_library.dart b/lib/flutter_input_library.dart index 37126cb..7bc3686 100644 --- a/lib/flutter_input_library.dart +++ b/lib/flutter_input_library.dart @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2022 Iconica // // SPDX-License-Identifier: BSD-3-Clause +/// A library for creating input fields in Flutter. library flutter_input_library; export 'src/inputs/inputs.dart'; diff --git a/lib/src/inputs/carousel/carousel.dart b/lib/src/inputs/carousel/carousel.dart index bd03da4..b6b2454 100644 --- a/lib/src/inputs/carousel/carousel.dart +++ b/lib/src/inputs/carousel/carousel.dart @@ -4,20 +4,18 @@ import 'package:flutter/material.dart'; -import 'carousel_form.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_form.dart'; class FlutterFormInputCarousel extends StatelessWidget { const FlutterFormInputCarousel({ - Key? key, required this.items, + super.key, this.height = 425, this.onSaved, this.onChanged, this.initialValue, this.validator, - }) : super( - key: key, - ); + }); final List items; final double height; @@ -27,14 +25,12 @@ class FlutterFormInputCarousel extends StatelessWidget { final int? initialValue; @override - Widget build(BuildContext context) { - return CarouselFormField( - onSaved: (value) => onSaved?.call(value), - validator: (value) => validator?.call(value), - onChanged: (value) => onChanged?.call(value), - initialValue: initialValue ?? 0, - items: items, - height: height, - ); - } + Widget build(BuildContext context) => CarouselFormField( + onSaved: (value) => onSaved?.call(value), + validator: (value) => validator?.call(value), + onChanged: (value) => onChanged?.call(value), + initialValue: initialValue ?? 0, + items: items, + height: height, + ); } diff --git a/lib/src/inputs/carousel/carousel_controller.dart b/lib/src/inputs/carousel/carousel_controller.dart index e0e4c9f..2c29c8b 100644 --- a/lib/src/inputs/carousel/carousel_controller.dart +++ b/lib/src/inputs/carousel/carousel_controller.dart @@ -5,12 +5,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; - -import 'carousel_utils.dart'; -import 'carousel_options.dart'; -import 'carousel_state.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_options.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_state.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_utils.dart'; abstract class CarouselController { + factory CarouselController() => CarouselControllerImpl(); bool get ready; Future get onReady; @@ -26,8 +26,6 @@ abstract class CarouselController { void startAutoPlay(); void stopAutoPlay(); - - factory CarouselController() => CarouselControllerImpl(); } class CarouselControllerImpl implements CarouselController { @@ -56,10 +54,11 @@ class CarouselControllerImpl implements CarouselController { /// The animation lasts for the given duration and follows the given curve. /// The returned [Future] resolves when the animation completes. @override - Future nextPage( - {Duration? duration = const Duration(milliseconds: 300), - Curve? curve = Curves.linear}) async { - final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate; + Future nextPage({ + Duration? duration = const Duration(milliseconds: 300), + Curve? curve = Curves.linear, + }) async { + var isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate; if (isNeedResetTimer) { _state!.onResetTimer(); } @@ -75,10 +74,11 @@ class CarouselControllerImpl implements CarouselController { /// The animation lasts for the given duration and follows the given curve. /// The returned [Future] resolves when the animation completes. @override - Future previousPage( - {Duration? duration = const Duration(milliseconds: 300), - Curve? curve = Curves.linear}) async { - final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate; + Future previousPage({ + Duration? duration = const Duration(milliseconds: 300), + Curve? curve = Curves.linear, + }) async { + var isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate; if (isNeedResetTimer) { _state!.onResetTimer(); } @@ -96,11 +96,14 @@ class CarouselControllerImpl implements CarouselController { /// without animation, and without checking if the new value is in range. @override void jumpToPage(int page) { - final index = getRealIndex(_state!.pageController!.page!.toInt(), - _state!.realPage - _state!.initialPage, _state!.itemCount); + var index = getRealIndex( + _state!.pageController!.page!.toInt(), + _state!.realPage - _state!.initialPage, + _state!.itemCount, + ); _setModeController(); - final int pageToJump = _state!.pageController!.page!.toInt() + page - index; + var pageToJump = _state!.pageController!.page!.toInt() + page - index; return _state!.pageController!.jumpToPage(pageToJump); } @@ -110,20 +113,26 @@ class CarouselControllerImpl implements CarouselController { /// The animation lasts for the given duration and follows the given curve. /// The returned [Future] resolves when the animation completes. @override - Future animateToPage(int page, - {Duration? duration = const Duration(milliseconds: 300), - Curve? curve = Curves.linear}) async { - final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate; + Future animateToPage( + int page, { + Duration? duration = const Duration(milliseconds: 300), + Curve? curve = Curves.linear, + }) async { + var isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate; if (isNeedResetTimer) { _state!.onResetTimer(); } - final index = getRealIndex(_state!.pageController!.page!.toInt(), - _state!.realPage - _state!.initialPage, _state!.itemCount); + var index = getRealIndex( + _state!.pageController!.page!.toInt(), + _state!.realPage - _state!.initialPage, + _state!.itemCount, + ); _setModeController(); await _state!.pageController!.animateToPage( - _state!.pageController!.page!.toInt() + page - index, - duration: duration!, - curve: curve!); + _state!.pageController!.page!.toInt() + page - index, + duration: duration!, + curve: curve!, + ); if (isNeedResetTimer) { _state!.onResumeTimer(); } diff --git a/lib/src/inputs/carousel/carousel_form.dart b/lib/src/inputs/carousel/carousel_form.dart index 6bd482d..2dff53e 100644 --- a/lib/src/inputs/carousel/carousel_form.dart +++ b/lib/src/inputs/carousel/carousel_form.dart @@ -3,41 +3,32 @@ // SPDX-License-Identifier: BSD-3-Clause import 'package:flutter/material.dart'; -import 'carousel_slider.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_slider.dart'; class CarouselFormField extends FormField { CarouselFormField({ - Key? key, - required FormFieldSetter onSaved, - required FormFieldValidator validator, - void Function(int value)? onChanged, - void Function(int value)? onSubmit, - int initialValue = 0, - bool autovalidate = false, + required FormFieldSetter super.onSaved, + required FormFieldValidator super.validator, required List items, + super.key, + void Function(int value)? onChanged, + int super.initialValue = 0, double height = 425, }) : super( - key: key, - onSaved: onSaved, - validator: validator, - initialValue: initialValue, - builder: (FormFieldState state) { - return CarouselSlider( - options: CarouselOptions( - initialPage: initialValue, - onPageChanged: (index, reason) { - onChanged?.call(index); + builder: (FormFieldState state) => CarouselSlider( + options: CarouselOptions( + initialPage: initialValue, + onPageChanged: (index, reason) { + onChanged?.call(index); - state.didChange(index); - }, - height: height, - aspectRatio: 2.0, - enlargeCenterPage: true, - enableInfiniteScroll: false, - ), - items: items.map((Widget item) { - return item; - }).toList(), - ); - }); + state.didChange(index); + }, + height: height, + aspectRatio: 2.0, + enlargeCenterPage: true, + enableInfiniteScroll: false, + ), + items: items.map((Widget item) => item).toList(), + ), + ); } diff --git a/lib/src/inputs/carousel/carousel_options.dart b/lib/src/inputs/carousel/carousel_options.dart index f279d5f..37cb91c 100644 --- a/lib/src/inputs/carousel/carousel_options.dart +++ b/lib/src/inputs/carousel/carousel_options.dart @@ -9,6 +9,33 @@ enum CarouselPageChangedReason { timed, manual, controller } enum CenterPageEnlargeStrategy { scale, height } class CarouselOptions { + CarouselOptions({ + this.height, + this.aspectRatio = 16 / 9, + this.viewportFraction = 0.8, + this.initialPage = 0, + this.enableInfiniteScroll = true, + this.reverse = false, + this.autoPlay = false, + this.autoPlayInterval = const Duration(seconds: 4), + this.autoPlayAnimationDuration = const Duration(milliseconds: 800), + this.autoPlayCurve = Curves.fastOutSlowIn, + this.enlargeCenterPage = false, + this.onPageChanged, + this.onScrolled, + this.scrollPhysics, + this.pageSnapping = true, + this.scrollDirection = Axis.horizontal, + this.pauseAutoPlayOnTouch = true, + this.pauseAutoPlayOnManualNavigate = true, + this.pauseAutoPlayInFiniteScroll = false, + this.pageViewKey, + this.enlargeStrategy = CenterPageEnlargeStrategy.scale, + this.disableCenter = false, + this.padEnds = true, + this.clipBehavior = Clip.hardEdge, + }); + /// Set carousel height and overrides any existing [aspectRatio]. final double? height; @@ -105,18 +132,19 @@ class CarouselOptions { /// Default to `true`. final bool pauseAutoPlayOnManualNavigate; - /// If [enableInfiniteScroll] is `false`, and [autoPlay] is `true`, this option - /// decide the carousel should go to the first item when it reach the last item or not. - /// If set to `true`, the auto play will be paused when it reach the last item. - /// If set to `false`, the auto play function will animate to the first item - /// when it was in the last item. + /// If [enableInfiniteScroll] is `false`, and [autoPlay] is `true`, + /// this option decide the carousel should go to the first item when it + /// reach the last item or not. If set to `true`, the auto play will be + /// paused when it reach the last item. If set to `false`, the auto play + /// function will animate to the first item when it was in the last item. final bool pauseAutoPlayInFiniteScroll; /// Pass a [PageStorageKey] if you want to keep the pageview's position when /// it was recreated. final PageStorageKey? pageViewKey; - /// Use [enlargeStrategy] to determine which method to enlarge the center page. + /// Use [enlargeStrategy] to determine which method to enlarge the + /// center page. final CenterPageEnlargeStrategy enlargeStrategy; /// Whether or not to disable the [Center] widget for each slide. @@ -134,60 +162,34 @@ class CarouselOptions { /// Exposed [clipBehavior] of [PageView] final Clip clipBehavior; - CarouselOptions({ - this.height, - this.aspectRatio = 16 / 9, - this.viewportFraction = 0.8, - this.initialPage = 0, - this.enableInfiniteScroll = true, - this.reverse = false, - this.autoPlay = false, - this.autoPlayInterval = const Duration(seconds: 4), - this.autoPlayAnimationDuration = const Duration(milliseconds: 800), - this.autoPlayCurve = Curves.fastOutSlowIn, - this.enlargeCenterPage = false, - this.onPageChanged, - this.onScrolled, - this.scrollPhysics, - this.pageSnapping = true, - this.scrollDirection = Axis.horizontal, - this.pauseAutoPlayOnTouch = true, - this.pauseAutoPlayOnManualNavigate = true, - this.pauseAutoPlayInFiniteScroll = false, - this.pageViewKey, - this.enlargeStrategy = CenterPageEnlargeStrategy.scale, - this.disableCenter = false, - this.padEnds = true, - this.clipBehavior = Clip.hardEdge, - }); - ///Generate new [CarouselOptions] based on old ones. - CarouselOptions copyWith( - {double? height, - double? aspectRatio, - double? viewportFraction, - int? initialPage, - bool? enableInfiniteScroll, - bool? reverse, - bool? autoPlay, - Duration? autoPlayInterval, - Duration? autoPlayAnimationDuration, - Curve? autoPlayCurve, - bool? enlargeCenterPage, - Function(int index, CarouselPageChangedReason reason)? onPageChanged, - ValueChanged? onScrolled, - ScrollPhysics? scrollPhysics, - bool? pageSnapping, - Axis? scrollDirection, - bool? pauseAutoPlayOnTouch, - bool? pauseAutoPlayOnManualNavigate, - bool? pauseAutoPlayInFiniteScroll, - PageStorageKey? pageViewKey, - CenterPageEnlargeStrategy? enlargeStrategy, - bool? disableCenter, - Clip? clipBehavior, - bool? padEnds}) => + CarouselOptions copyWith({ + double? height, + double? aspectRatio, + double? viewportFraction, + int? initialPage, + bool? enableInfiniteScroll, + bool? reverse, + bool? autoPlay, + Duration? autoPlayInterval, + Duration? autoPlayAnimationDuration, + Curve? autoPlayCurve, + bool? enlargeCenterPage, + Function(int index, CarouselPageChangedReason reason)? onPageChanged, + ValueChanged? onScrolled, + ScrollPhysics? scrollPhysics, + bool? pageSnapping, + Axis? scrollDirection, + bool? pauseAutoPlayOnTouch, + bool? pauseAutoPlayOnManualNavigate, + bool? pauseAutoPlayInFiniteScroll, + PageStorageKey? pageViewKey, + CenterPageEnlargeStrategy? enlargeStrategy, + bool? disableCenter, + Clip? clipBehavior, + bool? padEnds, + }) => CarouselOptions( height: height ?? this.height, aspectRatio: aspectRatio ?? this.aspectRatio, diff --git a/lib/src/inputs/carousel/carousel_slider.dart b/lib/src/inputs/carousel/carousel_slider.dart index 3f22e25..f143169 100644 --- a/lib/src/inputs/carousel/carousel_slider.dart +++ b/lib/src/inputs/carousel/carousel_slider.dart @@ -1,26 +1,51 @@ // SPDX-FileCopyrightText: 2022 Iconica // // SPDX-License-Identifier: BSD-3-Clause - +/// library carousel_slider; import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'carousel_state.dart'; -import 'carousel_utils.dart'; - -import 'carousel_controller.dart'; -import 'carousel_options.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_controller.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_options.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_state.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_utils.dart'; export 'carousel_controller.dart'; export 'carousel_options.dart'; typedef ExtendedIndexedWidgetBuilder = Widget Function( - BuildContext context, int index, int realIndex); + BuildContext context, + int index, + int realIndex, +); class CarouselSlider extends StatefulWidget { + CarouselSlider({ + required this.items, + required this.options, + CarouselController? carouselController, + super.key, + }) : itemBuilder = null, + itemCount = items != null ? items.length : 0, + _carouselController = carouselController != null + ? carouselController as CarouselControllerImpl + : CarouselController() as CarouselControllerImpl; + + /// The on demand item builder constructor/ + CarouselSlider.builder({ + required this.itemCount, + required this.itemBuilder, + required this.options, + CarouselController? carouselController, + super.key, + }) : items = null, + _carouselController = carouselController != null + ? carouselController as CarouselControllerImpl + : CarouselController() as CarouselControllerImpl; + /// [CarouselOptions] to create a [CarouselState] with. final CarouselOptions options; @@ -28,8 +53,8 @@ class CarouselSlider extends StatefulWidget { final List? items; /// The widget item builder that will be used to build item on demand - /// The third argument is the [PageView]'s real index, can be used to cooperate - /// with Hero. + /// The third argument is the [PageView]'s real index, can be used + /// to cooperate with Hero. final ExtendedIndexedWidgetBuilder? itemBuilder; /// A [MapController], used to control the map. @@ -37,37 +62,13 @@ class CarouselSlider extends StatefulWidget { final int? itemCount; - CarouselSlider( - {required this.items, - required this.options, - CarouselController? carouselController, - Key? key}) - : itemBuilder = null, - itemCount = items != null ? items.length : 0, - _carouselController = carouselController != null - ? carouselController as CarouselControllerImpl - : CarouselController() as CarouselControllerImpl, - super(key: key); - - /// The on demand item builder constructor/ - CarouselSlider.builder( - {required this.itemCount, - required this.itemBuilder, - required this.options, - CarouselController? carouselController, - Key? key}) - : items = null, - _carouselController = carouselController != null - ? carouselController as CarouselControllerImpl - : CarouselController() as CarouselControllerImpl, - super(key: key); - @override CarouselSliderState createState() => CarouselSliderState(); } class CarouselSliderState extends State with TickerProviderStateMixin { + CarouselSliderState(); late CarouselControllerImpl carouselController; Timer? timer; @@ -80,8 +81,6 @@ class CarouselSliderState extends State /// [mode] is related to why the page is being changed. CarouselPageChangedReason mode = CarouselPageChangedReason.controller; - CarouselSliderState(); - void changeMode(CarouselPageChangedReason mode) { this.mode = mode; } @@ -127,36 +126,35 @@ class CarouselSliderState extends State carouselState!.pageController = pageController; } - Timer? getTimer() { - return widget.options.autoPlay - ? Timer.periodic(widget.options.autoPlayInterval, (_) { - final route = ModalRoute.of(context); - if (route?.isCurrent == false) { + Timer? getTimer() => widget.options.autoPlay + ? Timer.periodic(widget.options.autoPlayInterval, (_) async { + var route = ModalRoute.of(context); + if (route?.isCurrent == false) { + return; + } + + var previousReason = mode; + changeMode(CarouselPageChangedReason.timed); + var nextPage = carouselState!.pageController!.page!.round() + 1; + var itemCount = widget.itemCount ?? widget.items!.length; + + if (nextPage >= itemCount && !widget.options.enableInfiniteScroll) { + if (widget.options.pauseAutoPlayInFiniteScroll) { + clearTimer(); return; } + nextPage = 0; + } - CarouselPageChangedReason previousReason = mode; - changeMode(CarouselPageChangedReason.timed); - int nextPage = carouselState!.pageController!.page!.round() + 1; - int itemCount = widget.itemCount ?? widget.items!.length; - - if (nextPage >= itemCount && - widget.options.enableInfiniteScroll == false) { - if (widget.options.pauseAutoPlayInFiniteScroll) { - clearTimer(); - return; - } - nextPage = 0; - } - - carouselState!.pageController! - .animateToPage(nextPage, - duration: widget.options.autoPlayAnimationDuration, - curve: widget.options.autoPlayCurve) - .then((_) => changeMode(previousReason)); - }) - : null; - } + await carouselState!.pageController! + .animateToPage( + nextPage, + duration: widget.options.autoPlayAnimationDuration, + curve: widget.options.autoPlayCurve, + ) + .then((_) => changeMode(previousReason)); + }) + : null; void clearTimer() { if (timer != null) { @@ -170,7 +168,7 @@ class CarouselSliderState extends State } void handleAutoPlay() { - bool autoPlayEnabled = widget.options.autoPlay; + var autoPlayEnabled = widget.options.autoPlay; if (autoPlayEnabled && timer != null) return; @@ -193,7 +191,7 @@ class CarouselSliderState extends State gestures: { _MultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>( - () => _MultipleGestureRecognizer(), + _MultipleGestureRecognizer.new, (_MultipleGestureRecognizer instance) { instance.onStart = (_) { onStart(); @@ -204,9 +202,7 @@ class CarouselSliderState extends State instance.onEnd = (_) { onPanUp(); }; - instance.onCancel = () { - onPanUp(); - }; + instance.onCancel = onPanUp; }), }, child: NotificationListener( @@ -231,14 +227,19 @@ class CarouselSliderState extends State return Center(child: child); } - Widget getEnlargeWrapper(Widget? child, - {double? width, double? height, double? scale}) { + Widget getEnlargeWrapper( + Widget? child, { + double? width, + double? height, + double? scale, + }) { if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.height) { return SizedBox(width: width, height: height, child: child); } return Transform.scale( - scale: scale!, - child: SizedBox(width: width, height: height, child: child)); + scale: scale, + child: SizedBox(width: width, height: height, child: child), + ); } void onStart() { @@ -266,93 +267,110 @@ class CarouselSliderState extends State } @override - Widget build(BuildContext context) { - return getGestureWrapper(PageView.builder( - padEnds: widget.options.padEnds, - scrollBehavior: ScrollConfiguration.of(context).copyWith( - scrollbars: false, - overscroll: false, - dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, - ), - clipBehavior: widget.options.clipBehavior, - physics: widget.options.scrollPhysics, - scrollDirection: widget.options.scrollDirection, - pageSnapping: widget.options.pageSnapping, - controller: carouselState!.pageController, - reverse: widget.options.reverse, - itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount, - key: widget.options.pageViewKey, - onPageChanged: (int index) { - int currentPage = getRealIndex(index + carouselState!.initialPage, - carouselState!.realPage, widget.itemCount); - if (widget.options.onPageChanged != null) { - widget.options.onPageChanged!(currentPage, mode); - } - }, - itemBuilder: (BuildContext context, int idx) { - final int index = getRealIndex(idx + carouselState!.initialPage, - carouselState!.realPage, widget.itemCount); - - return AnimatedBuilder( - animation: carouselState!.pageController!, - child: (widget.items != null) - ? (widget.items!.isNotEmpty ? widget.items![index] : Container()) - : widget.itemBuilder!(context, index, idx), - builder: (BuildContext context, child) { - double distortionValue = 1.0; - // if [enlargeCenterPage] is true, we must calculate the carousel item's height - // to display the visual effect - - if (widget.options.enlargeCenterPage != null && - widget.options.enlargeCenterPage == true) { - // [pageController.page] can only be accessed after the first build, - // so in the first build we calculate the [itemOffset] manually - double itemOffset = 0; - var position = carouselState?.pageController?.position; - if (position != null && - position.hasPixels && - position.hasContentDimensions) { - var page = carouselState?.pageController?.page; - if (page != null) { - itemOffset = page - idx; - } - } else { - BuildContext storageContext = carouselState! - .pageController!.position.context.storageContext; - final double? previousSavedPosition = - PageStorage.of(storageContext).readState(storageContext) - as double?; - if (previousSavedPosition != null) { - itemOffset = previousSavedPosition - idx.toDouble(); - } else { - itemOffset = - carouselState!.realPage.toDouble() - idx.toDouble(); - } - } - - final num distortionRatio = - (1 - (itemOffset.abs() * 0.3)).clamp(0.0, 1.0); - distortionValue = - Curves.easeOut.transform(distortionRatio as double); - } - - final double height = widget.options.height ?? - MediaQuery.of(context).size.width * - (1 / widget.options.aspectRatio); - - if (widget.options.scrollDirection == Axis.horizontal) { - return getCenterWrapper(getEnlargeWrapper(child, - height: distortionValue * height, scale: distortionValue)); - } else { - return getCenterWrapper(getEnlargeWrapper(child, - width: distortionValue * MediaQuery.of(context).size.width, - scale: distortionValue)); - } + Widget build(BuildContext context) => getGestureWrapper( + PageView.builder( + padEnds: widget.options.padEnds, + scrollBehavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + overscroll: false, + dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, + ), + clipBehavior: widget.options.clipBehavior, + physics: widget.options.scrollPhysics, + scrollDirection: widget.options.scrollDirection, + pageSnapping: widget.options.pageSnapping, + controller: carouselState!.pageController, + reverse: widget.options.reverse, + itemCount: + widget.options.enableInfiniteScroll ? null : widget.itemCount, + key: widget.options.pageViewKey, + onPageChanged: (int index) { + var currentPage = getRealIndex( + index + carouselState!.initialPage, + carouselState!.realPage, + widget.itemCount, + ); + widget.options.onPageChanged?.call(currentPage, mode); }, - ); - }, - )); - } + itemBuilder: (BuildContext context, int idx) { + var index = getRealIndex( + idx + carouselState!.initialPage, + carouselState!.realPage, + widget.itemCount, + ); + + return AnimatedBuilder( + animation: carouselState!.pageController!, + child: (widget.items != null) + ? (widget.items!.isNotEmpty + ? widget.items![index] + : Container()) + : widget.itemBuilder!(context, index, idx), + builder: (BuildContext context, child) { + var distortionValue = 1.0; + // if [enlargeCenterPage] is true, we must calculate the + // carousel item's height + // to display the visual effect + + if (widget.options.enlargeCenterPage ?? false) { + // [pageController.page] can only be accessed after the + // first build, so in the first build we calculate the + // [itemOffset] manually + var itemOffset = 0.0; + var position = carouselState?.pageController?.position; + if (position != null && + position.hasPixels && + position.hasContentDimensions) { + var page = carouselState?.pageController?.page; + if (page != null) { + itemOffset = page - idx; + } + } else { + var storageContext = carouselState! + .pageController!.position.context.storageContext; + var previousSavedPosition = PageStorage.of(storageContext) + .readState(storageContext) as double?; + if (previousSavedPosition != null) { + itemOffset = previousSavedPosition - idx.toDouble(); + } else { + itemOffset = + carouselState!.realPage.toDouble() - idx.toDouble(); + } + } + + num distortionRatio = + (1 - (itemOffset.abs() * 0.3)).clamp(0.0, 1.0); + distortionValue = + Curves.easeOut.transform(distortionRatio as double); + } + + var height = widget.options.height ?? + MediaQuery.of(context).size.width * + (1 / widget.options.aspectRatio); + + if (widget.options.scrollDirection == Axis.horizontal) { + return getCenterWrapper( + getEnlargeWrapper( + child, + height: distortionValue * height, + scale: distortionValue, + ), + ); + } else { + return getCenterWrapper( + getEnlargeWrapper( + child, + width: + distortionValue * MediaQuery.of(context).size.width, + scale: distortionValue, + ), + ); + } + }, + ); + }, + ), + ); } class _MultipleGestureRecognizer extends PanGestureRecognizer {} diff --git a/lib/src/inputs/carousel/carousel_state.dart b/lib/src/inputs/carousel/carousel_state.dart index 6410931..b3681e5 100644 --- a/lib/src/inputs/carousel/carousel_state.dart +++ b/lib/src/inputs/carousel/carousel_state.dart @@ -3,9 +3,16 @@ // SPDX-License-Identifier: BSD-3-Clause import 'package:flutter/material.dart'; -import 'carousel_slider.dart'; +import 'package:flutter_input_library/src/inputs/carousel/carousel_slider.dart'; class CarouselState { + CarouselState( + this.options, + this.onResetTimer, + this.onResumeTimer, + this.changeMode, + ); + /// The [CarouselOptions] to create this state CarouselOptions options; @@ -30,16 +37,13 @@ class CarouselState { /// Will be called when using [pageController] to go to next page or /// previous page. It will clear the autoPlay timer. /// Internal use only - Function onResetTimer; + Function() onResetTimer; /// Will be called when using pageController to go to next page or /// previous page. It will restart the autoPlay timer. /// Internal use only - Function onResumeTimer; + Function() onResumeTimer; /// The callback to set the Reason Carousel changed Function(CarouselPageChangedReason) changeMode; - - CarouselState( - this.options, this.onResetTimer, this.onResumeTimer, this.changeMode); } diff --git a/lib/src/inputs/carousel/carousel_utils.dart b/lib/src/inputs/carousel/carousel_utils.dart index 7d96007..b0506dd 100644 --- a/lib/src/inputs/carousel/carousel_utils.dart +++ b/lib/src/inputs/carousel/carousel_utils.dart @@ -2,26 +2,27 @@ // // SPDX-License-Identifier: BSD-3-Clause -/// Converts an index of a set size to the corresponding index of a collection of another size -/// as if they were circular. +/// Converts an index of a set size to the corresponding index of a collection +/// of another size as if they were circular. /// -/// Takes a [position] from collection Foo, a [base] from where Foo's index originated -/// and the [length] of a second collection Baa, for which the correlating index is sought. +/// Takes a [position] from collection Foo, a [base] from where Foo's index +/// originated and the [length] of a second collection Baa, for which the +/// correlating index is sought. /// -/// For example; We have a Carousel of 10000(simulating infinity) but only 6 images. -/// We need to repeat the images to give the illusion of a never ending stream. -/// By calling [getRealIndex] with position and base we get an offset. -/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image -/// to be placed in the given position. +/// For example; We have a Carousel of 10000(simulating infinity) but only 6 +/// images. We need to repeat the images to give the illusion of a never +/// ending stream. By calling [getRealIndex] with position and base we get +/// an offset. This offset modulo our length, 6, will return a number between +/// 0 and 5, which represent the image to be placed in the given position. int getRealIndex(int position, int base, int? length) { - final int offset = position - base; + var offset = position - base; return remainder(offset, length); } -/// Returns the remainder of the modulo operation [input] % [source], and adjust it for -/// negative values. +/// Returns the remainder of the modulo operation [input] % [source], and adjust +/// it for negative values. int remainder(int input, int? source) { if (source == 0) return 0; - final int result = input % source!; + var result = input % source!; return result < 0 ? source + result : result; } diff --git a/lib/src/inputs/date_picker/date_picker.dart b/lib/src/inputs/date_picker/date_picker.dart index b041a39..86de82d 100644 --- a/lib/src/inputs/date_picker/date_picker.dart +++ b/lib/src/inputs/date_picker/date_picker.dart @@ -15,13 +15,13 @@ enum FlutterFormDateTimeType { class FlutterFormInputDateTime extends StatelessWidget { const FlutterFormInputDateTime({ - this.decoration, - this.style, - Key? key, - this.label, - this.showIcon = true, required this.inputType, required this.dateFormat, + this.decoration, + this.style, + super.key, + this.label, + this.showIcon = true, this.firstDate, this.lastDate, this.initialDate, @@ -36,9 +36,7 @@ class FlutterFormInputDateTime extends StatelessWidget { this.timePickerEntryMode = TimePickerEntryMode.dial, this.enabled = true, this.onTapEnabled = true, - }) : super( - key: key, - ); + }); final TextStyle? style; final InputDecoration? decoration; final Widget? label; @@ -61,28 +59,26 @@ class FlutterFormInputDateTime extends StatelessWidget { final bool onTapEnabled; @override - Widget build(BuildContext context) { - return DateTimeInputField( - style: style, - decoration: decoration, - autovalidateMode: autovalidateMode, - validator: validator, - label: label, - icon: icon, - firstDate: firstDate, - lastDate: lastDate, - inputType: inputType, - dateFormat: dateFormat, - initialDate: initialDate, - initialDateTimeRange: initialDateTimeRange, - initialTime: initialTime, - initialValue: initialValue, - onChanged: (value) => onChanged?.call(value), - onSaved: (value) => onSaved?.call(value), - showIcon: showIcon, - timePickerEntryMode: timePickerEntryMode, - enabled: enabled, - onTapEnabled: onTapEnabled, - ); - } + Widget build(BuildContext context) => DateTimeInputField( + style: style, + decoration: decoration, + autovalidateMode: autovalidateMode, + validator: validator, + label: label, + icon: icon, + firstDate: firstDate, + lastDate: lastDate, + inputType: inputType, + dateFormat: dateFormat, + initialDate: initialDate, + initialDateTimeRange: initialDateTimeRange, + initialTime: initialTime, + initialValue: initialValue, + onChanged: (value) => onChanged?.call(value), + onSaved: (value) => onSaved?.call(value), + showIcon: showIcon, + timePickerEntryMode: timePickerEntryMode, + enabled: enabled, + onTapEnabled: onTapEnabled, + ); } diff --git a/lib/src/inputs/date_picker/date_picker_field.dart b/lib/src/inputs/date_picker/date_picker_field.dart index b430339..b614d8e 100644 --- a/lib/src/inputs/date_picker/date_picker_field.dart +++ b/lib/src/inputs/date_picker/date_picker_field.dart @@ -10,16 +10,18 @@ import 'package:intl/intl.dart'; class DateTimeInputField extends StatefulWidget { const DateTimeInputField({ - this.decoration, - Key? key, required this.inputType, required this.autovalidateMode, - this.label, - this.showIcon = true, - this.icon, required this.dateFormat, required this.firstDate, required this.lastDate, + required this.timePickerEntryMode, + required this.style, + this.decoration, + super.key, + this.label, + this.showIcon = true, + this.icon, this.initialDate, this.initialTime, this.initialDateTimeRange, @@ -27,13 +29,9 @@ class DateTimeInputField extends StatefulWidget { this.onChanged, this.onSaved, this.validator, - required this.timePickerEntryMode, - required this.style, this.enabled = true, this.onTapEnabled = true, - }) : super( - key: key, - ); + }); final TextStyle? style; final InputDecoration? decoration; final AutovalidateMode autovalidateMode; @@ -93,12 +91,14 @@ class _DateInputFieldState extends State { @override Widget build(BuildContext context) { - Future getInputFromUser(FlutterFormDateTimeType inputType, - [DateFormat? dateFormat]) async { - String userInput = ''; + Future getInputFromUser( + FlutterFormDateTimeType inputType, [ + DateFormat? dateFormat, + ]) async { + var userInput = ''; switch (inputType) { case FlutterFormDateTimeType.date: - DateTime? unformatted = await showDatePicker( + var unformatted = await showDatePicker( initialDate: initialDate, context: context, firstDate: firstDate, @@ -112,7 +112,7 @@ class _DateInputFieldState extends State { await getInputFromUser(FlutterFormDateTimeType.date) .then((value) async { if (value != '') { - String secondInput = + var secondInput = await getInputFromUser(FlutterFormDateTimeType.time); if (secondInput != '') { var date = widget.dateFormat.parse(value); @@ -120,49 +120,51 @@ class _DateInputFieldState extends State { ? dateFormat.parse('01 01 1970 $secondInput') : DateFormat('dd MM yyyy HH:mm') .parse('01 01 1970 $secondInput'); - userInput = widget.dateFormat.format(DateTime( - date.year, - date.month, - date.day, - time.hour, - time.minute, - )); + userInput = widget.dateFormat.format( + DateTime( + date.year, + date.month, + date.day, + time.hour, + time.minute, + ), + ); } } }); break; case FlutterFormDateTimeType.range: if (context.mounted) { - userInput = (await showDateRangePicker( - context: context, - firstDate: firstDate, - lastDate: lastDate, - initialDateRange: initialDateRange) - .then((value) { - return value != null - ? '${widget.dateFormat.format(value.start)} - ${widget.dateFormat.format(value.end)}' - : ''; - })) - .toString(); + userInput = await showDateRangePicker( + context: context, + firstDate: firstDate, + lastDate: lastDate, + initialDateRange: initialDateRange, + ).then( + (value) => value != null + ? '${widget.dateFormat.format(value.start)} -' + '${widget.dateFormat.format(value.end)}' + : '', + ); } break; case FlutterFormDateTimeType.time: if (context.mounted) { userInput = await showTimePicker( initialEntryMode: widget.timePickerEntryMode, - builder: (BuildContext context, Widget? child) { - return MediaQuery( - data: MediaQuery.of(context) - .copyWith(alwaysUse24HourFormat: true), - child: child!, - ); - }, + builder: (BuildContext context, Widget? child) => MediaQuery( + data: MediaQuery.of(context) + .copyWith(alwaysUse24HourFormat: true), + child: child!, + ), context: context, initialTime: initialTimeOfDay, - ).then((value) => value == null - ? '' - : MaterialLocalizations.of(context) - .formatTimeOfDay(value, alwaysUse24HourFormat: true)); + ).then( + (value) => value == null + ? '' + : MaterialLocalizations.of(context) + .formatTimeOfDay(value, alwaysUse24HourFormat: true), + ); } } return userInput; @@ -178,7 +180,7 @@ class _DateInputFieldState extends State { onSaved: (value) => widget.onSaved?.call(value), onTap: widget.onTapEnabled ? () async { - String userInput = await getInputFromUser( + var userInput = await getInputFromUser( widget.inputType, DateFormat('dd MM yyyy HH:mm'), ); @@ -194,7 +196,7 @@ class _DateInputFieldState extends State { InputDecoration( suffixIcon: widget.showIcon ? Icon(widget.icon) : null, focusColor: Theme.of(context).primaryColor, - label: widget.label ?? const Text("Date"), + label: widget.label ?? const Text('Date'), ), enabled: widget.enabled, ); diff --git a/lib/src/inputs/inputs.dart b/lib/src/inputs/inputs.dart index 9f74a31..16759bd 100644 --- a/lib/src/inputs/inputs.dart +++ b/lib/src/inputs/inputs.dart @@ -1,8 +1,8 @@ export 'carousel/carousel.dart'; +export 'date_picker/date_picker.dart'; export 'number_picker/number_picker.dart'; -export 'text/password.dart'; -export 'text/plain_text.dart'; +export 'scroll_picker/scroll_picker.dart'; export 'slider/slider.dart'; export 'switch/switch.dart'; -export 'date_picker/date_picker.dart'; -export 'scroll_picker/scroll_picker.dart'; +export 'text/password.dart'; +export 'text/plain_text.dart'; diff --git a/lib/src/inputs/number_picker/decimal_numberpicker.dart b/lib/src/inputs/number_picker/decimal_numberpicker.dart index 57fb2de..dde8d7f 100644 --- a/lib/src/inputs/number_picker/decimal_numberpicker.dart +++ b/lib/src/inputs/number_picker/decimal_numberpicker.dart @@ -6,9 +6,30 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'number_picker_field.dart'; +import 'package:flutter_input_library/src/inputs/number_picker/number_picker_field.dart'; class DecimalNumberPicker extends StatelessWidget { + const DecimalNumberPicker({ + required this.minValue, + required this.maxValue, + required this.value, + required this.onChanged, + super.key, + this.itemCount = 3, + this.itemHeight = 50, + this.itemWidth = 100, + this.axis = Axis.vertical, + this.textStyle, + this.selectedTextStyle, + this.haptics = false, + this.decimalPlaces = 1, + this.integerTextMapper, + this.decimalTextMapper, + this.integerZeroPad = false, + this.integerDecoration, + this.decimalDecoration, + }) : assert(minValue <= value, 'value must be greater than minValue'), + assert(value <= maxValue, 'value must be less than maxValue'); final int minValue; final int maxValue; final double value; @@ -24,47 +45,26 @@ class DecimalNumberPicker extends StatelessWidget { final TextMapper? decimalTextMapper; final bool integerZeroPad; - /// Decoration to apply to central box where the selected integer value is placed + /// Decoration to apply to central box where the selected integer value + /// is placed final Decoration? integerDecoration; - /// Decoration to apply to central box where the selected decimal value is placed + /// Decoration to apply to central box where the selected decimal value + /// is placed final Decoration? decimalDecoration; /// Inidcates how many decimal places to show /// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...] final int decimalPlaces; - const DecimalNumberPicker({ - Key? key, - required this.minValue, - required this.maxValue, - required this.value, - required this.onChanged, - this.itemCount = 3, - this.itemHeight = 50, - this.itemWidth = 100, - this.axis = Axis.vertical, - this.textStyle, - this.selectedTextStyle, - this.haptics = false, - this.decimalPlaces = 1, - this.integerTextMapper, - this.decimalTextMapper, - this.integerZeroPad = false, - this.integerDecoration, - this.decimalDecoration, - }) : assert(minValue <= value), - assert(value <= maxValue), - super(key: key); - @override Widget build(BuildContext context) { - final isMax = value.floor() == maxValue; - final decimalValue = isMax + var isMax = value.floor() == maxValue; + var decimalValue = isMax ? 0 : ((value - value.floorToDouble()) * math.pow(10, decimalPlaces)) .round(); - final doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1; + var doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -102,15 +102,15 @@ class DecimalNumberPicker extends StatelessWidget { } void _onIntChanged(int intValue) { - final newValue = - (value - value.floor() + intValue).clamp(minValue, maxValue); + var newValue = (value - value.floor() + intValue).clamp(minValue, maxValue); onChanged(newValue.toDouble()); } void _onDoubleChanged(int doubleValue) { - final decimalPart = double.parse( - (doubleValue * math.pow(10, -decimalPlaces)) - .toStringAsFixed(decimalPlaces)); + var decimalPart = double.parse( + (doubleValue * math.pow(10, -decimalPlaces)) + .toStringAsFixed(decimalPlaces), + ); onChanged(value.floor() + decimalPart); } } diff --git a/lib/src/inputs/number_picker/infinite_listview.dart b/lib/src/inputs/number_picker/infinite_listview.dart index 2d1d9ab..30e8dd3 100644 --- a/lib/src/inputs/number_picker/infinite_listview.dart +++ b/lib/src/inputs/number_picker/infinite_listview.dart @@ -2,8 +2,6 @@ // // SPDX-License-Identifier: BSD-3-Clause -library infinite_listview; - import 'dart:math' as math; import 'package:flutter/gestures.dart' show DragStartBehavior; @@ -18,14 +16,14 @@ import 'package:flutter/widgets.dart'; class InfiniteListView extends StatefulWidget { /// See [ListView.builder] const InfiniteListView.builder({ - Key? key, + required this.itemBuilder, + super.key, this.scrollDirection = Axis.vertical, this.reverse = false, this.controller, this.physics, this.padding, this.itemExtent, - required this.itemBuilder, this.itemCount, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, @@ -36,19 +34,18 @@ class InfiniteListView extends StatefulWidget { this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, this.clipBehavior = Clip.hardEdge, - }) : separatorBuilder = null, - super(key: key); + }) : separatorBuilder = null; /// See [ListView.separated] const InfiniteListView.separated({ - Key? key, + required this.itemBuilder, + required this.separatorBuilder, + super.key, this.scrollDirection = Axis.vertical, this.reverse = false, this.controller, this.physics, this.padding, - required this.itemBuilder, - required this.separatorBuilder, this.itemCount, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, @@ -59,8 +56,7 @@ class InfiniteListView extends StatefulWidget { this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, this.clipBehavior = Clip.hardEdge, - }) : itemExtent = null, - super(key: key); + }) : itemExtent = null; /// See: [ScrollView.scrollDirection] final Axis scrollDirection; @@ -153,20 +149,20 @@ class InfiniteListViewState extends State { @override Widget build(BuildContext context) { - final List slivers = _buildSlivers(context, negative: false); - final List negativeSlivers = _buildSlivers(context, negative: true); - final AxisDirection axisDirection = _getDirection(context); - final scrollPhysics = - widget.physics ?? const AlwaysScrollableScrollPhysics(); + var slivers = _buildSlivers(context, negative: false); + var negativeSlivers = _buildSlivers(context, negative: true); + var axisDirection = _getDirection(context); + var scrollPhysics = widget.physics ?? const AlwaysScrollableScrollPhysics(); return Scrollable( axisDirection: axisDirection, controller: _effectiveController, physics: scrollPhysics, - viewportBuilder: (BuildContext context, ViewportOffset offset) { - return Builder(builder: (BuildContext context) { - /// Build negative [ScrollPosition] for the negative scrolling [Viewport]. - final state = Scrollable.of(context); - final negativeOffset = _InfiniteScrollPosition( + viewportBuilder: (BuildContext context, ViewportOffset offset) => Builder( + builder: (BuildContext context) { + /// Build negative [ScrollPosition] for the negative scrolling + /// [Viewport]. + var state = Scrollable.of(context); + var negativeOffset = _InfiniteScrollPosition( physics: scrollPhysics, context: state, initialPixels: -offset.pixels, @@ -174,12 +170,14 @@ class InfiniteListViewState extends State { negativeScroll: true, ); - /// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition]. + /// Keep the negative scrolling [Viewport] positioned to the + /// [ScrollPosition]. offset.addListener(() { negativeOffset._forceNegativePixels(offset.pixels); }); - /// Stack the two [Viewport]s on top of each other so they move in sync. + /// Stack the two [Viewport]s on top of each other so they move in + /// sync. return Stack( children: [ Viewport( @@ -198,19 +196,21 @@ class InfiniteListViewState extends State { ), ], ); - }); - }, + }, + ), ); } - AxisDirection _getDirection(BuildContext context) { - return getAxisDirectionFromAxisReverseAndDirectionality( - context, widget.scrollDirection, widget.reverse); - } + AxisDirection _getDirection(BuildContext context) => + getAxisDirectionFromAxisReverseAndDirectionality( + context, + widget.scrollDirection, + widget.reverse, + ); List _buildSlivers(BuildContext context, {bool negative = false}) { - final itemExtent = widget.itemExtent; - final padding = widget.padding ?? EdgeInsets.zero; + var itemExtent = widget.itemExtent; + var padding = widget.padding ?? EdgeInsets.zero; return [ SliverPadding( padding: negative @@ -228,36 +228,35 @@ class InfiniteListViewState extends State { ? negativeChildrenDelegate : positiveChildrenDelegate, ), - ) + ), ]; } - SliverChildDelegate get negativeChildrenDelegate { - return SliverChildBuilderDelegate( - (BuildContext context, int index) { - final separatorBuilder = widget.separatorBuilder; - if (separatorBuilder != null) { - final itemIndex = (-1 - index) ~/ 2; - return index.isOdd - ? widget.itemBuilder(context, itemIndex) - : separatorBuilder(context, itemIndex); - } else { - return widget.itemBuilder(context, -1 - index); - } - }, - childCount: widget.itemCount, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - addRepaintBoundaries: widget.addRepaintBoundaries, - ); - } + SliverChildDelegate get negativeChildrenDelegate => + SliverChildBuilderDelegate( + (BuildContext context, int index) { + var separatorBuilder = widget.separatorBuilder; + if (separatorBuilder != null) { + var itemIndex = (-1 - index) ~/ 2; + return index.isOdd + ? widget.itemBuilder(context, itemIndex) + : separatorBuilder(context, itemIndex); + } else { + return widget.itemBuilder(context, -1 - index); + } + }, + childCount: widget.itemCount, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + ); SliverChildDelegate get positiveChildrenDelegate { - final separatorBuilder = widget.separatorBuilder; - final itemCount = widget.itemCount; + var separatorBuilder = widget.separatorBuilder; + var itemCount = widget.itemCount; return SliverChildBuilderDelegate( (separatorBuilder != null) ? (BuildContext context, int index) { - final itemIndex = index ~/ 2; + var itemIndex = index ~/ 2; return index.isEven ? widget.itemBuilder(context, itemIndex) : separatorBuilder(context, itemIndex); @@ -276,67 +275,82 @@ class InfiniteListViewState extends State { super.debugFillProperties(properties); properties .add(EnumProperty('scrollDirection', widget.scrollDirection)); - properties.add(FlagProperty('reverse', - value: widget.reverse, ifTrue: 'reversed', showName: true)); - properties.add(DiagnosticsProperty( - 'controller', widget.controller, - showName: false, defaultValue: null)); - properties.add(DiagnosticsProperty('physics', widget.physics, - showName: false, defaultValue: null)); - properties.add(DiagnosticsProperty( - 'padding', widget.padding, - defaultValue: null)); properties.add( - DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null)); + FlagProperty( + 'reverse', + value: widget.reverse, + ifTrue: 'reversed', + showName: true, + ), + ); properties.add( - DoubleProperty('cacheExtent', widget.cacheExtent, defaultValue: null)); - } -} - -/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds. -class InfiniteScrollController extends ScrollController { - /// Creates a new [InfiniteScrollController] - InfiniteScrollController({ - double initialScrollOffset = 0.0, - bool keepScrollOffset = true, - String? debugLabel, - }) : super( - initialScrollOffset: initialScrollOffset, - keepScrollOffset: keepScrollOffset, - debugLabel: debugLabel, - ); - - @override - ScrollPosition createScrollPosition(ScrollPhysics physics, - ScrollContext context, ScrollPosition? oldPosition) { - return _InfiniteScrollPosition( - physics: physics, - context: context, - initialPixels: initialScrollOffset, - keepScrollOffset: keepScrollOffset, - oldPosition: oldPosition, - debugLabel: debugLabel, + DiagnosticsProperty( + 'controller', + widget.controller, + showName: false, + defaultValue: null, + ), + ); + properties.add( + DiagnosticsProperty( + 'physics', + widget.physics, + showName: false, + defaultValue: null, + ), + ); + properties.add( + DiagnosticsProperty( + 'padding', + widget.padding, + defaultValue: null, + ), + ); + properties.add( + DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null), + ); + properties.add( + DoubleProperty('cacheExtent', widget.cacheExtent, defaultValue: null), ); } } +/// Same as a [ScrollController] except it provides [ScrollPosition] objects +/// with infinite bounds. +class InfiniteScrollController extends ScrollController { + /// Creates a new [InfiniteScrollController] + InfiniteScrollController({ + super.initialScrollOffset, + super.keepScrollOffset, + super.debugLabel, + }); + + @override + ScrollPosition createScrollPosition( + ScrollPhysics physics, + ScrollContext context, + ScrollPosition? oldPosition, + ) => + _InfiniteScrollPosition( + physics: physics, + context: context, + initialPixels: initialScrollOffset, + keepScrollOffset: keepScrollOffset, + oldPosition: oldPosition, + debugLabel: debugLabel, + ); +} + class _InfiniteScrollPosition extends ScrollPositionWithSingleContext { _InfiniteScrollPosition({ - required ScrollPhysics physics, - required ScrollContext context, - double? initialPixels = 0.0, - bool keepScrollOffset = true, - ScrollPosition? oldPosition, - String? debugLabel, + required super.physics, + required super.context, + super.initialPixels, + super.keepScrollOffset, + super.oldPosition, + super.debugLabel, this.negativeScroll = false, - }) : super( - physics: physics, - context: context, - initialPixels: initialPixels, - keepScrollOffset: keepScrollOffset, - oldPosition: oldPosition, - debugLabel: debugLabel, - ); + }); final bool negativeScroll; diff --git a/lib/src/inputs/number_picker/number_picker.dart b/lib/src/inputs/number_picker/number_picker.dart index dce1ffd..67fc8d0 100644 --- a/lib/src/inputs/number_picker/number_picker.dart +++ b/lib/src/inputs/number_picker/number_picker.dart @@ -4,22 +4,18 @@ import 'package:flutter/material.dart'; -import 'number_picker_field.dart'; +import 'package:flutter_input_library/src/inputs/number_picker/number_picker_field.dart'; class FlutterFormInputNumberPicker extends StatelessWidget { const FlutterFormInputNumberPicker({ - Key? key, - Widget? label, + super.key, this.minValue = 0, this.maxValue = 100, this.onSaved, this.onChanged, this.initialValue, this.validator, - }) : assert(minValue < maxValue), - super( - key: key, - ); + }) : assert(minValue < maxValue, 'minValue must be less than maxValue'); final int minValue; final int maxValue; @@ -29,45 +25,37 @@ class FlutterFormInputNumberPicker extends StatelessWidget { final Function(int?)? onChanged; @override - Widget build(BuildContext context) { - return NumberPickerFormField( - minValue: minValue, - maxValue: maxValue, - onSaved: (value) => onSaved?.call(value), - validator: (value) => validator?.call(value), - onChanged: (value) => onChanged?.call(value), - initialValue: initialValue ?? 0, - ); - } + Widget build(BuildContext context) => NumberPickerFormField( + minValue: minValue, + maxValue: maxValue, + onSaved: (value) => onSaved?.call(value), + validator: (value) => validator?.call(value), + onChanged: (value) => onChanged?.call(value), + initialValue: initialValue ?? 0, + ); } class NumberPickerFormField extends FormField { NumberPickerFormField({ - Key? key, - required FormFieldSetter onSaved, - required FormFieldValidator validator, + required FormFieldSetter super.onSaved, + required FormFieldValidator super.validator, + super.key, void Function(int value)? onChanged, - int initialValue = 0, - bool autovalidate = false, + int super.initialValue = 0, int minValue = 0, int maxValue = 100, }) : super( - key: key, - onSaved: onSaved, - validator: validator, - initialValue: initialValue, - builder: (FormFieldState state) { - return NumberPicker( - minValue: minValue, - maxValue: maxValue, - value: initialValue, - onChanged: (int value) { - onChanged?.call(value); + builder: (FormFieldState state) => NumberPicker( + minValue: minValue, + maxValue: maxValue, + value: initialValue, + onChanged: (int value) { + onChanged?.call(value); - state.didChange(value); - }, - itemHeight: 35, - itemCount: 5, - ); - }); + state.didChange(value); + }, + itemHeight: 35, + itemCount: 5, + ), + ); } diff --git a/lib/src/inputs/number_picker/number_picker_field.dart b/lib/src/inputs/number_picker/number_picker_field.dart index e77247f..2b8d854 100644 --- a/lib/src/inputs/number_picker/number_picker_field.dart +++ b/lib/src/inputs/number_picker/number_picker_field.dart @@ -2,13 +2,36 @@ // // SPDX-License-Identifier: BSD-3-Clause +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'infinite_listview.dart'; +import 'package:flutter_input_library/src/inputs/number_picker/infinite_listview.dart'; typedef TextMapper = String Function(String numberText); class NumberPicker extends StatefulWidget { + const NumberPicker({ + required this.minValue, + required this.maxValue, + required this.value, + required this.onChanged, + super.key, + this.itemCount = 3, + this.step = 1, + this.itemHeight = 50, + this.itemWidth = 100, + this.axis = Axis.vertical, + this.textStyle, + this.selectedTextStyle, + this.haptics = false, + this.decoration, + this.zeroPad = false, + this.textMapper, + this.infiniteLoop = false, + }) : assert(minValue <= value, 'value must be greater than minValue'), + assert(value <= maxValue, 'value must be less than maxValue'); + /// Min value user can pick final int minValue; @@ -43,7 +66,8 @@ class NumberPicker extends StatefulWidget { /// Style of non-selected numbers. If null, it uses Theme's bodyText2 final TextStyle? textStyle; - /// Style of non-selected numbers. If null, it uses Theme's headline5 with accentColor + /// Style of non-selected numbers. If null, it uses Theme's headline5 with + /// accentColor final TextStyle? selectedTextStyle; /// Whether to trigger haptic pulses or not @@ -60,28 +84,6 @@ class NumberPicker extends StatefulWidget { final bool infiniteLoop; - const NumberPicker({ - Key? key, - required this.minValue, - required this.maxValue, - required this.value, - required this.onChanged, - this.itemCount = 3, - this.step = 1, - this.itemHeight = 50, - this.itemWidth = 100, - this.axis = Axis.vertical, - this.textStyle, - this.selectedTextStyle, - this.haptics = false, - this.decoration, - this.zeroPad = false, - this.textMapper, - this.infiniteLoop = false, - }) : assert(minValue <= value), - assert(value <= maxValue), - super(key: key); - @override NumberPickerState createState() => NumberPickerState(); } @@ -97,7 +99,7 @@ class NumberPickerState extends State { value = widget.value; - final initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent; + var initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent; if (widget.infiniteLoop) { _scrollController = InfiniteScrollController(initialScrollOffset: initialOffset); @@ -107,14 +109,14 @@ class NumberPickerState extends State { _scrollController.addListener(_scrollListener); } - void _scrollListener() { + Future _scrollListener() async { var indexOfMiddleElement = (_scrollController.offset / itemExtent).round(); if (widget.infiniteLoop) { indexOfMiddleElement %= itemCount; } else { indexOfMiddleElement = indexOfMiddleElement.clamp(0, itemCount - 1); } - final intValueInTheMiddle = + var intValueInTheMiddle = _intValueFromIndex(indexOfMiddleElement + additionalItemsOnEachSide); if (value != intValueInTheMiddle) { @@ -124,12 +126,12 @@ class NumberPickerState extends State { widget.onChanged(intValueInTheMiddle); if (widget.haptics) { - HapticFeedback.selectionClick(); + await HapticFeedback.selectionClick(); } } Future.delayed( const Duration(milliseconds: 100), - () => _maybeCenterValue(), + _maybeCenterValue, ); } @@ -137,7 +139,7 @@ class NumberPickerState extends State { void didUpdateWidget(NumberPicker oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.value != value) { - _maybeCenterValue(); + unawaited(_maybeCenterValue()); } } @@ -159,78 +161,77 @@ class NumberPickerState extends State { int get additionalItemsOnEachSide => (widget.itemCount - 1) ~/ 2; @override - Widget build(BuildContext context) { - return SizedBox( - width: widget.axis == Axis.vertical - ? widget.itemWidth - : widget.itemCount * widget.itemWidth, - height: widget.axis == Axis.vertical - ? widget.itemCount * widget.itemHeight - : widget.itemHeight, - child: NotificationListener( - onNotification: (not) { - if (not.dragDetails?.primaryVelocity == 0) { - Future.microtask(() => _maybeCenterValue()); - } - return true; - }, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: Stack( - children: [ - Center( - child: Container( - width: 300, - height: 45, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: const Color(0xFFD8D8D8).withOpacity(0.50), + Widget build(BuildContext context) => SizedBox( + width: widget.axis == Axis.vertical + ? widget.itemWidth + : widget.itemCount * widget.itemWidth, + height: widget.axis == Axis.vertical + ? widget.itemCount * widget.itemHeight + : widget.itemHeight, + child: NotificationListener( + onNotification: (not) { + if (not.dragDetails?.primaryVelocity == 0) { + Future.microtask(_maybeCenterValue); + } + return true; + }, + child: ScrollConfiguration( + behavior: + ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: Stack( + children: [ + Center( + child: Container( + width: 300, + height: 45, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: const Color(0xFFD8D8D8).withOpacity(0.50), + ), ), ), - ), - if (widget.infiniteLoop) - InfiniteListView.builder( - scrollDirection: widget.axis, - controller: _scrollController as InfiniteScrollController, + if (widget.infiniteLoop) + InfiniteListView.builder( + scrollDirection: widget.axis, + controller: _scrollController as InfiniteScrollController, + itemExtent: itemExtent, + itemBuilder: _itemBuilder, + padding: EdgeInsets.zero, + ) + else + ListView.builder( + itemCount: listItemsCount, + scrollDirection: widget.axis, + controller: _scrollController, + itemExtent: itemExtent, + itemBuilder: _itemBuilder, + padding: EdgeInsets.zero, + ), + _NumberPickerSelectedItemDecoration( + axis: widget.axis, itemExtent: itemExtent, - itemBuilder: _itemBuilder, - padding: EdgeInsets.zero, - ) - else - ListView.builder( - itemCount: listItemsCount, - scrollDirection: widget.axis, - controller: _scrollController, - itemExtent: itemExtent, - itemBuilder: _itemBuilder, - padding: EdgeInsets.zero, + decoration: widget.decoration, ), - _NumberPickerSelectedItemDecoration( - axis: widget.axis, - itemExtent: itemExtent, - decoration: widget.decoration, - ), - ], + ], + ), ), ), - ), - ); - } + ); Widget _itemBuilder(BuildContext context, int index) { - final themeData = Theme.of(context); - final defaultStyle = widget.textStyle ?? themeData.textTheme.bodyMedium; - final selectedStyle = widget.selectedTextStyle ?? + var themeData = Theme.of(context); + var defaultStyle = widget.textStyle ?? themeData.textTheme.bodyMedium; + var selectedStyle = widget.selectedTextStyle ?? themeData.textTheme.headlineSmall ?.copyWith(color: themeData.highlightColor); - final valueFromIndex = _intValueFromIndex(index % itemCount); - final isExtra = !widget.infiniteLoop && + var valueFromIndex = _intValueFromIndex(index % itemCount); + var isExtra = !widget.infiniteLoop && (index < additionalItemsOnEachSide || index >= listItemsCount - additionalItemsOnEachSide); - final itemStyle = valueFromIndex == value ? selectedStyle : defaultStyle; + var itemStyle = valueFromIndex == value ? selectedStyle : defaultStyle; - final child = isExtra + var child = isExtra ? const SizedBox.shrink() : Text( _getDisplayedValue(valueFromIndex), @@ -246,7 +247,7 @@ class NumberPickerState extends State { } String _getDisplayedValue(int value) { - final text = widget.zeroPad + var text = widget.zeroPad ? value.toString().padLeft(widget.maxValue.toString().length, '0') : value.toString(); if (widget.textMapper != null) { @@ -257,21 +258,22 @@ class NumberPickerState extends State { } int _intValueFromIndex(int index) { - index -= additionalItemsOnEachSide; - index %= itemCount; - return widget.minValue + index * widget.step; + var newIndex = index; + newIndex -= additionalItemsOnEachSide; + newIndex %= itemCount; + return widget.minValue + newIndex * widget.step; } - void _maybeCenterValue() { + Future _maybeCenterValue() async { if (_scrollController.hasClients && !isScrolling) { - int diff = value - widget.minValue; - int index = diff ~/ widget.step; + var diff = value - widget.minValue; + var index = diff ~/ widget.step; if (widget.infiniteLoop) { - final offset = _scrollController.offset + 0.5 * itemExtent; - final cycles = (offset / (itemCount * itemExtent)).floor(); + var offset = _scrollController.offset + 0.5 * itemExtent; + var cycles = (offset / (itemCount * itemExtent)).floor(); index += cycles * itemCount; } - _scrollController.animateTo( + await _scrollController.animateTo( index * itemExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, @@ -281,29 +283,25 @@ class NumberPickerState extends State { } class _NumberPickerSelectedItemDecoration extends StatelessWidget { + const _NumberPickerSelectedItemDecoration({ + required this.axis, + required this.itemExtent, + required this.decoration, + }); final Axis axis; final double itemExtent; final Decoration? decoration; - const _NumberPickerSelectedItemDecoration({ - Key? key, - required this.axis, - required this.itemExtent, - required this.decoration, - }) : super(key: key); - @override - Widget build(BuildContext context) { - return Center( - child: IgnorePointer( - child: Container( - width: isVertical ? double.infinity : itemExtent, - height: isVertical ? itemExtent : double.infinity, - decoration: decoration, + Widget build(BuildContext context) => Center( + child: IgnorePointer( + child: Container( + width: isVertical ? double.infinity : itemExtent, + height: isVertical ? itemExtent : double.infinity, + decoration: decoration, + ), ), - ), - ); - } + ); bool get isVertical => axis == Axis.vertical; } diff --git a/lib/src/inputs/scroll_picker/scroll_picker.dart b/lib/src/inputs/scroll_picker/scroll_picker.dart index 6be9f3f..d11b622 100644 --- a/lib/src/inputs/scroll_picker/scroll_picker.dart +++ b/lib/src/inputs/scroll_picker/scroll_picker.dart @@ -1,3 +1,3 @@ -export 'scroll_picker_widget.dart'; export 'scroll_picker_decoration.dart'; export 'scroll_picker_type_extensions.dart'; +export 'scroll_picker_widget.dart'; diff --git a/lib/src/inputs/scroll_picker/scroll_picker_decoration.dart b/lib/src/inputs/scroll_picker/scroll_picker_decoration.dart index bbe4b1b..cd94d7a 100644 --- a/lib/src/inputs/scroll_picker/scroll_picker_decoration.dart +++ b/lib/src/inputs/scroll_picker/scroll_picker_decoration.dart @@ -22,57 +22,87 @@ class ScrollPickerDecoration { }); /// Ability to provide your own builder for the scroll items + // ignore: avoid_annotating_with_dynamic final Widget Function(BuildContext context, int index, dynamic value)? scrollItemBuilder; - /// Override the standard highlight widget. (Grey container which is placed behind the selected item). + /// Override the standard highlight widget. (Grey container which is placed + /// behind the selected item). final Widget? highlightWidget; - /// Textstyle of the scroll items. Will be overridden if the [scrollItemBuilder] is set. + /// Textstyle of the scroll items. Will be overridden if the + /// [scrollItemBuilder] is set. final TextStyle? scrollItemTextStyle; - /// Amount of visible items in the scroll wheel. Changing this changes the height of the widget. + /// Amount of visible items in the scroll wheel. Changing this changes the + /// height of the widget. final int numberOfVisibleItems; - /// Height of each item in the scrollwheel. Chaging this changes the height of the widget. + /// Height of each item in the scrollwheel. Chaging this changes the height + /// of the widget. final double itemHeight; - /// A ratio between the diameter of the cylinder and the viewport's size in the main axis. + /// A ratio between the diameter of the cylinder and the viewport's size in + /// the main axis. /// - /// A value of 1 means the cylinder has the same diameter as the viewport's size. + /// A value of 1 means the cylinder has the same diameter as the viewport's + /// size. /// - /// A value smaller than 1 means items at the edges of the cylinder are entirely contained inside the viewport. + /// A value smaller than 1 means items at the edges of the cylinder are + /// entirely contained inside the viewport. /// - /// A value larger than 1 means angles less than ±[pi] / 2 from the center of the cylinder are visible. + /// A value larger than 1 means angles less than ±[pi] / 2 from the center + /// of the cylinder are visible. /// - /// The same number of children will be visible in the viewport regardless of the [diameterRatio]. The number of children visible is based on the viewport's length along the main axis divided by the children's [itemExtent]. Then the children are evenly distributed along the visible angles up to ±[pi] / 2. + /// The same number of children will be visible in the viewport regardless of + /// the [diameterRatio]. The number of children visible is based on the + /// viewport's length along the main axis divided by the children's + /// [itemExtent]. Then the children are evenly distributed along the visible angles up to ±[pi] / 2. /// - /// Just as it's impossible to stretch a paper to cover the an entire half of a cylinder's surface where the cylinder has the same diameter as the paper's length, choosing a [diameterRatio] smaller than [pi] will leave same gaps between the children. + /// Just as it's impossible to stretch a paper to cover the an entire half of + /// a cylinder's surface where the cylinder has the same diameter as the + /// paper's length, choosing a [diameterRatio] smaller than [pi] will leave + /// same gaps between the children. /// /// Defaults to an arbitrary but aesthetically reasonable number of 2.0. /// /// Must not be null and must be positive. final double diameterRatio; - /// Size of each child in the main axis. Must not be null and must be positive. + /// Size of each child in the main axis. Must not be null and + /// must be positive. final double perspective; - /// The opacity value that will be applied to the wheel that appears below and above the magnifier. + /// The opacity value that will be applied to the wheel that appears below + /// and above the magnifier. /// /// The default value is 1.0, which will not change anything. /// /// Must be greater than or equal to 0, and less than or equal to 1. final double overAndUnderCenterOpacity; - /// How much the wheel is horizontally off-center, as a fraction of its width. This property creates the visual effect of looking at a vertical wheel from its side where its vanishing points at the edge curves to one side instead of looking at the wheel head-on. + /// How much the wheel is horizontally off-center, as a fraction of its width. + /// This property creates the visual effect of looking at a vertical wheel + /// from its side where its vanishing points at the edge curves to one side + /// instead of looking at the wheel head-on. /// - /// The value is horizontal distance between the wheel's center and the vertical vanishing line at the edges of the wheel, represented as a fraction of the wheel's width. + /// The value is horizontal distance between the wheel's center and the + /// vertical vanishing line at the edges of the wheel, represented as a + /// fraction of the wheel's width. /// - /// The value 0.0 means the wheel is looked at head-on and its vanishing line runs through the center of the wheel. Negative values means moving the wheel to the left of the observer, thus the edges curve to the right. Positive values means moving the wheel to the right of the observer, thus the edges curve to the left. + /// The value 0.0 means the wheel is looked at head-on and its vanishing line + /// runs through the center of the wheel. Negative values means moving the + /// wheel to the left of the observer, thus the edges curve to the right. + /// Positive values means moving the wheel to the right of the observer, + /// thus the edges curve to the left. /// - /// The visual effect causes the wheel's edges to curve rather than moving the center. So a value of 0.5 means the edges' vanishing line will touch the wheel's size's left edge. + /// The visual effect causes the wheel's edges to curve rather than moving + /// the center. So a value of 0.5 means the edges' vanishing line will touch + /// the wheel's size's left edge. /// - /// Defaults to 0.0, which means looking at the wheel head-on. The visual effect can be unaesthetic if this value is too far from the range [-0.5, 0.5]. + /// Defaults to 0.0, which means looking at the wheel head-on. The visual + /// effect can be unaesthetic if this value is too far from the range + /// [-0.5, 0.5]. final double offAxisFraction; /// Whether to use the magnifier for the center item of the wheel. @@ -80,18 +110,28 @@ class ScrollPickerDecoration { /// The zoomed-in rate of the magnifier, if it is used. /// - /// The default value is 1.0, which will not change anything. If the value is > 1.0, the center item will be zoomed in by that rate, and it will also be rendered as flat, not cylindrical like the rest of the list. The item will be zoomed out if magnification < 1.0. + /// The default value is 1.0, which will not change anything. If the value + /// is > 1.0, the center item will be zoomed in by that rate, and it will + /// also be rendered as flat, not cylindrical like the rest of the list. + /// The item will be zoomed out if magnification < 1.0. /// /// Must be positive. final double magnification; /// The angular compactness of the children on the wheel. /// - /// This denotes a ratio of the number of children on the wheel vs the number of children that would fit on a flat list of equivalent size, assuming [diameterRatio] of 1. + /// This denotes a ratio of the number of children on the wheel vs the number + /// of children that would fit on a flat list of equivalent size, assuming + /// [diameterRatio] of 1. /// - /// For instance, if this RenderListWheelViewport has a height of 100px and [itemExtent] is 20px, 5 items would fit on an equivalent flat list. With a [squeeze] of 1, 5 items would also be shown in the RenderListWheelViewport. With a [squeeze] of 2, 10 items would be shown in the RenderListWheelViewport. + /// For instance, if this RenderListWheelViewport has a height of 100px and + /// [itemExtent] is 20px, 5 items would fit on an equivalent flat list. With + /// a [squeeze] of 1, 5 items would also be shown in the + /// RenderListWheelViewport. With a [squeeze] of 2, 10 items would be shown + /// in the RenderListWheelViewport. /// - /// Changing this value will change the number of children built and shown inside the wheel. + /// Changing this value will change the number of children built and shown + /// inside the wheel. /// /// Must not be null and must be positive. /// @@ -100,8 +140,11 @@ class ScrollPickerDecoration { /// Whether to paint children inside the viewport only. /// - /// If false, every child will be painted. However the [Scrollable] is still the size of the viewport and detects gestures inside only. + /// If false, every child will be painted. However the [Scrollable] is still + /// the size of the viewport and detects gestures inside only. /// - /// Defaults to false. Must not be null. Cannot be true if [clipBehavior] is not [Clip.none] since children outside the viewport will be clipped, and therefore cannot render children outside the viewport. + /// Defaults to false. Must not be null. Cannot be true if [clipBehavior] is + /// not [Clip.none] since children outside the viewport will be clipped, and + /// therefore cannot render children outside the viewport. final bool renderChildrenOutsideViewport; } diff --git a/lib/src/inputs/scroll_picker/scroll_picker_field.dart b/lib/src/inputs/scroll_picker/scroll_picker_field.dart index 8a5931d..d51a3a4 100644 --- a/lib/src/inputs/scroll_picker/scroll_picker_field.dart +++ b/lib/src/inputs/scroll_picker/scroll_picker_field.dart @@ -58,7 +58,8 @@ class _ScrollPickerState extends State { if (newIndex != selectedIndex) { widget.onChanged.call( - (scrollController.offset / widget.decoration.itemHeight).round()); + (scrollController.offset / widget.decoration.itemHeight).round(), + ); selectedIndex = newIndex; } @@ -73,54 +74,52 @@ class _ScrollPickerState extends State { } @override - Widget build(BuildContext context) { - return Stack( - children: [ - Positioned.fill( - child: Center( - child: widget.decoration.highlightWidget ?? - Container( - height: widget.decoration.itemHeight, - decoration: ShapeDecoration( - color: Colors.grey.shade300, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + Widget build(BuildContext context) => Stack( + children: [ + Positioned.fill( + child: Center( + child: widget.decoration.highlightWidget ?? + Container( + height: widget.decoration.itemHeight, + decoration: ShapeDecoration( + color: Colors.grey.shade300, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), ), ), - ), - ), - ), - SizedBox( - height: pickerHeight, - child: ListWheelScrollView.useDelegate( - physics: const FixedExtentScrollPhysics(), - diameterRatio: widget.decoration.diameterRatio, - itemExtent: widget.decoration.itemHeight, - controller: scrollController, - perspective: widget.decoration.perspective, - overAndUnderCenterOpacity: - widget.decoration.overAndUnderCenterOpacity, - childDelegate: ListWheelChildBuilderDelegate( - builder: (context, index) => - widget.decoration.scrollItemBuilder - ?.call(context, index, widget.list[index]) ?? - Center( - child: Text( - widget.list[index], - style: widget.decoration.scrollItemTextStyle, - ), - ), - childCount: widget.list.length, ), - offAxisFraction: widget.decoration.offAxisFraction, - useMagnifier: widget.decoration.useMagnifier, - magnification: widget.decoration.magnification, - squeeze: widget.decoration.squeeze, - renderChildrenOutsideViewport: - widget.decoration.renderChildrenOutsideViewport, ), - ), - ], - ); - } + SizedBox( + height: pickerHeight, + child: ListWheelScrollView.useDelegate( + physics: const FixedExtentScrollPhysics(), + diameterRatio: widget.decoration.diameterRatio, + itemExtent: widget.decoration.itemHeight, + controller: scrollController, + perspective: widget.decoration.perspective, + overAndUnderCenterOpacity: + widget.decoration.overAndUnderCenterOpacity, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) => + widget.decoration.scrollItemBuilder + ?.call(context, index, widget.list[index]) ?? + Center( + child: Text( + widget.list[index], + style: widget.decoration.scrollItemTextStyle, + ), + ), + childCount: widget.list.length, + ), + offAxisFraction: widget.decoration.offAxisFraction, + useMagnifier: widget.decoration.useMagnifier, + magnification: widget.decoration.magnification, + squeeze: widget.decoration.squeeze, + renderChildrenOutsideViewport: + widget.decoration.renderChildrenOutsideViewport, + ), + ), + ], + ); } diff --git a/lib/src/inputs/scroll_picker/scroll_picker_type_extensions.dart b/lib/src/inputs/scroll_picker/scroll_picker_type_extensions.dart index 11d1f02..2860646 100644 --- a/lib/src/inputs/scroll_picker/scroll_picker_type_extensions.dart +++ b/lib/src/inputs/scroll_picker/scroll_picker_type_extensions.dart @@ -28,7 +28,8 @@ enum Month { } class TypeUtils { - /// Creates list of Datetime with days. These fall on the respective week days from start to end. + /// Creates list of Datetime with days. These fall on the respective week + /// days from start to end. List createWeekDays( WeekDay start, WeekDay end, @@ -37,8 +38,8 @@ class TypeUtils { throw ArgumentError('Start month must be before or equal to end month.'); } - List result = []; - for (int i = start.index; i <= end.index; i++) { + var result = []; + for (var i = start.index; i <= end.index; i++) { result.add(DateTime(2024, 1, WeekDay.values[i].index + 1)); } @@ -51,10 +52,11 @@ class TypeUtils { throw ArgumentError('Start month must be before or equal to end month.'); } - List result = []; - for (int i = start.index; i <= end.index; i++) { + var result = []; + for (var i = start.index; i <= end.index; i++) { result.add( - DateTime(year ?? DateTime.now().year, Month.values[i].index + 1, 1)); + DateTime(year ?? DateTime.now().year, Month.values[i].index + 1, 1), + ); } return result; @@ -66,8 +68,8 @@ class TypeUtils { throw ArgumentError('Start year must be before or equal to year month.'); } - List result = []; - for (int i = 0; i <= end - start; i++) { + var result = []; + for (var i = 0; i <= end - start; i++) { result.add(DateTime(start + i, 1, 1)); } diff --git a/lib/src/inputs/scroll_picker/scroll_picker_widget.dart b/lib/src/inputs/scroll_picker/scroll_picker_widget.dart index 267aa01..7dbc32d 100644 --- a/lib/src/inputs/scroll_picker/scroll_picker_widget.dart +++ b/lib/src/inputs/scroll_picker/scroll_picker_widget.dart @@ -14,8 +14,8 @@ class FlutterFormInputScrollPicker extends StatelessWidget { this.onSaved, this.onChanged, this.initialIndex = 0, - Key? key, - }) : super(key: key); + super.key, + }); /// Values that will be shown in the scroll picker. final List values; @@ -36,42 +36,36 @@ class FlutterFormInputScrollPicker extends StatelessWidget { final ScrollPickerDecoration decoration; @override - Widget build(BuildContext context) { - return ScrollPickerFormField( - values: values, - initialIndex: initialIndex, - decoration: decoration, - childToString: childToString, - onSaved: (value) => onSaved?.call(value), - onChanged: (value) => onChanged?.call(value), - ); - } + Widget build(BuildContext context) => ScrollPickerFormField( + values: values, + initialIndex: initialIndex, + decoration: decoration, + childToString: childToString, + onSaved: (value) => onSaved?.call(value), + onChanged: (value) => onChanged?.call(value), + ); } class ScrollPickerFormField extends FormField { ScrollPickerFormField({ required List values, - int? initialIndex, required ScrollPickerDecoration decoration, - required FormFieldSetter onSaved, - void Function(T value)? onChanged, + required FormFieldSetter super.onSaved, required String Function(T) childToString, - Key? key, + int? initialIndex, + void Function(T value)? onChanged, + super.key, }) : super( - key: key, - onSaved: onSaved, initialValue: values[initialIndex ?? (values.length / 2).floor()], - builder: (FormFieldState state) { - return ScrollPicker( - list: values.map((e) => childToString(e)).toList(), - decoration: decoration, - initialIndex: initialIndex, - onChanged: (int index) { - onChanged?.call(values[index]); + builder: (FormFieldState state) => ScrollPicker( + list: values.map((e) => childToString(e)).toList(), + decoration: decoration, + initialIndex: initialIndex, + onChanged: (int index) { + onChanged?.call(values[index]); - state.didChange(values[index]); - }, - ); - }, + state.didChange(values[index]); + }, + ), ); } diff --git a/lib/src/inputs/slider/slider.dart b/lib/src/inputs/slider/slider.dart index 2ba8fe6..ac0d643 100644 --- a/lib/src/inputs/slider/slider.dart +++ b/lib/src/inputs/slider/slider.dart @@ -7,7 +7,7 @@ import 'package:flutter_input_library/src/inputs/slider/slider_field.dart'; class FlutterFormInputSlider extends StatelessWidget { const FlutterFormInputSlider({ - Key? key, + super.key, this.minValue = 0, this.maxValue = 100, this.onSaved, @@ -15,10 +15,7 @@ class FlutterFormInputSlider extends StatelessWidget { this.initialValue, this.validator, this.focusNode, - }) : assert(minValue < maxValue), - super( - key: key, - ); + }) : assert(minValue < maxValue, 'minValue must be less than maxValue'); final int minValue; final int maxValue; @@ -29,13 +26,11 @@ class FlutterFormInputSlider extends StatelessWidget { final FocusNode? focusNode; @override - Widget build(BuildContext context) { - return SliderFormField( - onSaved: (value) => onSaved?.call(value), - validator: (value) => validator?.call(value), - onChanged: (value) => onChanged?.call(value), - initialValue: initialValue ?? 0.5, - focusNode: focusNode, - ); - } + Widget build(BuildContext context) => SliderFormField( + onSaved: (value) => onSaved?.call(value), + validator: (value) => validator?.call(value), + onChanged: (value) => onChanged?.call(value), + initialValue: initialValue ?? 0.5, + focusNode: focusNode, + ); } diff --git a/lib/src/inputs/slider/slider_field.dart b/lib/src/inputs/slider/slider_field.dart index 8c5ad01..053f185 100644 --- a/lib/src/inputs/slider/slider_field.dart +++ b/lib/src/inputs/slider/slider_field.dart @@ -7,26 +7,21 @@ import 'package:flutter/material.dart'; /// Creates a slider with the given input parameters class SliderFormField extends FormField { SliderFormField({ - Key? key, - required FormFieldSetter onSaved, - required FormFieldValidator validator, + required FormFieldSetter super.onSaved, + required FormFieldValidator super.validator, + super.key, void Function(double value)? onChanged, FocusNode? focusNode, - double initialValue = 0.5, + double super.initialValue = 0.5, }) : super( - key: key, - onSaved: onSaved, - validator: validator, - initialValue: initialValue, - builder: (FormFieldState state) { - return Slider( - value: state.value ?? initialValue, - focusNode: focusNode, - onChanged: (double value) { - onChanged?.call(value); + builder: (FormFieldState state) => Slider( + value: state.value ?? initialValue, + focusNode: focusNode, + onChanged: (double value) { + onChanged?.call(value); - state.didChange(value); - }, - ); - }); + state.didChange(value); + }, + ), + ); } diff --git a/lib/src/inputs/switch/switch.dart b/lib/src/inputs/switch/switch.dart index c0962f2..0e7f920 100644 --- a/lib/src/inputs/switch/switch.dart +++ b/lib/src/inputs/switch/switch.dart @@ -2,11 +2,22 @@ // // SPDX-License-Identifier: BSD-3-Clause +// ignore_for_file: avoid_positional_boolean_parameters + import 'package:flutter/material.dart'; import 'package:flutter_input_library/src/inputs/switch/switch_field.dart'; class FlutterFormInputSwitch extends StatelessWidget { + const FlutterFormInputSwitch({ + super.key, + this.label, + this.onSaved, + this.validator, + this.onChanged, + this.focusNode, + this.initialValue = false, + }); final Widget? label; final Function(bool?)? onSaved; final String? Function(bool?)? validator; @@ -14,26 +25,12 @@ class FlutterFormInputSwitch extends StatelessWidget { final bool? initialValue; final FocusNode? focusNode; - const FlutterFormInputSwitch({ - Key? key, - this.label, - this.onSaved, - this.validator, - this.onChanged, - this.focusNode, - this.initialValue = false, - }) : super( - key: key, - ); - @override - Widget build(BuildContext context) { - return SwitchFormField( - onSaved: (value) => onSaved?.call(value), - onChanged: (value) => onChanged?.call(value), - validator: (value) => validator?.call(value), - initialValue: initialValue ?? false, - focusNode: focusNode, - ); - } + Widget build(BuildContext context) => SwitchFormField( + onSaved: (value) => onSaved?.call(value), + onChanged: (value) => onChanged?.call(value), + validator: (value) => validator?.call(value), + initialValue: initialValue ?? false, + focusNode: focusNode, + ); } diff --git a/lib/src/inputs/switch/switch_field.dart b/lib/src/inputs/switch/switch_field.dart index 6720f1d..fc4655e 100644 --- a/lib/src/inputs/switch/switch_field.dart +++ b/lib/src/inputs/switch/switch_field.dart @@ -6,32 +6,27 @@ import 'package:flutter/material.dart'; class SwitchFormField extends FormField { SwitchFormField({ - Key? key, - required FormFieldSetter onSaved, - required FormFieldValidator validator, + required FormFieldSetter super.onSaved, + required FormFieldValidator super.validator, + super.key, FocusNode? focusNode, - bool initialValue = false, - bool autovalidate = false, + bool super.initialValue = false, + // ignore: avoid_positional_boolean_parameters void Function(bool? value)? onChanged, }) : super( - key: key, - onSaved: onSaved, - validator: validator, + builder: (FormFieldState state) => SwitchWidget( initialValue: initialValue, - builder: (FormFieldState state) { - return SwitchWidget( - initialValue: initialValue, - state: state, - focusNode: focusNode, - onChanged: onChanged, - ); - }); + state: state, + focusNode: focusNode, + onChanged: onChanged, + ), + ); } class SwitchWidget extends StatefulWidget { const SwitchWidget({ - this.initialValue = false, required this.state, + this.initialValue = false, this.onChanged, this.focusNode, super.key, @@ -40,6 +35,7 @@ class SwitchWidget extends StatefulWidget { final bool initialValue; final FormFieldState state; final FocusNode? focusNode; + // ignore: avoid_positional_boolean_parameters final void Function(bool? value)? onChanged; @override @@ -50,19 +46,17 @@ class _SwitchWidgetState extends State { late bool value = widget.initialValue; @override - Widget build(BuildContext context) { - return Switch( - value: value, - focusNode: widget.focusNode, - onChanged: (bool value) { - widget.onChanged?.call(value); + Widget build(BuildContext context) => Switch( + value: value, + focusNode: widget.focusNode, + onChanged: (bool value) { + widget.onChanged?.call(value); - widget.state.didChange(value); + widget.state.didChange(value); - setState(() { - this.value = value; - }); - }, - ); - } + setState(() { + this.value = value; + }); + }, + ); } diff --git a/lib/src/inputs/text/password.dart b/lib/src/inputs/text/password.dart index 76eb082..cab9381 100644 --- a/lib/src/inputs/text/password.dart +++ b/lib/src/inputs/text/password.dart @@ -5,23 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -/// Generates a [TextFormField] for passwords. It requires a [FlutterFormInputController] -/// as the [controller] parameter and an optional [Widget] as [label] +/// Generates a [TextFormField] for passwords. It requires a +/// [FlutterFormInputController] as the [controller] parameter and an +/// optional [Widget] as [label] class FlutterFormInputPassword extends StatefulWidget { - final Widget? label; - final FocusNode? focusNode; - final TextStyle? style; - final String? initialValue; - final List? inputFormatters; - final Function(String?)? onSaved; - final String? Function(String?)? validator; - final Function(String?)? onChanged; - final Function(String?)? onFieldSubmitted; - final bool enabled; - final InputDecoration? decoration; - const FlutterFormInputPassword({ - Key? key, + super.key, this.label, this.focusNode, this.style, @@ -33,7 +22,18 @@ class FlutterFormInputPassword extends StatefulWidget { this.onFieldSubmitted, this.enabled = true, this.decoration, - }) : super(key: key); + }); + final Widget? label; + final FocusNode? focusNode; + final TextStyle? style; + final String? initialValue; + final List? inputFormatters; + final Function(String?)? onSaved; + final String? Function(String?)? validator; + final Function(String?)? onChanged; + final Function(String?)? onFieldSubmitted; + final bool enabled; + final InputDecoration? decoration; @override State createState() => _PasswordTextFieldState(); diff --git a/lib/src/inputs/text/plain_text.dart b/lib/src/inputs/text/plain_text.dart index ffa4507..6773b6d 100644 --- a/lib/src/inputs/text/plain_text.dart +++ b/lib/src/inputs/text/plain_text.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; class FlutterFormInputPlainText extends StatelessWidget { const FlutterFormInputPlainText({ - Key? key, + super.key, this.label, this.focusNode, this.decoration, @@ -27,9 +27,7 @@ class FlutterFormInputPlainText extends StatelessWidget { this.enabled = true, this.textCapitalization = TextCapitalization.none, this.obscureText = false, - }) : super( - key: key, - ); + }); final InputDecoration? decoration; final TextAlignVertical? textAlignVertical; @@ -53,9 +51,9 @@ class FlutterFormInputPlainText extends StatelessWidget { @override Widget build(BuildContext context) { - InputDecoration inputDecoration = decoration ?? + var inputDecoration = decoration ?? InputDecoration( - label: label ?? const Text("Plain text"), + label: label ?? const Text('Plain text'), ); return TextFormField( @@ -83,7 +81,7 @@ class FlutterFormInputPlainText extends StatelessWidget { class FlutterFormInputMultiLine extends StatelessWidget { const FlutterFormInputMultiLine({ - Key? key, + super.key, this.label, this.focusNode, this.hint, @@ -98,7 +96,7 @@ class FlutterFormInputMultiLine extends StatelessWidget { this.validator, this.onFieldSubmitted, this.textCapitalization = TextCapitalization.sentences, - }) : super(key: key); + }); final Widget? label; final FocusNode? focusNode; @@ -117,42 +115,40 @@ class FlutterFormInputMultiLine extends StatelessWidget { final TextCapitalization textCapitalization; @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: FlutterFormInputPlainText( - label: label, - textAlignVertical: TextAlignVertical.top, - expands: true, - maxLines: null, - focusNode: focusNode, - maxLength: maxCharacters, - initialValue: initialValue, - scrollPadding: scrollPadding, - keyboardType: keyboardType, - onSaved: onSaved, - validator: validator, - onChanged: onChanged, - onFieldSubmitted: onFieldSubmitted, - decoration: decoration ?? - InputDecoration( - hintText: hint, - floatingLabelBehavior: FloatingLabelBehavior.never, - isDense: true, - border: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFF979797)), + Widget build(BuildContext context) => Column( + children: [ + Expanded( + child: FlutterFormInputPlainText( + label: label, + textAlignVertical: TextAlignVertical.top, + expands: true, + maxLines: null, + focusNode: focusNode, + maxLength: maxCharacters, + initialValue: initialValue, + scrollPadding: scrollPadding, + keyboardType: keyboardType, + onSaved: onSaved, + validator: validator, + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + decoration: decoration ?? + InputDecoration( + hintText: hint, + floatingLabelBehavior: FloatingLabelBehavior.never, + isDense: true, + border: const OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFF979797)), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFF979797)), + ), + filled: true, ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFF979797)), - ), - filled: true, - ), - enabled: enabled, - textCapitalization: textCapitalization, + enabled: enabled, + textCapitalization: textCapitalization, + ), ), - ), - ], - ); - } + ], + ); } diff --git a/lib/src/utils/validators/email/email.dart b/lib/src/utils/validators/email/email.dart index a8b613c..7459254 100644 --- a/lib/src/utils/validators/email/email.dart +++ b/lib/src/utils/validators/email/email.dart @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: BSD-3-Clause -class EmailValidator { +mixin EmailValidator { static bool isValid(String? email) { if (email == null) { return false; diff --git a/pubspec.yaml b/pubspec.yaml index 145ad3a..ba2c3db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_input_library description: A new Flutter package project. -version: 2.7.0 +version: 2.7.1 repository: https://github.com/Iconica-Development/flutter_input_library environment: @@ -15,6 +15,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 6.0.0 flutter: