mirror of
https://github.com/Iconica-Development/flutter_form_wizard.git
synced 2025-05-19 19:03:47 +02:00
feat: changed input from specific to inputs from flutter_input_library
This commit is contained in:
parent
f79c1defe5
commit
8bbe4a3f48
25 changed files with 67 additions and 2027 deletions
|
@ -38,3 +38,8 @@
|
||||||
## 4.0.2 - November 29th 2022
|
## 4.0.2 - November 29th 2022
|
||||||
|
|
||||||
- Name change to flutter_form_wizard
|
- Name change to flutter_form_wizard
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.3 - November 29th 2022
|
||||||
|
|
||||||
|
- Change from input to `flutter_input_library`
|
||||||
|
|
|
@ -61,7 +61,16 @@ packages:
|
||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "4.0.2"
|
version: "4.0.3"
|
||||||
|
flutter_input_library:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: "0.0.3"
|
||||||
|
resolved-ref: "739198b0b8b6fb80fa71dff8a2a11bdda67dcdd7"
|
||||||
|
url: "https://github.com/Iconica-Development/flutter_input_library"
|
||||||
|
source: git
|
||||||
|
version: "0.0.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -204,5 +213,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.18.0 <3.0.0"
|
dart: ">=2.18.2 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
|
|
@ -5,4 +5,6 @@
|
||||||
export 'src/form.dart';
|
export 'src/form.dart';
|
||||||
export 'src/widgets/input/abstractions.dart';
|
export 'src/widgets/input/abstractions.dart';
|
||||||
export 'src/widgets/input/input_types/input_types.dart';
|
export 'src/widgets/input/input_types/input_types.dart';
|
||||||
|
export 'package:flutter_input_library/flutter_input_library.dart'
|
||||||
|
show FlutterFormDateTimeType;
|
||||||
export 'utils/form.dart';
|
export 'utils/form.dart';
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'carousel_utils.dart';
|
|
||||||
import 'carousel_options.dart';
|
|
||||||
import 'carousel_state.dart';
|
|
||||||
|
|
||||||
abstract class CarouselController {
|
|
||||||
bool get ready;
|
|
||||||
|
|
||||||
Future<void> get onReady;
|
|
||||||
|
|
||||||
Future<void> nextPage({Duration? duration, Curve? curve});
|
|
||||||
|
|
||||||
Future<void> previousPage({Duration? duration, Curve? curve});
|
|
||||||
|
|
||||||
void jumpToPage(int page);
|
|
||||||
|
|
||||||
Future<void> animateToPage(int page, {Duration? duration, Curve? curve});
|
|
||||||
|
|
||||||
void startAutoPlay();
|
|
||||||
|
|
||||||
void stopAutoPlay();
|
|
||||||
|
|
||||||
factory CarouselController() => CarouselControllerImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
class CarouselControllerImpl implements CarouselController {
|
|
||||||
final Completer<void> _readyCompleter = Completer<void>();
|
|
||||||
|
|
||||||
CarouselState? _state;
|
|
||||||
|
|
||||||
set state(CarouselState? state) {
|
|
||||||
_state = state;
|
|
||||||
if (!_readyCompleter.isCompleted) {
|
|
||||||
_readyCompleter.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setModeController() =>
|
|
||||||
_state!.changeMode(CarouselPageChangedReason.controller);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get ready => _state != null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> get onReady => _readyCompleter.future;
|
|
||||||
|
|
||||||
/// Animates the controlled [CarouselSlider] to the next page.
|
|
||||||
///
|
|
||||||
/// The animation lasts for the given duration and follows the given curve.
|
|
||||||
/// The returned [Future] resolves when the animation completes.
|
|
||||||
@override
|
|
||||||
Future<void> nextPage(
|
|
||||||
{Duration? duration = const Duration(milliseconds: 300),
|
|
||||||
Curve? curve = Curves.linear}) async {
|
|
||||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
|
||||||
if (isNeedResetTimer) {
|
|
||||||
_state!.onResetTimer();
|
|
||||||
}
|
|
||||||
_setModeController();
|
|
||||||
await _state!.pageController!.nextPage(duration: duration!, curve: curve!);
|
|
||||||
if (isNeedResetTimer) {
|
|
||||||
_state!.onResumeTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animates the controlled [CarouselSlider] to the previous page.
|
|
||||||
///
|
|
||||||
/// The animation lasts for the given duration and follows the given curve.
|
|
||||||
/// The returned [Future] resolves when the animation completes.
|
|
||||||
@override
|
|
||||||
Future<void> previousPage(
|
|
||||||
{Duration? duration = const Duration(milliseconds: 300),
|
|
||||||
Curve? curve = Curves.linear}) async {
|
|
||||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
|
||||||
if (isNeedResetTimer) {
|
|
||||||
_state!.onResetTimer();
|
|
||||||
}
|
|
||||||
_setModeController();
|
|
||||||
await _state!.pageController!
|
|
||||||
.previousPage(duration: duration!, curve: curve!);
|
|
||||||
if (isNeedResetTimer) {
|
|
||||||
_state!.onResumeTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes which page is displayed in the controlled [CarouselSlider].
|
|
||||||
///
|
|
||||||
/// Jumps the page position from its current value to the given value,
|
|
||||||
/// 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);
|
|
||||||
|
|
||||||
_setModeController();
|
|
||||||
final int pageToJump = _state!.pageController!.page!.toInt() + page - index;
|
|
||||||
return _state!.pageController!.jumpToPage(pageToJump);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animates the controlled [CarouselSlider] from the current page to the
|
|
||||||
/// given page.
|
|
||||||
///
|
|
||||||
/// The animation lasts for the given duration and follows the given curve.
|
|
||||||
/// The returned [Future] resolves when the animation completes.
|
|
||||||
@override
|
|
||||||
Future<void> animateToPage(int page,
|
|
||||||
{Duration? duration = const Duration(milliseconds: 300),
|
|
||||||
Curve? curve = Curves.linear}) async {
|
|
||||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
|
||||||
if (isNeedResetTimer) {
|
|
||||||
_state!.onResetTimer();
|
|
||||||
}
|
|
||||||
final 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!);
|
|
||||||
if (isNeedResetTimer) {
|
|
||||||
_state!.onResumeTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts the controlled [CarouselSlider] autoplay.
|
|
||||||
///
|
|
||||||
/// The carousel will only autoPlay if the [autoPlay] parameter
|
|
||||||
/// in [CarouselOptions] is true.
|
|
||||||
@override
|
|
||||||
void startAutoPlay() {
|
|
||||||
_state!.onResumeTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops the controlled [CarouselSlider] from autoplaying.
|
|
||||||
///
|
|
||||||
/// This is a more on-demand way of doing this. Use the [autoPlay]
|
|
||||||
/// parameter in [CarouselOptions] to specify the autoPlay behaviour of the
|
|
||||||
/// carousel.
|
|
||||||
@override
|
|
||||||
void stopAutoPlay() {
|
|
||||||
_state!.onResetTimer();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'carousel_slider.dart';
|
|
||||||
|
|
||||||
class CarouselFormField extends FormField<int> {
|
|
||||||
CarouselFormField({
|
|
||||||
Key? key,
|
|
||||||
required FormFieldSetter<int> onSaved,
|
|
||||||
required FormFieldValidator<int> validator,
|
|
||||||
void Function(int value)? onChanged,
|
|
||||||
void Function(int value)? onSubmit,
|
|
||||||
int initialValue = 0,
|
|
||||||
bool autovalidate = false,
|
|
||||||
required List<Widget> items,
|
|
||||||
double height = 425,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
onSaved: onSaved,
|
|
||||||
validator: validator,
|
|
||||||
initialValue: initialValue,
|
|
||||||
builder: (FormFieldState<int> state) {
|
|
||||||
return 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(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,220 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
enum CarouselPageChangedReason { timed, manual, controller }
|
|
||||||
|
|
||||||
enum CenterPageEnlargeStrategy { scale, height }
|
|
||||||
|
|
||||||
class CarouselOptions {
|
|
||||||
/// Set carousel height and overrides any existing [aspectRatio].
|
|
||||||
final double? height;
|
|
||||||
|
|
||||||
/// Aspect ratio is used if no height have been declared.
|
|
||||||
///
|
|
||||||
/// Defaults to 16:9 aspect ratio.
|
|
||||||
final double aspectRatio;
|
|
||||||
|
|
||||||
/// The fraction of the viewport that each page should occupy.
|
|
||||||
///
|
|
||||||
/// Defaults to 0.8, which means each page fills 80% of the carousel.
|
|
||||||
final double viewportFraction;
|
|
||||||
|
|
||||||
/// The initial page to show when first creating the [CarouselSlider].
|
|
||||||
///
|
|
||||||
/// Defaults to 0.
|
|
||||||
final int initialPage;
|
|
||||||
|
|
||||||
///Determines if carousel should loop infinitely or be limited to item length.
|
|
||||||
///
|
|
||||||
///Defaults to true, i.e. infinite loop.
|
|
||||||
final bool enableInfiniteScroll;
|
|
||||||
|
|
||||||
/// Reverse the order of items if set to true.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool reverse;
|
|
||||||
|
|
||||||
/// Enables auto play, sliding one page at a time.
|
|
||||||
///
|
|
||||||
/// Use [autoPlayInterval] to determine the frequency of slides.
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool autoPlay;
|
|
||||||
|
|
||||||
/// Sets Duration to determine the frequency of slides when [autoPlay] is set
|
|
||||||
/// to true.
|
|
||||||
/// Defaults to 4 seconds.
|
|
||||||
final Duration autoPlayInterval;
|
|
||||||
|
|
||||||
/// The animation duration between two transitioning pages while in auto
|
|
||||||
/// playback.
|
|
||||||
///
|
|
||||||
/// Defaults to 800 ms.
|
|
||||||
final Duration autoPlayAnimationDuration;
|
|
||||||
|
|
||||||
/// Determines the animation curve physics.
|
|
||||||
///
|
|
||||||
/// Defaults to [Curves.fastOutSlowIn].
|
|
||||||
final Curve autoPlayCurve;
|
|
||||||
|
|
||||||
/// Determines if current page should be larger than the side images,
|
|
||||||
/// creating a feeling of depth in the carousel.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool? enlargeCenterPage;
|
|
||||||
|
|
||||||
/// The axis along which the page view scrolls.
|
|
||||||
///
|
|
||||||
/// Defaults to [Axis.horizontal].
|
|
||||||
final Axis scrollDirection;
|
|
||||||
|
|
||||||
/// Called whenever the page in the center of the viewport changes.
|
|
||||||
final Function(int index, CarouselPageChangedReason reason)? onPageChanged;
|
|
||||||
|
|
||||||
/// Called whenever the carousel is scrolled
|
|
||||||
final ValueChanged<double?>? onScrolled;
|
|
||||||
|
|
||||||
/// How the carousel should respond to user input.
|
|
||||||
///
|
|
||||||
/// For example, determines how the items continues to animate after the
|
|
||||||
/// user stops dragging the page view.
|
|
||||||
///
|
|
||||||
/// The physics are modified to snap to page boundaries using
|
|
||||||
/// [PageScrollPhysics] prior to being used.
|
|
||||||
///
|
|
||||||
/// Defaults to matching platform conventions.
|
|
||||||
final ScrollPhysics? scrollPhysics;
|
|
||||||
|
|
||||||
/// Set to false to disable page snapping, useful for custom scroll behavior.
|
|
||||||
///
|
|
||||||
/// Default to `true`.
|
|
||||||
final bool pageSnapping;
|
|
||||||
|
|
||||||
/// If `true`, the auto play function will be paused when user is interacting
|
|
||||||
/// with the carousel, and will be resumed when user finish interacting.
|
|
||||||
///
|
|
||||||
/// Default to `true`.
|
|
||||||
final bool pauseAutoPlayOnTouch;
|
|
||||||
|
|
||||||
/// If `true`, the auto play function will be paused when user is calling
|
|
||||||
/// [PageController]'s [nextPage] or [previousPage] or [animateToPage] method.
|
|
||||||
/// And after the animation complete, the auto play will be resumed.
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
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.
|
|
||||||
final CenterPageEnlargeStrategy enlargeStrategy;
|
|
||||||
|
|
||||||
/// Whether or not to disable the [Center] widget for each slide.
|
|
||||||
final bool disableCenter;
|
|
||||||
|
|
||||||
/// Whether to add padding to both ends of the list.
|
|
||||||
/// If this is set to true and [viewportFraction] < 1.0, padding will be added
|
|
||||||
/// such that the first and last child slivers will be in the center of the 1
|
|
||||||
/// viewport when scrolled all the way to the start or end, respectively.
|
|
||||||
///
|
|
||||||
/// If [viewportFraction] >= 1.0, this property has no effect.
|
|
||||||
/// This property defaults to true and must not be null.
|
|
||||||
final bool padEnds;
|
|
||||||
|
|
||||||
/// 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<double?>? 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,
|
|
||||||
viewportFraction: viewportFraction ?? this.viewportFraction,
|
|
||||||
initialPage: initialPage ?? this.initialPage,
|
|
||||||
enableInfiniteScroll: enableInfiniteScroll ?? this.enableInfiniteScroll,
|
|
||||||
reverse: reverse ?? this.reverse,
|
|
||||||
autoPlay: autoPlay ?? this.autoPlay,
|
|
||||||
autoPlayInterval: autoPlayInterval ?? this.autoPlayInterval,
|
|
||||||
autoPlayAnimationDuration:
|
|
||||||
autoPlayAnimationDuration ?? this.autoPlayAnimationDuration,
|
|
||||||
autoPlayCurve: autoPlayCurve ?? this.autoPlayCurve,
|
|
||||||
enlargeCenterPage: enlargeCenterPage ?? this.enlargeCenterPage,
|
|
||||||
onPageChanged: onPageChanged ?? this.onPageChanged,
|
|
||||||
onScrolled: onScrolled ?? this.onScrolled,
|
|
||||||
scrollPhysics: scrollPhysics ?? this.scrollPhysics,
|
|
||||||
pageSnapping: pageSnapping ?? this.pageSnapping,
|
|
||||||
scrollDirection: scrollDirection ?? this.scrollDirection,
|
|
||||||
pauseAutoPlayOnTouch: pauseAutoPlayOnTouch ?? this.pauseAutoPlayOnTouch,
|
|
||||||
pauseAutoPlayOnManualNavigate:
|
|
||||||
pauseAutoPlayOnManualNavigate ?? this.pauseAutoPlayOnManualNavigate,
|
|
||||||
pauseAutoPlayInFiniteScroll:
|
|
||||||
pauseAutoPlayInFiniteScroll ?? this.pauseAutoPlayInFiniteScroll,
|
|
||||||
pageViewKey: pageViewKey ?? this.pageViewKey,
|
|
||||||
enlargeStrategy: enlargeStrategy ?? this.enlargeStrategy,
|
|
||||||
disableCenter: disableCenter ?? this.disableCenter,
|
|
||||||
clipBehavior: clipBehavior ?? this.clipBehavior,
|
|
||||||
padEnds: padEnds ?? this.padEnds,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,358 +0,0 @@
|
||||||
// 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';
|
|
||||||
|
|
||||||
export 'carousel_controller.dart';
|
|
||||||
export 'carousel_options.dart';
|
|
||||||
|
|
||||||
typedef ExtendedIndexedWidgetBuilder = Widget Function(
|
|
||||||
BuildContext context, int index, int realIndex);
|
|
||||||
|
|
||||||
class CarouselSlider extends StatefulWidget {
|
|
||||||
/// [CarouselOptions] to create a [CarouselState] with.
|
|
||||||
final CarouselOptions options;
|
|
||||||
|
|
||||||
/// The widgets to be shown in the carousel of default constructor.
|
|
||||||
final List<Widget>? 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.
|
|
||||||
final ExtendedIndexedWidgetBuilder? itemBuilder;
|
|
||||||
|
|
||||||
/// A [MapController], used to control the map.
|
|
||||||
final CarouselControllerImpl _carouselController;
|
|
||||||
|
|
||||||
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<CarouselSlider>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late CarouselControllerImpl carouselController;
|
|
||||||
Timer? timer;
|
|
||||||
|
|
||||||
CarouselOptions get options => widget.options;
|
|
||||||
|
|
||||||
CarouselState? carouselState;
|
|
||||||
|
|
||||||
PageController? pageController;
|
|
||||||
|
|
||||||
/// [mode] is related to why the page is being changed.
|
|
||||||
CarouselPageChangedReason mode = CarouselPageChangedReason.controller;
|
|
||||||
|
|
||||||
CarouselSliderState();
|
|
||||||
|
|
||||||
void changeMode(CarouselPageChangedReason mode) {
|
|
||||||
this.mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(CarouselSlider oldWidget) {
|
|
||||||
carouselState!.options = options;
|
|
||||||
carouselState!.itemCount = widget.itemCount;
|
|
||||||
|
|
||||||
/// [pageController] needs to be re-initialized to respond to state changes.
|
|
||||||
pageController = PageController(
|
|
||||||
viewportFraction: options.viewportFraction,
|
|
||||||
initialPage: carouselState!.realPage,
|
|
||||||
);
|
|
||||||
carouselState!.pageController = pageController;
|
|
||||||
|
|
||||||
/// handle autoplay when state changes
|
|
||||||
handleAutoPlay();
|
|
||||||
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
carouselController = widget._carouselController;
|
|
||||||
|
|
||||||
carouselState = CarouselState(options, clearTimer, resumeTimer, changeMode);
|
|
||||||
|
|
||||||
carouselState!.itemCount = widget.itemCount;
|
|
||||||
carouselController.state = carouselState;
|
|
||||||
carouselState!.initialPage = widget.options.initialPage;
|
|
||||||
carouselState!.realPage = options.enableInfiniteScroll
|
|
||||||
? carouselState!.realPage + carouselState!.initialPage
|
|
||||||
: carouselState!.initialPage;
|
|
||||||
handleAutoPlay();
|
|
||||||
|
|
||||||
pageController = PageController(
|
|
||||||
viewportFraction: options.viewportFraction,
|
|
||||||
initialPage: carouselState!.realPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
carouselState!.pageController = pageController;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer? getTimer() {
|
|
||||||
return widget.options.autoPlay
|
|
||||||
? Timer.periodic(widget.options.autoPlayInterval, (_) {
|
|
||||||
final route = ModalRoute.of(context);
|
|
||||||
if (route?.isCurrent == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearTimer() {
|
|
||||||
if (timer != null) {
|
|
||||||
timer?.cancel();
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void resumeTimer() {
|
|
||||||
timer ??= getTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleAutoPlay() {
|
|
||||||
bool autoPlayEnabled = widget.options.autoPlay;
|
|
||||||
|
|
||||||
if (autoPlayEnabled && timer != null) return;
|
|
||||||
|
|
||||||
clearTimer();
|
|
||||||
if (autoPlayEnabled) {
|
|
||||||
resumeTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget getGestureWrapper(Widget child) {
|
|
||||||
Widget wrapper;
|
|
||||||
if (widget.options.height != null) {
|
|
||||||
wrapper = SizedBox(height: widget.options.height, child: child);
|
|
||||||
} else {
|
|
||||||
wrapper =
|
|
||||||
AspectRatio(aspectRatio: widget.options.aspectRatio, child: child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RawGestureDetector(
|
|
||||||
gestures: {
|
|
||||||
_MultipleGestureRecognizer:
|
|
||||||
GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>(
|
|
||||||
() => _MultipleGestureRecognizer(),
|
|
||||||
(_MultipleGestureRecognizer instance) {
|
|
||||||
instance.onStart = (_) {
|
|
||||||
onStart();
|
|
||||||
};
|
|
||||||
instance.onDown = (_) {
|
|
||||||
onPanDown();
|
|
||||||
};
|
|
||||||
instance.onEnd = (_) {
|
|
||||||
onPanUp();
|
|
||||||
};
|
|
||||||
instance.onCancel = () {
|
|
||||||
onPanUp();
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
child: NotificationListener(
|
|
||||||
onNotification: (Notification notification) {
|
|
||||||
if (widget.options.onScrolled != null &&
|
|
||||||
notification is ScrollUpdateNotification) {
|
|
||||||
widget.options.onScrolled!(carouselState!.pageController!.page);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
child: wrapper,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget getCenterWrapper(Widget child) {
|
|
||||||
if (widget.options.disableCenter) {
|
|
||||||
return Container(
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Center(child: child);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
void onStart() {
|
|
||||||
changeMode(CarouselPageChangedReason.manual);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPanDown() {
|
|
||||||
if (widget.options.pauseAutoPlayOnTouch) {
|
|
||||||
clearTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
changeMode(CarouselPageChangedReason.manual);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPanUp() {
|
|
||||||
if (widget.options.pauseAutoPlayOnTouch) {
|
|
||||||
resumeTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
clearTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MultipleGestureRecognizer extends PanGestureRecognizer {}
|
|
|
@ -1,45 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'carousel_slider.dart';
|
|
||||||
|
|
||||||
class CarouselState {
|
|
||||||
/// The [CarouselOptions] to create this state
|
|
||||||
CarouselOptions options;
|
|
||||||
|
|
||||||
/// [pageController] is created using the properties passed to the constructor
|
|
||||||
/// and can be used to control the [PageView] it is passed to.
|
|
||||||
PageController? pageController;
|
|
||||||
|
|
||||||
/// The actual index of the [PageView].
|
|
||||||
///
|
|
||||||
/// This value can be ignored unless you know the carousel will be scrolled
|
|
||||||
/// backwards more then 10000 pages.
|
|
||||||
/// Defaults to 10000 to simulate infinite backwards scrolling.
|
|
||||||
int realPage = 10000;
|
|
||||||
|
|
||||||
/// The initial index of the [PageView] on [CarouselSlider] init.
|
|
||||||
///
|
|
||||||
int initialPage = 0;
|
|
||||||
|
|
||||||
/// The widgets count that should be shown at carousel
|
|
||||||
int? itemCount;
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// The callback to set the Reason Carousel changed
|
|
||||||
Function(CarouselPageChangedReason) changeMode;
|
|
||||||
|
|
||||||
CarouselState(
|
|
||||||
this.options, this.onResetTimer, this.onResumeTimer, this.changeMode);
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
int getRealIndex(int position, int base, int? length) {
|
|
||||||
final int offset = position - base;
|
|
||||||
return remainder(offset, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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!;
|
|
||||||
return result < 0 ? source + result : result;
|
|
||||||
}
|
|
|
@ -6,8 +6,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_form_wizard/flutter_form.dart';
|
import 'package:flutter_form_wizard/flutter_form.dart';
|
||||||
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'carousel_form.dart';
|
|
||||||
|
|
||||||
/// Input for a carousel of items used in a [FlutterForm].
|
/// Input for a carousel of items used in a [FlutterForm].
|
||||||
///
|
///
|
||||||
|
@ -35,7 +34,7 @@ class FlutterFormInputCarousel extends FlutterFormInputWidget<int> {
|
||||||
|
|
||||||
super.registerController(context);
|
super.registerController(context);
|
||||||
|
|
||||||
return CarouselFormField(
|
return input.FlutterFormInputCarousel(
|
||||||
onSaved: (value) => controller.onSaved(value),
|
onSaved: (value) => controller.onSaved(value),
|
||||||
validator: (value) => controller.onValidate(value, _),
|
validator: (value) => controller.onValidate(value, _),
|
||||||
onChanged: controller.onChanged,
|
onChanged: controller.onChanged,
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import '../../../../../flutter_form.dart';
|
|
||||||
|
|
||||||
/// Generates a [DateTimeInputField] for DateTimes/Dates/Times/DateRanges.
|
|
||||||
/// It requires a [FlutterFormInputController], [inputType], [dateFormat], [firstDate], and [lastDate]
|
|
||||||
class DateTimeInputField extends ConsumerStatefulWidget {
|
|
||||||
const DateTimeInputField({
|
|
||||||
Key? key,
|
|
||||||
required this.inputType,
|
|
||||||
required this.controller,
|
|
||||||
this.label,
|
|
||||||
this.showIcon = true,
|
|
||||||
this.icon,
|
|
||||||
required this.dateFormat,
|
|
||||||
required this.firstDate,
|
|
||||||
required this.lastDate,
|
|
||||||
this.initialDate,
|
|
||||||
this.initialDateTimeRange,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
final FlutterFormDateTimeType inputType;
|
|
||||||
final FlutterFormInputController<String> controller;
|
|
||||||
final DateFormat dateFormat;
|
|
||||||
final bool showIcon;
|
|
||||||
final DateTime? firstDate;
|
|
||||||
final DateTime? lastDate;
|
|
||||||
final DateTime? initialDate;
|
|
||||||
final DateTimeRange? initialDateTimeRange;
|
|
||||||
final IconData? icon;
|
|
||||||
final Widget? label;
|
|
||||||
@override
|
|
||||||
ConsumerState<DateTimeInputField> createState() => _DateInputFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DateInputFieldState extends ConsumerState<DateTimeInputField> {
|
|
||||||
late final DateTime firstDate;
|
|
||||||
late final DateTime lastDate;
|
|
||||||
late final DateTime initialDate;
|
|
||||||
late final DateTimeRange initialDateRange;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
firstDate = widget.firstDate ??
|
|
||||||
DateTime.now().subtract(
|
|
||||||
const Duration(days: 1000),
|
|
||||||
);
|
|
||||||
lastDate = widget.lastDate ??
|
|
||||||
DateTime.now().add(
|
|
||||||
const Duration(days: 1000),
|
|
||||||
);
|
|
||||||
initialDate = widget.initialDate ?? DateTime.now();
|
|
||||||
initialDateRange = widget.initialDateTimeRange ??
|
|
||||||
DateTimeRange(
|
|
||||||
start: DateTime.now(),
|
|
||||||
end: DateTime.now().add(
|
|
||||||
const Duration(days: 7),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
String Function(String, {List<String>? params}) _ =
|
|
||||||
getTranslator(context, ref);
|
|
||||||
|
|
||||||
Future<String> getInputFromUser(FlutterFormDateTimeType inputType) async {
|
|
||||||
String userInput = '';
|
|
||||||
switch (inputType) {
|
|
||||||
case FlutterFormDateTimeType.date:
|
|
||||||
DateTime? unformatted = await showDatePicker(
|
|
||||||
initialDate: initialDate,
|
|
||||||
context: context,
|
|
||||||
firstDate: firstDate,
|
|
||||||
lastDate: lastDate,
|
|
||||||
);
|
|
||||||
userInput = unformatted != null
|
|
||||||
? widget.dateFormat.format(unformatted)
|
|
||||||
: userInput;
|
|
||||||
break;
|
|
||||||
case FlutterFormDateTimeType.dateTime:
|
|
||||||
await getInputFromUser(FlutterFormDateTimeType.date)
|
|
||||||
.then((value) async {
|
|
||||||
if (value != '') {
|
|
||||||
String secondInput =
|
|
||||||
await getInputFromUser(FlutterFormDateTimeType.time);
|
|
||||||
if (secondInput != '') {
|
|
||||||
userInput = '$value $secondInput';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case FlutterFormDateTimeType.range:
|
|
||||||
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();
|
|
||||||
break;
|
|
||||||
case FlutterFormDateTimeType.time:
|
|
||||||
userInput = await showTimePicker(
|
|
||||||
context: context, initialTime: TimeOfDay.now())
|
|
||||||
.then((value) => value == null ? '' : value.format(context));
|
|
||||||
}
|
|
||||||
return userInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TextFormField(
|
|
||||||
keyboardType: TextInputType.none,
|
|
||||||
readOnly: true,
|
|
||||||
key: Key(widget.controller.value.toString()),
|
|
||||||
initialValue: widget.controller.value,
|
|
||||||
onSaved: (value) {
|
|
||||||
widget.controller.onSaved(value);
|
|
||||||
},
|
|
||||||
onTap: () async {
|
|
||||||
String userInput = await getInputFromUser(widget.inputType);
|
|
||||||
setState(() {
|
|
||||||
widget.controller.value =
|
|
||||||
userInput != '' ? userInput : widget.controller.value;
|
|
||||||
widget.controller.onChanged
|
|
||||||
?.call(userInput != '' ? userInput : widget.controller.value);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
validator: (value) => widget.controller.onValidate(value, _),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
suffixIcon: widget.showIcon ? Icon(widget.icon) : null,
|
|
||||||
focusColor: Theme.of(context).primaryColor,
|
|
||||||
label: widget.label ?? const Text("Date"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,21 +3,13 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/src/widgets/input/input_types/input_date_picker/date_picker.dart';
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../../../../../flutter_form.dart';
|
import '../../../../../flutter_form.dart';
|
||||||
|
|
||||||
/// Select Input Types in a [FlutterFormInputDateTime]
|
|
||||||
enum FlutterFormDateTimeType {
|
|
||||||
date,
|
|
||||||
time,
|
|
||||||
dateTime,
|
|
||||||
range,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Input for a dateTime used in a [FlutterForm].
|
/// Input for a dateTime used in a [FlutterForm].
|
||||||
///
|
///
|
||||||
/// Standard controller is [FlutterFormInputDateController].
|
/// Standard controller is [FlutterFormInputDateController].
|
||||||
|
@ -40,7 +32,7 @@ class FlutterFormInputDateTime extends FlutterFormInputWidget<String> {
|
||||||
label: label,
|
label: label,
|
||||||
);
|
);
|
||||||
final bool showIcon;
|
final bool showIcon;
|
||||||
final FlutterFormDateTimeType inputType;
|
final input.FlutterFormDateTimeType inputType;
|
||||||
final DateFormat dateFormat;
|
final DateFormat dateFormat;
|
||||||
final DateTime? initialDate;
|
final DateTime? initialDate;
|
||||||
final DateTimeRange? initialDateTimeRange;
|
final DateTimeRange? initialDateTimeRange;
|
||||||
|
@ -54,13 +46,16 @@ class FlutterFormInputDateTime extends FlutterFormInputWidget<String> {
|
||||||
getTranslator(context, ref);
|
getTranslator(context, ref);
|
||||||
super.registerController(context);
|
super.registerController(context);
|
||||||
|
|
||||||
return DateTimeInputField(
|
return input.FlutterFormInputDateTime(
|
||||||
label: label,
|
label: label,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
firstDate: firstDate,
|
firstDate: firstDate,
|
||||||
lastDate: lastDate,
|
lastDate: lastDate,
|
||||||
inputType: inputType,
|
inputType: inputType,
|
||||||
controller: controller,
|
onChanged: (value) => controller.onChanged?.call(value),
|
||||||
|
onSaved: (value) => controller.onSaved(value),
|
||||||
|
validator: (value) => controller.onValidate(value, _),
|
||||||
|
initialValue: controller.value,
|
||||||
dateFormat: dateFormat,
|
dateFormat: dateFormat,
|
||||||
initialDate: initialDate,
|
initialDate: initialDate,
|
||||||
initialDateTimeRange: initialDateTimeRange,
|
initialDateTimeRange: initialDateTimeRange,
|
||||||
|
@ -89,7 +84,7 @@ class FlutterFormInputDateTimeController
|
||||||
final DateTime? initialDate;
|
final DateTime? initialDate;
|
||||||
final DateTimeRange? initialDateTimeRange;
|
final DateTimeRange? initialDateTimeRange;
|
||||||
final DateFormat dateFormat;
|
final DateFormat dateFormat;
|
||||||
final FlutterFormDateTimeType dateTimeType;
|
final input.FlutterFormDateTimeType dateTimeType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? id;
|
String? id;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../flutter_form.dart';
|
import '../../../../flutter_form.dart';
|
||||||
|
@ -29,7 +30,7 @@ class FlutterFormInputEmail extends FlutterFormInputWidget<String> {
|
||||||
|
|
||||||
super.registerController(context);
|
super.registerController(context);
|
||||||
|
|
||||||
return TextFormField(
|
return input.FlutterFormInputPlainText(
|
||||||
initialValue: controller.value,
|
initialValue: controller.value,
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
controller.onSaved(value);
|
controller.onSaved(value);
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'numberpicker.dart';
|
|
||||||
|
|
||||||
class DecimalNumberPicker extends StatelessWidget {
|
|
||||||
final int minValue;
|
|
||||||
final int maxValue;
|
|
||||||
final double value;
|
|
||||||
final ValueChanged<double> onChanged;
|
|
||||||
final int itemCount;
|
|
||||||
final double itemHeight;
|
|
||||||
final double itemWidth;
|
|
||||||
final Axis axis;
|
|
||||||
final TextStyle? textStyle;
|
|
||||||
final TextStyle? selectedTextStyle;
|
|
||||||
final bool haptics;
|
|
||||||
final TextMapper? integerTextMapper;
|
|
||||||
final TextMapper? decimalTextMapper;
|
|
||||||
final bool integerZeroPad;
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
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
|
|
||||||
? 0
|
|
||||||
: ((value - value.floorToDouble()) * math.pow(10, decimalPlaces))
|
|
||||||
.round();
|
|
||||||
final doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1;
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
NumberPicker(
|
|
||||||
minValue: minValue,
|
|
||||||
maxValue: maxValue,
|
|
||||||
value: value.floor(),
|
|
||||||
onChanged: _onIntChanged,
|
|
||||||
itemCount: itemCount,
|
|
||||||
itemHeight: itemHeight,
|
|
||||||
itemWidth: itemWidth,
|
|
||||||
textStyle: textStyle,
|
|
||||||
selectedTextStyle: selectedTextStyle,
|
|
||||||
haptics: haptics,
|
|
||||||
zeroPad: integerZeroPad,
|
|
||||||
textMapper: integerTextMapper,
|
|
||||||
decoration: integerDecoration,
|
|
||||||
),
|
|
||||||
NumberPicker(
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: doubleMaxValue,
|
|
||||||
value: decimalValue,
|
|
||||||
onChanged: _onDoubleChanged,
|
|
||||||
itemCount: itemCount,
|
|
||||||
itemHeight: itemHeight,
|
|
||||||
itemWidth: itemWidth,
|
|
||||||
textStyle: textStyle,
|
|
||||||
selectedTextStyle: selectedTextStyle,
|
|
||||||
haptics: haptics,
|
|
||||||
textMapper: decimalTextMapper,
|
|
||||||
decoration: decimalDecoration,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onIntChanged(int intValue) {
|
|
||||||
final 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));
|
|
||||||
onChanged(value.floor() + decimalPart);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,366 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
library infinite_listview;
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
/// Infinite ListView
|
|
||||||
///
|
|
||||||
/// ListView that builds its children with to an infinite extent.
|
|
||||||
///
|
|
||||||
class InfiniteListView extends StatefulWidget {
|
|
||||||
/// See [ListView.builder]
|
|
||||||
const InfiniteListView.builder({
|
|
||||||
Key? 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,
|
|
||||||
this.addSemanticIndexes = true,
|
|
||||||
this.cacheExtent,
|
|
||||||
this.anchor = 0.0,
|
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
|
||||||
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
|
||||||
this.restorationId,
|
|
||||||
this.clipBehavior = Clip.hardEdge,
|
|
||||||
}) : separatorBuilder = null,
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// See [ListView.separated]
|
|
||||||
const InfiniteListView.separated({
|
|
||||||
Key? 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,
|
|
||||||
this.addSemanticIndexes = true,
|
|
||||||
this.cacheExtent,
|
|
||||||
this.anchor = 0.0,
|
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
|
||||||
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
|
||||||
this.restorationId,
|
|
||||||
this.clipBehavior = Clip.hardEdge,
|
|
||||||
}) : itemExtent = null,
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// See: [ScrollView.scrollDirection]
|
|
||||||
final Axis scrollDirection;
|
|
||||||
|
|
||||||
/// See: [ScrollView.reverse]
|
|
||||||
final bool reverse;
|
|
||||||
|
|
||||||
/// See: [ScrollView.controller]
|
|
||||||
final InfiniteScrollController? controller;
|
|
||||||
|
|
||||||
/// See: [ScrollView.physics]
|
|
||||||
final ScrollPhysics? physics;
|
|
||||||
|
|
||||||
/// See: [BoxScrollView.padding]
|
|
||||||
final EdgeInsets? padding;
|
|
||||||
|
|
||||||
/// See: [ListView.builder]
|
|
||||||
final IndexedWidgetBuilder itemBuilder;
|
|
||||||
|
|
||||||
/// See: [ListView.separated]
|
|
||||||
final IndexedWidgetBuilder? separatorBuilder;
|
|
||||||
|
|
||||||
/// See: [SliverChildBuilderDelegate.childCount]
|
|
||||||
final int? itemCount;
|
|
||||||
|
|
||||||
/// See: [ListView.itemExtent]
|
|
||||||
final double? itemExtent;
|
|
||||||
|
|
||||||
/// See: [ScrollView.cacheExtent]
|
|
||||||
final double? cacheExtent;
|
|
||||||
|
|
||||||
/// See: [ScrollView.anchor]
|
|
||||||
final double anchor;
|
|
||||||
|
|
||||||
/// See: [SliverChildBuilderDelegate.addAutomaticKeepAlives]
|
|
||||||
final bool addAutomaticKeepAlives;
|
|
||||||
|
|
||||||
/// See: [SliverChildBuilderDelegate.addRepaintBoundaries]
|
|
||||||
final bool addRepaintBoundaries;
|
|
||||||
|
|
||||||
/// See: [SliverChildBuilderDelegate.addSemanticIndexes]
|
|
||||||
final bool addSemanticIndexes;
|
|
||||||
|
|
||||||
/// See: [ScrollView.dragStartBehavior]
|
|
||||||
final DragStartBehavior dragStartBehavior;
|
|
||||||
|
|
||||||
/// See: [ScrollView.keyboardDismissBehavior]
|
|
||||||
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
|
||||||
|
|
||||||
/// See: [ScrollView.restorationId]
|
|
||||||
final String? restorationId;
|
|
||||||
|
|
||||||
/// See: [ScrollView.clipBehavior]
|
|
||||||
final Clip clipBehavior;
|
|
||||||
|
|
||||||
@override
|
|
||||||
InfiniteListViewState createState() => InfiniteListViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class InfiniteListViewState extends State<InfiniteListView> {
|
|
||||||
InfiniteScrollController? _controller;
|
|
||||||
|
|
||||||
InfiniteScrollController get _effectiveController =>
|
|
||||||
widget.controller ?? _controller!;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
if (widget.controller == null) {
|
|
||||||
_controller = InfiniteScrollController();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(InfiniteListView oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (widget.controller == null && oldWidget.controller != null) {
|
|
||||||
_controller = InfiniteScrollController();
|
|
||||||
} else if (widget.controller != null && oldWidget.controller == null) {
|
|
||||||
_controller!.dispose();
|
|
||||||
_controller = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller?.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final List<Widget> slivers = _buildSlivers(context, negative: false);
|
|
||||||
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
|
|
||||||
final AxisDirection axisDirection = _getDirection(context);
|
|
||||||
final 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(
|
|
||||||
physics: scrollPhysics,
|
|
||||||
context: state,
|
|
||||||
initialPixels: -offset.pixels,
|
|
||||||
keepScrollOffset: _effectiveController.keepScrollOffset,
|
|
||||||
negativeScroll: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
return Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
Viewport(
|
|
||||||
axisDirection: flipAxisDirection(axisDirection),
|
|
||||||
anchor: 1.0 - widget.anchor,
|
|
||||||
offset: negativeOffset,
|
|
||||||
slivers: negativeSlivers,
|
|
||||||
cacheExtent: widget.cacheExtent,
|
|
||||||
),
|
|
||||||
Viewport(
|
|
||||||
axisDirection: axisDirection,
|
|
||||||
anchor: widget.anchor,
|
|
||||||
offset: offset,
|
|
||||||
slivers: slivers,
|
|
||||||
cacheExtent: widget.cacheExtent,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AxisDirection _getDirection(BuildContext context) {
|
|
||||||
return getAxisDirectionFromAxisReverseAndDirectionality(
|
|
||||||
context, widget.scrollDirection, widget.reverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
|
|
||||||
final itemExtent = widget.itemExtent;
|
|
||||||
final padding = widget.padding ?? EdgeInsets.zero;
|
|
||||||
return <Widget>[
|
|
||||||
SliverPadding(
|
|
||||||
padding: negative
|
|
||||||
? padding - EdgeInsets.only(bottom: padding.bottom)
|
|
||||||
: padding - EdgeInsets.only(top: padding.top),
|
|
||||||
sliver: (itemExtent != null)
|
|
||||||
? SliverFixedExtentList(
|
|
||||||
delegate: negative
|
|
||||||
? negativeChildrenDelegate
|
|
||||||
: positiveChildrenDelegate,
|
|
||||||
itemExtent: itemExtent,
|
|
||||||
)
|
|
||||||
: SliverList(
|
|
||||||
delegate: negative
|
|
||||||
? 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 positiveChildrenDelegate {
|
|
||||||
final separatorBuilder = widget.separatorBuilder;
|
|
||||||
final itemCount = widget.itemCount;
|
|
||||||
return SliverChildBuilderDelegate(
|
|
||||||
(separatorBuilder != null)
|
|
||||||
? (BuildContext context, int index) {
|
|
||||||
final itemIndex = index ~/ 2;
|
|
||||||
return index.isEven
|
|
||||||
? widget.itemBuilder(context, itemIndex)
|
|
||||||
: separatorBuilder(context, itemIndex);
|
|
||||||
}
|
|
||||||
: widget.itemBuilder,
|
|
||||||
childCount: separatorBuilder == null
|
|
||||||
? itemCount
|
|
||||||
: (itemCount != null ? math.max(0, itemCount * 2 - 1) : null),
|
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties
|
|
||||||
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
|
|
||||||
properties.add(FlagProperty('reverse',
|
|
||||||
value: widget.reverse, ifTrue: 'reversed', showName: true));
|
|
||||||
properties.add(DiagnosticsProperty<ScrollController>(
|
|
||||||
'controller', widget.controller,
|
|
||||||
showName: false, defaultValue: null));
|
|
||||||
properties.add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics,
|
|
||||||
showName: false, defaultValue: null));
|
|
||||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
|
|
||||||
'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({
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InfiniteScrollPosition extends ScrollPositionWithSingleContext {
|
|
||||||
_InfiniteScrollPosition({
|
|
||||||
required ScrollPhysics physics,
|
|
||||||
required ScrollContext context,
|
|
||||||
double? initialPixels = 0.0,
|
|
||||||
bool keepScrollOffset = true,
|
|
||||||
ScrollPosition? oldPosition,
|
|
||||||
String? debugLabel,
|
|
||||||
this.negativeScroll = false,
|
|
||||||
}) : super(
|
|
||||||
physics: physics,
|
|
||||||
context: context,
|
|
||||||
initialPixels: initialPixels,
|
|
||||||
keepScrollOffset: keepScrollOffset,
|
|
||||||
oldPosition: oldPosition,
|
|
||||||
debugLabel: debugLabel,
|
|
||||||
);
|
|
||||||
|
|
||||||
final bool negativeScroll;
|
|
||||||
|
|
||||||
void _forceNegativePixels(double value) {
|
|
||||||
super.forcePixels(-value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void saveScrollOffset() {
|
|
||||||
if (!negativeScroll) {
|
|
||||||
super.saveScrollOffset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void restoreScrollOffset() {
|
|
||||||
if (!negativeScroll) {
|
|
||||||
super.restoreScrollOffset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get minScrollExtent => double.negativeInfinity;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get maxScrollExtent => double.infinity;
|
|
||||||
}
|
|
|
@ -4,11 +4,10 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../../../flutter_form.dart';
|
import '../../../../../flutter_form.dart';
|
||||||
|
|
||||||
import 'numberpicker.dart';
|
|
||||||
|
|
||||||
/// Input for a number used in a [FlutterForm].
|
/// Input for a number used in a [FlutterForm].
|
||||||
///
|
///
|
||||||
/// [minValue] sets the minimal value of the picker.
|
/// [minValue] sets the minimal value of the picker.
|
||||||
|
@ -35,7 +34,7 @@ class FlutterFormInputNumberPicker extends FlutterFormInputWidget<int> {
|
||||||
|
|
||||||
super.registerController(context);
|
super.registerController(context);
|
||||||
|
|
||||||
return NumberPickerFormField(
|
return input.FlutterFormInputNumberPicker(
|
||||||
minValue: minValue,
|
minValue: minValue,
|
||||||
maxValue: maxValue,
|
maxValue: maxValue,
|
||||||
onSaved: (value) => controller.onSaved(value),
|
onSaved: (value) => controller.onSaved(value),
|
||||||
|
@ -46,40 +45,6 @@ class FlutterFormInputNumberPicker extends FlutterFormInputWidget<int> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controller for the numberPicker used by a [FlutterFormInputWidget] used in a [FlutterForm].
|
|
||||||
///
|
|
||||||
/// Mainly used by [FlutterFormInputNumberPicker].
|
|
||||||
class NumberPickerFormField extends FormField<int> {
|
|
||||||
NumberPickerFormField({
|
|
||||||
Key? key,
|
|
||||||
required FormFieldSetter<int> onSaved,
|
|
||||||
required FormFieldValidator<int> validator,
|
|
||||||
void Function(int value)? onChanged,
|
|
||||||
int initialValue = 0,
|
|
||||||
bool autovalidate = false,
|
|
||||||
int minValue = 0,
|
|
||||||
int maxValue = 100,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
onSaved: onSaved,
|
|
||||||
validator: validator,
|
|
||||||
initialValue: initialValue,
|
|
||||||
builder: (FormFieldState<int> state) {
|
|
||||||
return NumberPicker(
|
|
||||||
minValue: minValue,
|
|
||||||
maxValue: maxValue,
|
|
||||||
value: initialValue,
|
|
||||||
onChanged: (int value) {
|
|
||||||
onChanged?.call(value);
|
|
||||||
|
|
||||||
state.didChange(value);
|
|
||||||
},
|
|
||||||
itemHeight: 35,
|
|
||||||
itemCount: 5,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlutterFormInputNumberPickerController
|
class FlutterFormInputNumberPickerController
|
||||||
implements FlutterFormInputController<int> {
|
implements FlutterFormInputController<int> {
|
||||||
FlutterFormInputNumberPickerController({
|
FlutterFormInputNumberPickerController({
|
||||||
|
|
|
@ -1,309 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'infinite_listview.dart';
|
|
||||||
|
|
||||||
typedef TextMapper = String Function(String numberText);
|
|
||||||
|
|
||||||
class NumberPicker extends StatefulWidget {
|
|
||||||
/// Min value user can pick
|
|
||||||
final int minValue;
|
|
||||||
|
|
||||||
/// Max value user can pick
|
|
||||||
final int maxValue;
|
|
||||||
|
|
||||||
/// Currently selected value
|
|
||||||
final int value;
|
|
||||||
|
|
||||||
/// Called when selected value changes
|
|
||||||
final ValueChanged<int> onChanged;
|
|
||||||
|
|
||||||
/// Specifies how many items should be shown - defaults to 3
|
|
||||||
final int itemCount;
|
|
||||||
|
|
||||||
/// Step between elements. Only for integer datePicker
|
|
||||||
/// Examples:
|
|
||||||
/// if step is 100 the following elements may be 100, 200, 300...
|
|
||||||
/// if min=0, max=6, step=3, then items will be 0, 3 and 6
|
|
||||||
/// if min=0, max=5, step=3, then items will be 0 and 3.
|
|
||||||
final int step;
|
|
||||||
|
|
||||||
/// height of single item in pixels
|
|
||||||
final double itemHeight;
|
|
||||||
|
|
||||||
/// width of single item in pixels
|
|
||||||
final double itemWidth;
|
|
||||||
|
|
||||||
/// Direction of scrolling
|
|
||||||
final Axis axis;
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
final TextStyle? selectedTextStyle;
|
|
||||||
|
|
||||||
/// Whether to trigger haptic pulses or not
|
|
||||||
final bool haptics;
|
|
||||||
|
|
||||||
/// Build the text of each item on the picker
|
|
||||||
final TextMapper? textMapper;
|
|
||||||
|
|
||||||
/// Pads displayed integer values up to the length of maxValue
|
|
||||||
final bool zeroPad;
|
|
||||||
|
|
||||||
/// Decoration to apply to central box where the selected value is placed
|
|
||||||
final Decoration? decoration;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
class NumberPickerState extends State<NumberPicker> {
|
|
||||||
late ScrollController _scrollController;
|
|
||||||
|
|
||||||
late int value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
value = widget.value;
|
|
||||||
|
|
||||||
final initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent;
|
|
||||||
if (widget.infiniteLoop) {
|
|
||||||
_scrollController =
|
|
||||||
InfiniteScrollController(initialScrollOffset: initialOffset);
|
|
||||||
} else {
|
|
||||||
_scrollController = ScrollController(initialScrollOffset: initialOffset);
|
|
||||||
}
|
|
||||||
_scrollController.addListener(_scrollListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scrollListener() {
|
|
||||||
var indexOfMiddleElement = (_scrollController.offset / itemExtent).round();
|
|
||||||
if (widget.infiniteLoop) {
|
|
||||||
indexOfMiddleElement %= itemCount;
|
|
||||||
} else {
|
|
||||||
indexOfMiddleElement = indexOfMiddleElement.clamp(0, itemCount - 1);
|
|
||||||
}
|
|
||||||
final intValueInTheMiddle =
|
|
||||||
_intValueFromIndex(indexOfMiddleElement + additionalItemsOnEachSide);
|
|
||||||
|
|
||||||
if (value != intValueInTheMiddle) {
|
|
||||||
setState(() {
|
|
||||||
value = intValueInTheMiddle;
|
|
||||||
});
|
|
||||||
|
|
||||||
widget.onChanged(intValueInTheMiddle);
|
|
||||||
if (widget.haptics) {
|
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Future.delayed(
|
|
||||||
const Duration(milliseconds: 100),
|
|
||||||
() => _maybeCenterValue(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(NumberPicker oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.value != value) {
|
|
||||||
_maybeCenterValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isScrolling => _scrollController.position.isScrollingNotifier.value;
|
|
||||||
|
|
||||||
double get itemExtent =>
|
|
||||||
widget.axis == Axis.vertical ? widget.itemHeight : widget.itemWidth;
|
|
||||||
|
|
||||||
int get itemCount => (widget.maxValue - widget.minValue) ~/ widget.step + 1;
|
|
||||||
|
|
||||||
int get listItemsCount => itemCount + 2 * additionalItemsOnEachSide;
|
|
||||||
|
|
||||||
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<ScrollEndNotification>(
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
decoration: widget.decoration,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _itemBuilder(BuildContext context, int index) {
|
|
||||||
final themeData = Theme.of(context);
|
|
||||||
final defaultStyle = widget.textStyle ?? themeData.textTheme.bodyText2;
|
|
||||||
final selectedStyle = widget.selectedTextStyle ??
|
|
||||||
themeData.textTheme.headline5
|
|
||||||
?.copyWith(color: themeData.highlightColor);
|
|
||||||
|
|
||||||
final valueFromIndex = _intValueFromIndex(index % itemCount);
|
|
||||||
final isExtra = !widget.infiniteLoop &&
|
|
||||||
(index < additionalItemsOnEachSide ||
|
|
||||||
index >= listItemsCount - additionalItemsOnEachSide);
|
|
||||||
final itemStyle = valueFromIndex == value ? selectedStyle : defaultStyle;
|
|
||||||
|
|
||||||
final child = isExtra
|
|
||||||
? const SizedBox.shrink()
|
|
||||||
: Text(
|
|
||||||
_getDisplayedValue(valueFromIndex),
|
|
||||||
style: itemStyle,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: widget.itemWidth,
|
|
||||||
height: widget.itemHeight,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getDisplayedValue(int value) {
|
|
||||||
final text = widget.zeroPad
|
|
||||||
? value.toString().padLeft(widget.maxValue.toString().length, '0')
|
|
||||||
: value.toString();
|
|
||||||
if (widget.textMapper != null) {
|
|
||||||
return widget.textMapper!(text);
|
|
||||||
} else {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int _intValueFromIndex(int index) {
|
|
||||||
index -= additionalItemsOnEachSide;
|
|
||||||
index %= itemCount;
|
|
||||||
return widget.minValue + index * widget.step;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _maybeCenterValue() {
|
|
||||||
if (_scrollController.hasClients && !isScrolling) {
|
|
||||||
int diff = value - widget.minValue;
|
|
||||||
int index = diff ~/ widget.step;
|
|
||||||
if (widget.infiniteLoop) {
|
|
||||||
final offset = _scrollController.offset + 0.5 * itemExtent;
|
|
||||||
final cycles = (offset / (itemCount * itemExtent)).floor();
|
|
||||||
index += cycles * itemCount;
|
|
||||||
}
|
|
||||||
_scrollController.animateTo(
|
|
||||||
index * itemExtent,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NumberPickerSelectedItemDecoration extends StatelessWidget {
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isVertical => axis == Axis.vertical;
|
|
||||||
}
|
|
|
@ -3,7 +3,8 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/src/widgets/input/input_types/input_password/password.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../../../flutter_form.dart';
|
import '../../../../../flutter_form.dart';
|
||||||
|
|
||||||
|
@ -21,9 +22,15 @@ class FlutterFormInputPassword extends FlutterFormInputWidget<String> {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
super.registerController(context);
|
super.registerController(context);
|
||||||
|
|
||||||
return PasswordTextField(
|
String Function(String, {List<String>? params}) _ =
|
||||||
label: label,
|
getTranslator(context, ref);
|
||||||
controller: controller,
|
|
||||||
|
return input.FlutterFormInputPassword(
|
||||||
|
initialValue: controller.value,
|
||||||
|
onSaved: (value) => controller.onSaved(value),
|
||||||
|
validator: (value) => controller.onValidate(value, _),
|
||||||
|
onChanged: (value) => controller.onChanged?.call(value),
|
||||||
|
onFieldSubmitted: (value) => controller.onSubmit?.call(value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import '../../../../../flutter_form.dart';
|
|
||||||
|
|
||||||
/// Generates a [TextFormField] for passwords. It requires a [FlutterFormInputController]
|
|
||||||
/// as the [controller] parameter and an optional [Widget] as [label]
|
|
||||||
class PasswordTextField extends ConsumerStatefulWidget {
|
|
||||||
final Widget? label;
|
|
||||||
final FlutterFormInputController controller;
|
|
||||||
|
|
||||||
const PasswordTextField({
|
|
||||||
Key? key,
|
|
||||||
required this.controller,
|
|
||||||
this.label,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<PasswordTextField> createState() => _PasswordTextFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PasswordTextFieldState extends ConsumerState<PasswordTextField> {
|
|
||||||
bool obscured = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
String Function(String, {List<String>? params}) _ =
|
|
||||||
getTranslator(context, ref);
|
|
||||||
|
|
||||||
return TextFormField(
|
|
||||||
initialValue: widget.controller.value,
|
|
||||||
obscureText: obscured,
|
|
||||||
onSaved: (value) => widget.controller.onSaved(value),
|
|
||||||
validator: (value) => widget.controller.onValidate(value, _),
|
|
||||||
onChanged: (value) => widget.controller.onChanged?.call(value),
|
|
||||||
onFieldSubmitted: (value) => widget.controller.onSubmit?.call(value),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
label: widget.label ?? const Text("Password"),
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
obscured = !obscured;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: Icon(obscured ? Icons.visibility_off : Icons.visibility),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../flutter_form.dart';
|
import '../../../../flutter_form.dart';
|
||||||
|
@ -45,7 +46,7 @@ class FlutterFormInputPlainText extends FlutterFormInputWidget<String> {
|
||||||
label: label ?? const Text("Plain text"),
|
label: label ?? const Text("Plain text"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return TextFormField(
|
return input.FlutterFormInputPlainText(
|
||||||
scrollPadding: scrollPadding ?? const EdgeInsets.all(20.0),
|
scrollPadding: scrollPadding ?? const EdgeInsets.all(20.0),
|
||||||
initialValue: controller.value,
|
initialValue: controller.value,
|
||||||
onSaved: (value) => controller.onSaved(value),
|
onSaved: (value) => controller.onSaved(value),
|
||||||
|
@ -69,7 +70,7 @@ class FlutterFormInputPlainText extends FlutterFormInputWidget<String> {
|
||||||
/// Hint can be set to set a hint inside the field.
|
/// Hint can be set to set a hint inside the field.
|
||||||
///
|
///
|
||||||
/// MaxCharacters can be set to set a maximum amount of characters.
|
/// MaxCharacters can be set to set a maximum amount of characters.
|
||||||
class FlutterFormInputMultiLine extends StatelessWidget {
|
class FlutterFormInputMultiLine extends ConsumerWidget {
|
||||||
const FlutterFormInputMultiLine({
|
const FlutterFormInputMultiLine({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
@ -85,32 +86,17 @@ class FlutterFormInputMultiLine extends StatelessWidget {
|
||||||
final int? maxCharacters;
|
final int? maxCharacters;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Column(
|
String Function(String, {List<String>? params}) _ =
|
||||||
children: [
|
getTranslator(context, ref);
|
||||||
Expanded(
|
|
||||||
child: FlutterFormInputPlainText(
|
return input.FlutterFormInputMultiLine(
|
||||||
label: label,
|
label: label,
|
||||||
controller: controller,
|
hint: hint,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
maxCharacters: maxCharacters,
|
||||||
expands: true,
|
onChanged: controller.onChanged,
|
||||||
maxLines: null,
|
onSaved: controller.onSaved,
|
||||||
maxLength: maxCharacters,
|
validator: (v) => controller.onValidate(v, _),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/src/widgets/input/input_types/input_slider/slider.dart';
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class FlutterFormInputSlider extends FlutterFormInputWidget<double> {
|
||||||
|
|
||||||
super.registerController(context);
|
super.registerController(context);
|
||||||
|
|
||||||
return SliderFormField(
|
return input.FlutterFormInputSlider(
|
||||||
onSaved: (value) => controller.onSaved(value),
|
onSaved: (value) => controller.onSaved(value),
|
||||||
validator: (value) => controller.onValidate(value, _),
|
validator: (value) => controller.onValidate(value, _),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// Creates a slider with the given input parameters
|
|
||||||
class SliderFormField extends FormField<double> {
|
|
||||||
SliderFormField({
|
|
||||||
Key? key,
|
|
||||||
required FormFieldSetter<double> onSaved,
|
|
||||||
required FormFieldValidator<double> validator,
|
|
||||||
void Function(double value)? onChanged,
|
|
||||||
double initialValue = 0.5,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
onSaved: onSaved,
|
|
||||||
validator: validator,
|
|
||||||
initialValue: initialValue,
|
|
||||||
builder: (FormFieldState<double> state) {
|
|
||||||
return Slider(
|
|
||||||
value: state.value ?? initialValue,
|
|
||||||
onChanged: (double value) {
|
|
||||||
onChanged?.call(value);
|
|
||||||
|
|
||||||
state.didChange(value);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_wizard/flutter_form.dart';
|
import 'package:flutter_form_wizard/flutter_form.dart';
|
||||||
import 'package:flutter_form_wizard/src/widgets/input/input_types/input_switch/input_switch_field.dart';
|
import 'package:flutter_input_library/flutter_input_library.dart' as input;
|
||||||
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
import 'package:flutter_form_wizard/utils/translation_service.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
@ -25,10 +25,8 @@ class FlutterFormInputSwitch extends FlutterFormInputWidget<bool> {
|
||||||
|
|
||||||
super.registerController(context);
|
super.registerController(context);
|
||||||
|
|
||||||
return SwitchFormField(
|
return input.FlutterFormInputSwitch(
|
||||||
onSaved: (value) {
|
onSaved: (value) => controller.onSaved(value),
|
||||||
controller.onSaved(value);
|
|
||||||
},
|
|
||||||
onChanged: controller.onChanged,
|
onChanged: controller.onChanged,
|
||||||
validator: (value) => controller.onValidate(value, _),
|
validator: (value) => controller.onValidate(value, _),
|
||||||
initialValue: controller.value ?? false,
|
initialValue: controller.value ?? false,
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SwitchFormField extends FormField<bool> {
|
|
||||||
SwitchFormField({
|
|
||||||
Key? key,
|
|
||||||
required FormFieldSetter<bool> onSaved,
|
|
||||||
required FormFieldValidator<bool> validator,
|
|
||||||
bool initialValue = false,
|
|
||||||
bool autovalidate = false,
|
|
||||||
void Function(bool? value)? onChanged,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
onSaved: onSaved,
|
|
||||||
validator: validator,
|
|
||||||
initialValue: initialValue,
|
|
||||||
builder: (FormFieldState<bool> state) {
|
|
||||||
return SwitchWidget(
|
|
||||||
initialValue: initialValue,
|
|
||||||
state: state,
|
|
||||||
onChanged: onChanged,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class SwitchWidget extends StatefulWidget {
|
|
||||||
const SwitchWidget({
|
|
||||||
this.initialValue = false,
|
|
||||||
required this.state,
|
|
||||||
this.onChanged,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool initialValue;
|
|
||||||
final FormFieldState<bool> state;
|
|
||||||
final void Function(bool? value)? onChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SwitchWidget> createState() => _SwitchWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SwitchWidgetState extends State<SwitchWidget> {
|
|
||||||
late bool value = widget.initialValue;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Switch(
|
|
||||||
value: value,
|
|
||||||
onChanged: (bool value) {
|
|
||||||
widget.onChanged?.call(value);
|
|
||||||
|
|
||||||
widget.state.didChange(value);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
this.value = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: flutter_form_wizard
|
name: flutter_form_wizard
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
homepage: https://github.com/Iconica-Development/flutter_form_wizard
|
homepage: https://github.com/Iconica-Development/flutter_form_wizard
|
||||||
|
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
@ -17,6 +17,10 @@ dependencies:
|
||||||
flutter_riverpod: ^2.1.1
|
flutter_riverpod: ^2.1.1
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
localization: ^2.1.0
|
localization: ^2.1.0
|
||||||
|
flutter_input_library:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Iconica-Development/flutter_input_library
|
||||||
|
ref: 0.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue