mirror of
https://github.com/Iconica-Development/flutter_input_library.git
synced 2025-05-18 08:53:45 +02:00
feat: add ci and linter
This commit is contained in:
parent
8eb1d80a9f
commit
fee3d681cb
32 changed files with 1008 additions and 961 deletions
|
@ -59,4 +59,8 @@
|
||||||
* Addition of 'obscureText' parameter to 'FlutterFormInputPlainText'
|
* Addition of 'obscureText' parameter to 'FlutterFormInputPlainText'
|
||||||
|
|
||||||
## 2.7.0
|
## 2.7.0
|
||||||
* Addition of 'decoration' parameter to 'FlutterFormInputPassword'
|
* Addition of 'decoration' parameter to 'FlutterFormInputPassword'
|
||||||
|
|
||||||
|
## 2.7.1
|
||||||
|
|
||||||
|
* Added Iconica CI and Iconica Linter
|
|
@ -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
|
# Possible to overwrite the rules from the package
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# 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:
|
version:
|
||||||
revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4"
|
||||||
channel: stable
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@ project_type: app
|
||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||||
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||||
- platform: web
|
- platform: ios
|
||||||
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||||
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|
|
@ -37,10 +37,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.2"
|
version: "1.18.0"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -68,7 +68,7 @@ packages:
|
||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "2.4.0"
|
version: "2.7.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -118,10 +118,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.10.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -147,18 +147,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.11.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -179,10 +179,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -195,10 +195,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4-beta"
|
version: "0.3.0"
|
||||||
sdks:
|
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"
|
flutter: ">=1.17.0"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
/// A library for creating input fields in Flutter.
|
||||||
library flutter_input_library;
|
library flutter_input_library;
|
||||||
|
|
||||||
export 'src/inputs/inputs.dart';
|
export 'src/inputs/inputs.dart';
|
||||||
|
|
|
@ -4,20 +4,18 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'carousel_form.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_form.dart';
|
||||||
|
|
||||||
class FlutterFormInputCarousel extends StatelessWidget {
|
class FlutterFormInputCarousel extends StatelessWidget {
|
||||||
const FlutterFormInputCarousel({
|
const FlutterFormInputCarousel({
|
||||||
Key? key,
|
|
||||||
required this.items,
|
required this.items,
|
||||||
|
super.key,
|
||||||
this.height = 425,
|
this.height = 425,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.initialValue,
|
this.initialValue,
|
||||||
this.validator,
|
this.validator,
|
||||||
}) : super(
|
});
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<Widget> items;
|
final List<Widget> items;
|
||||||
final double height;
|
final double height;
|
||||||
|
@ -27,14 +25,12 @@ class FlutterFormInputCarousel extends StatelessWidget {
|
||||||
final int? initialValue;
|
final int? initialValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => CarouselFormField(
|
||||||
return CarouselFormField(
|
onSaved: (value) => onSaved?.call(value),
|
||||||
onSaved: (value) => onSaved?.call(value),
|
validator: (value) => validator?.call(value),
|
||||||
validator: (value) => validator?.call(value),
|
onChanged: (value) => onChanged?.call(value),
|
||||||
onChanged: (value) => onChanged?.call(value),
|
initialValue: initialValue ?? 0,
|
||||||
initialValue: initialValue ?? 0,
|
items: items,
|
||||||
items: items,
|
height: height,
|
||||||
height: height,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_options.dart';
|
||||||
import 'carousel_utils.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_state.dart';
|
||||||
import 'carousel_options.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_utils.dart';
|
||||||
import 'carousel_state.dart';
|
|
||||||
|
|
||||||
abstract class CarouselController {
|
abstract class CarouselController {
|
||||||
|
factory CarouselController() => CarouselControllerImpl();
|
||||||
bool get ready;
|
bool get ready;
|
||||||
|
|
||||||
Future<void> get onReady;
|
Future<void> get onReady;
|
||||||
|
@ -26,8 +26,6 @@ abstract class CarouselController {
|
||||||
void startAutoPlay();
|
void startAutoPlay();
|
||||||
|
|
||||||
void stopAutoPlay();
|
void stopAutoPlay();
|
||||||
|
|
||||||
factory CarouselController() => CarouselControllerImpl();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarouselControllerImpl implements CarouselController {
|
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 animation lasts for the given duration and follows the given curve.
|
||||||
/// The returned [Future] resolves when the animation completes.
|
/// The returned [Future] resolves when the animation completes.
|
||||||
@override
|
@override
|
||||||
Future<void> nextPage(
|
Future<void> nextPage({
|
||||||
{Duration? duration = const Duration(milliseconds: 300),
|
Duration? duration = const Duration(milliseconds: 300),
|
||||||
Curve? curve = Curves.linear}) async {
|
Curve? curve = Curves.linear,
|
||||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
}) async {
|
||||||
|
var isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
||||||
if (isNeedResetTimer) {
|
if (isNeedResetTimer) {
|
||||||
_state!.onResetTimer();
|
_state!.onResetTimer();
|
||||||
}
|
}
|
||||||
|
@ -75,10 +74,11 @@ class CarouselControllerImpl implements CarouselController {
|
||||||
/// The animation lasts for the given duration and follows the given curve.
|
/// The animation lasts for the given duration and follows the given curve.
|
||||||
/// The returned [Future] resolves when the animation completes.
|
/// The returned [Future] resolves when the animation completes.
|
||||||
@override
|
@override
|
||||||
Future<void> previousPage(
|
Future<void> previousPage({
|
||||||
{Duration? duration = const Duration(milliseconds: 300),
|
Duration? duration = const Duration(milliseconds: 300),
|
||||||
Curve? curve = Curves.linear}) async {
|
Curve? curve = Curves.linear,
|
||||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
}) async {
|
||||||
|
var isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
||||||
if (isNeedResetTimer) {
|
if (isNeedResetTimer) {
|
||||||
_state!.onResetTimer();
|
_state!.onResetTimer();
|
||||||
}
|
}
|
||||||
|
@ -96,11 +96,14 @@ class CarouselControllerImpl implements CarouselController {
|
||||||
/// without animation, and without checking if the new value is in range.
|
/// without animation, and without checking if the new value is in range.
|
||||||
@override
|
@override
|
||||||
void jumpToPage(int page) {
|
void jumpToPage(int page) {
|
||||||
final index = getRealIndex(_state!.pageController!.page!.toInt(),
|
var index = getRealIndex(
|
||||||
_state!.realPage - _state!.initialPage, _state!.itemCount);
|
_state!.pageController!.page!.toInt(),
|
||||||
|
_state!.realPage - _state!.initialPage,
|
||||||
|
_state!.itemCount,
|
||||||
|
);
|
||||||
|
|
||||||
_setModeController();
|
_setModeController();
|
||||||
final int pageToJump = _state!.pageController!.page!.toInt() + page - index;
|
var pageToJump = _state!.pageController!.page!.toInt() + page - index;
|
||||||
return _state!.pageController!.jumpToPage(pageToJump);
|
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 animation lasts for the given duration and follows the given curve.
|
||||||
/// The returned [Future] resolves when the animation completes.
|
/// The returned [Future] resolves when the animation completes.
|
||||||
@override
|
@override
|
||||||
Future<void> animateToPage(int page,
|
Future<void> animateToPage(
|
||||||
{Duration? duration = const Duration(milliseconds: 300),
|
int page, {
|
||||||
Curve? curve = Curves.linear}) async {
|
Duration? duration = const Duration(milliseconds: 300),
|
||||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
Curve? curve = Curves.linear,
|
||||||
|
}) async {
|
||||||
|
var isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
||||||
if (isNeedResetTimer) {
|
if (isNeedResetTimer) {
|
||||||
_state!.onResetTimer();
|
_state!.onResetTimer();
|
||||||
}
|
}
|
||||||
final index = getRealIndex(_state!.pageController!.page!.toInt(),
|
var index = getRealIndex(
|
||||||
_state!.realPage - _state!.initialPage, _state!.itemCount);
|
_state!.pageController!.page!.toInt(),
|
||||||
|
_state!.realPage - _state!.initialPage,
|
||||||
|
_state!.itemCount,
|
||||||
|
);
|
||||||
_setModeController();
|
_setModeController();
|
||||||
await _state!.pageController!.animateToPage(
|
await _state!.pageController!.animateToPage(
|
||||||
_state!.pageController!.page!.toInt() + page - index,
|
_state!.pageController!.page!.toInt() + page - index,
|
||||||
duration: duration!,
|
duration: duration!,
|
||||||
curve: curve!);
|
curve: curve!,
|
||||||
|
);
|
||||||
if (isNeedResetTimer) {
|
if (isNeedResetTimer) {
|
||||||
_state!.onResumeTimer();
|
_state!.onResumeTimer();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,41 +3,32 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'carousel_slider.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_slider.dart';
|
||||||
|
|
||||||
class CarouselFormField extends FormField<int> {
|
class CarouselFormField extends FormField<int> {
|
||||||
CarouselFormField({
|
CarouselFormField({
|
||||||
Key? key,
|
required FormFieldSetter<int> super.onSaved,
|
||||||
required FormFieldSetter<int> onSaved,
|
required FormFieldValidator<int> super.validator,
|
||||||
required FormFieldValidator<int> validator,
|
|
||||||
void Function(int value)? onChanged,
|
|
||||||
void Function(int value)? onSubmit,
|
|
||||||
int initialValue = 0,
|
|
||||||
bool autovalidate = false,
|
|
||||||
required List<Widget> items,
|
required List<Widget> items,
|
||||||
|
super.key,
|
||||||
|
void Function(int value)? onChanged,
|
||||||
|
int super.initialValue = 0,
|
||||||
double height = 425,
|
double height = 425,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
builder: (FormFieldState<int> state) => CarouselSlider(
|
||||||
onSaved: onSaved,
|
options: CarouselOptions(
|
||||||
validator: validator,
|
initialPage: initialValue,
|
||||||
initialValue: initialValue,
|
onPageChanged: (index, reason) {
|
||||||
builder: (FormFieldState<int> state) {
|
onChanged?.call(index);
|
||||||
return CarouselSlider(
|
|
||||||
options: CarouselOptions(
|
|
||||||
initialPage: initialValue,
|
|
||||||
onPageChanged: (index, reason) {
|
|
||||||
onChanged?.call(index);
|
|
||||||
|
|
||||||
state.didChange(index);
|
state.didChange(index);
|
||||||
},
|
},
|
||||||
height: height,
|
height: height,
|
||||||
aspectRatio: 2.0,
|
aspectRatio: 2.0,
|
||||||
enlargeCenterPage: true,
|
enlargeCenterPage: true,
|
||||||
enableInfiniteScroll: false,
|
enableInfiniteScroll: false,
|
||||||
),
|
),
|
||||||
items: items.map((Widget item) {
|
items: items.map((Widget item) => item).toList(),
|
||||||
return item;
|
),
|
||||||
}).toList(),
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,33 @@ enum CarouselPageChangedReason { timed, manual, controller }
|
||||||
enum CenterPageEnlargeStrategy { scale, height }
|
enum CenterPageEnlargeStrategy { scale, height }
|
||||||
|
|
||||||
class CarouselOptions {
|
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].
|
/// Set carousel height and overrides any existing [aspectRatio].
|
||||||
final double? height;
|
final double? height;
|
||||||
|
|
||||||
|
@ -105,18 +132,19 @@ class CarouselOptions {
|
||||||
/// Default to `true`.
|
/// Default to `true`.
|
||||||
final bool pauseAutoPlayOnManualNavigate;
|
final bool pauseAutoPlayOnManualNavigate;
|
||||||
|
|
||||||
/// If [enableInfiniteScroll] is `false`, and [autoPlay] is `true`, this option
|
/// If [enableInfiniteScroll] is `false`, and [autoPlay] is `true`,
|
||||||
/// decide the carousel should go to the first item when it reach the last item or not.
|
/// this option decide the carousel should go to the first item when it
|
||||||
/// If set to `true`, the auto play will be paused when it reach the last item.
|
/// reach the last item or not. If set to `true`, the auto play will be
|
||||||
/// If set to `false`, the auto play function will animate to the first item
|
/// paused when it reach the last item. If set to `false`, the auto play
|
||||||
/// when it was in the last item.
|
/// function will animate to the first item when it was in the last item.
|
||||||
final bool pauseAutoPlayInFiniteScroll;
|
final bool pauseAutoPlayInFiniteScroll;
|
||||||
|
|
||||||
/// Pass a [PageStorageKey] if you want to keep the pageview's position when
|
/// Pass a [PageStorageKey] if you want to keep the pageview's position when
|
||||||
/// it was recreated.
|
/// it was recreated.
|
||||||
final PageStorageKey? pageViewKey;
|
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;
|
final CenterPageEnlargeStrategy enlargeStrategy;
|
||||||
|
|
||||||
/// Whether or not to disable the [Center] widget for each slide.
|
/// Whether or not to disable the [Center] widget for each slide.
|
||||||
|
@ -134,60 +162,34 @@ class CarouselOptions {
|
||||||
/// Exposed [clipBehavior] of [PageView]
|
/// Exposed [clipBehavior] of [PageView]
|
||||||
final Clip clipBehavior;
|
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.
|
///Generate new [CarouselOptions] based on old ones.
|
||||||
|
|
||||||
CarouselOptions copyWith(
|
CarouselOptions copyWith({
|
||||||
{double? height,
|
double? height,
|
||||||
double? aspectRatio,
|
double? aspectRatio,
|
||||||
double? viewportFraction,
|
double? viewportFraction,
|
||||||
int? initialPage,
|
int? initialPage,
|
||||||
bool? enableInfiniteScroll,
|
bool? enableInfiniteScroll,
|
||||||
bool? reverse,
|
bool? reverse,
|
||||||
bool? autoPlay,
|
bool? autoPlay,
|
||||||
Duration? autoPlayInterval,
|
Duration? autoPlayInterval,
|
||||||
Duration? autoPlayAnimationDuration,
|
Duration? autoPlayAnimationDuration,
|
||||||
Curve? autoPlayCurve,
|
Curve? autoPlayCurve,
|
||||||
bool? enlargeCenterPage,
|
bool? enlargeCenterPage,
|
||||||
Function(int index, CarouselPageChangedReason reason)? onPageChanged,
|
Function(int index, CarouselPageChangedReason reason)? onPageChanged,
|
||||||
ValueChanged<double?>? onScrolled,
|
ValueChanged<double?>? onScrolled,
|
||||||
ScrollPhysics? scrollPhysics,
|
ScrollPhysics? scrollPhysics,
|
||||||
bool? pageSnapping,
|
bool? pageSnapping,
|
||||||
Axis? scrollDirection,
|
Axis? scrollDirection,
|
||||||
bool? pauseAutoPlayOnTouch,
|
bool? pauseAutoPlayOnTouch,
|
||||||
bool? pauseAutoPlayOnManualNavigate,
|
bool? pauseAutoPlayOnManualNavigate,
|
||||||
bool? pauseAutoPlayInFiniteScroll,
|
bool? pauseAutoPlayInFiniteScroll,
|
||||||
PageStorageKey? pageViewKey,
|
PageStorageKey? pageViewKey,
|
||||||
CenterPageEnlargeStrategy? enlargeStrategy,
|
CenterPageEnlargeStrategy? enlargeStrategy,
|
||||||
bool? disableCenter,
|
bool? disableCenter,
|
||||||
Clip? clipBehavior,
|
Clip? clipBehavior,
|
||||||
bool? padEnds}) =>
|
bool? padEnds,
|
||||||
|
}) =>
|
||||||
CarouselOptions(
|
CarouselOptions(
|
||||||
height: height ?? this.height,
|
height: height ?? this.height,
|
||||||
aspectRatio: aspectRatio ?? this.aspectRatio,
|
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||||
|
|
|
@ -1,26 +1,51 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Iconica
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
///
|
||||||
library carousel_slider;
|
library carousel_slider;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'carousel_state.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_controller.dart';
|
||||||
import 'carousel_utils.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_options.dart';
|
||||||
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_state.dart';
|
||||||
import 'carousel_controller.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_utils.dart';
|
||||||
import 'carousel_options.dart';
|
|
||||||
|
|
||||||
export 'carousel_controller.dart';
|
export 'carousel_controller.dart';
|
||||||
export 'carousel_options.dart';
|
export 'carousel_options.dart';
|
||||||
|
|
||||||
typedef ExtendedIndexedWidgetBuilder = Widget Function(
|
typedef ExtendedIndexedWidgetBuilder = Widget Function(
|
||||||
BuildContext context, int index, int realIndex);
|
BuildContext context,
|
||||||
|
int index,
|
||||||
|
int realIndex,
|
||||||
|
);
|
||||||
|
|
||||||
class CarouselSlider extends StatefulWidget {
|
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.
|
/// [CarouselOptions] to create a [CarouselState] with.
|
||||||
final CarouselOptions options;
|
final CarouselOptions options;
|
||||||
|
|
||||||
|
@ -28,8 +53,8 @@ class CarouselSlider extends StatefulWidget {
|
||||||
final List<Widget>? items;
|
final List<Widget>? items;
|
||||||
|
|
||||||
/// The widget item builder that will be used to build item on demand
|
/// 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
|
/// The third argument is the [PageView]'s real index, can be used
|
||||||
/// with Hero.
|
/// to cooperate with Hero.
|
||||||
final ExtendedIndexedWidgetBuilder? itemBuilder;
|
final ExtendedIndexedWidgetBuilder? itemBuilder;
|
||||||
|
|
||||||
/// A [MapController], used to control the map.
|
/// A [MapController], used to control the map.
|
||||||
|
@ -37,37 +62,13 @@ class CarouselSlider extends StatefulWidget {
|
||||||
|
|
||||||
final int? itemCount;
|
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
|
@override
|
||||||
CarouselSliderState createState() => CarouselSliderState();
|
CarouselSliderState createState() => CarouselSliderState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarouselSliderState extends State<CarouselSlider>
|
class CarouselSliderState extends State<CarouselSlider>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
|
CarouselSliderState();
|
||||||
late CarouselControllerImpl carouselController;
|
late CarouselControllerImpl carouselController;
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
|
|
||||||
|
@ -80,8 +81,6 @@ class CarouselSliderState extends State<CarouselSlider>
|
||||||
/// [mode] is related to why the page is being changed.
|
/// [mode] is related to why the page is being changed.
|
||||||
CarouselPageChangedReason mode = CarouselPageChangedReason.controller;
|
CarouselPageChangedReason mode = CarouselPageChangedReason.controller;
|
||||||
|
|
||||||
CarouselSliderState();
|
|
||||||
|
|
||||||
void changeMode(CarouselPageChangedReason mode) {
|
void changeMode(CarouselPageChangedReason mode) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
}
|
}
|
||||||
|
@ -127,36 +126,35 @@ class CarouselSliderState extends State<CarouselSlider>
|
||||||
carouselState!.pageController = pageController;
|
carouselState!.pageController = pageController;
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer? getTimer() {
|
Timer? getTimer() => widget.options.autoPlay
|
||||||
return widget.options.autoPlay
|
? Timer.periodic(widget.options.autoPlayInterval, (_) async {
|
||||||
? Timer.periodic(widget.options.autoPlayInterval, (_) {
|
var route = ModalRoute.of(context);
|
||||||
final route = ModalRoute.of(context);
|
if (route?.isCurrent == false) {
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
nextPage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
CarouselPageChangedReason previousReason = mode;
|
await carouselState!.pageController!
|
||||||
changeMode(CarouselPageChangedReason.timed);
|
.animateToPage(
|
||||||
int nextPage = carouselState!.pageController!.page!.round() + 1;
|
nextPage,
|
||||||
int itemCount = widget.itemCount ?? widget.items!.length;
|
duration: widget.options.autoPlayAnimationDuration,
|
||||||
|
curve: widget.options.autoPlayCurve,
|
||||||
if (nextPage >= itemCount &&
|
)
|
||||||
widget.options.enableInfiniteScroll == false) {
|
.then((_) => changeMode(previousReason));
|
||||||
if (widget.options.pauseAutoPlayInFiniteScroll) {
|
})
|
||||||
clearTimer();
|
: null;
|
||||||
return;
|
|
||||||
}
|
|
||||||
nextPage = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
carouselState!.pageController!
|
|
||||||
.animateToPage(nextPage,
|
|
||||||
duration: widget.options.autoPlayAnimationDuration,
|
|
||||||
curve: widget.options.autoPlayCurve)
|
|
||||||
.then((_) => changeMode(previousReason));
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearTimer() {
|
void clearTimer() {
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
|
@ -170,7 +168,7 @@ class CarouselSliderState extends State<CarouselSlider>
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleAutoPlay() {
|
void handleAutoPlay() {
|
||||||
bool autoPlayEnabled = widget.options.autoPlay;
|
var autoPlayEnabled = widget.options.autoPlay;
|
||||||
|
|
||||||
if (autoPlayEnabled && timer != null) return;
|
if (autoPlayEnabled && timer != null) return;
|
||||||
|
|
||||||
|
@ -193,7 +191,7 @@ class CarouselSliderState extends State<CarouselSlider>
|
||||||
gestures: {
|
gestures: {
|
||||||
_MultipleGestureRecognizer:
|
_MultipleGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>(
|
||||||
() => _MultipleGestureRecognizer(),
|
_MultipleGestureRecognizer.new,
|
||||||
(_MultipleGestureRecognizer instance) {
|
(_MultipleGestureRecognizer instance) {
|
||||||
instance.onStart = (_) {
|
instance.onStart = (_) {
|
||||||
onStart();
|
onStart();
|
||||||
|
@ -204,9 +202,7 @@ class CarouselSliderState extends State<CarouselSlider>
|
||||||
instance.onEnd = (_) {
|
instance.onEnd = (_) {
|
||||||
onPanUp();
|
onPanUp();
|
||||||
};
|
};
|
||||||
instance.onCancel = () {
|
instance.onCancel = onPanUp;
|
||||||
onPanUp();
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
child: NotificationListener(
|
child: NotificationListener(
|
||||||
|
@ -231,14 +227,19 @@ class CarouselSliderState extends State<CarouselSlider>
|
||||||
return Center(child: child);
|
return Center(child: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getEnlargeWrapper(Widget? child,
|
Widget getEnlargeWrapper(
|
||||||
{double? width, double? height, double? scale}) {
|
Widget? child, {
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
double? scale,
|
||||||
|
}) {
|
||||||
if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.height) {
|
if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.height) {
|
||||||
return SizedBox(width: width, height: height, child: child);
|
return SizedBox(width: width, height: height, child: child);
|
||||||
}
|
}
|
||||||
return Transform.scale(
|
return Transform.scale(
|
||||||
scale: scale!,
|
scale: scale,
|
||||||
child: SizedBox(width: width, height: height, child: child));
|
child: SizedBox(width: width, height: height, child: child),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onStart() {
|
void onStart() {
|
||||||
|
@ -266,93 +267,110 @@ class CarouselSliderState extends State<CarouselSlider>
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => getGestureWrapper(
|
||||||
return getGestureWrapper(PageView.builder(
|
PageView.builder(
|
||||||
padEnds: widget.options.padEnds,
|
padEnds: widget.options.padEnds,
|
||||||
scrollBehavior: ScrollConfiguration.of(context).copyWith(
|
scrollBehavior: ScrollConfiguration.of(context).copyWith(
|
||||||
scrollbars: false,
|
scrollbars: false,
|
||||||
overscroll: false,
|
overscroll: false,
|
||||||
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
|
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
|
||||||
),
|
),
|
||||||
clipBehavior: widget.options.clipBehavior,
|
clipBehavior: widget.options.clipBehavior,
|
||||||
physics: widget.options.scrollPhysics,
|
physics: widget.options.scrollPhysics,
|
||||||
scrollDirection: widget.options.scrollDirection,
|
scrollDirection: widget.options.scrollDirection,
|
||||||
pageSnapping: widget.options.pageSnapping,
|
pageSnapping: widget.options.pageSnapping,
|
||||||
controller: carouselState!.pageController,
|
controller: carouselState!.pageController,
|
||||||
reverse: widget.options.reverse,
|
reverse: widget.options.reverse,
|
||||||
itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount,
|
itemCount:
|
||||||
key: widget.options.pageViewKey,
|
widget.options.enableInfiniteScroll ? null : widget.itemCount,
|
||||||
onPageChanged: (int index) {
|
key: widget.options.pageViewKey,
|
||||||
int currentPage = getRealIndex(index + carouselState!.initialPage,
|
onPageChanged: (int index) {
|
||||||
carouselState!.realPage, widget.itemCount);
|
var currentPage = getRealIndex(
|
||||||
if (widget.options.onPageChanged != null) {
|
index + carouselState!.initialPage,
|
||||||
widget.options.onPageChanged!(currentPage, mode);
|
carouselState!.realPage,
|
||||||
}
|
widget.itemCount,
|
||||||
},
|
);
|
||||||
itemBuilder: (BuildContext context, int idx) {
|
widget.options.onPageChanged?.call(currentPage, mode);
|
||||||
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));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
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 {}
|
class _MultipleGestureRecognizer extends PanGestureRecognizer {}
|
||||||
|
|
|
@ -3,9 +3,16 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'carousel_slider.dart';
|
import 'package:flutter_input_library/src/inputs/carousel/carousel_slider.dart';
|
||||||
|
|
||||||
class CarouselState {
|
class CarouselState {
|
||||||
|
CarouselState(
|
||||||
|
this.options,
|
||||||
|
this.onResetTimer,
|
||||||
|
this.onResumeTimer,
|
||||||
|
this.changeMode,
|
||||||
|
);
|
||||||
|
|
||||||
/// The [CarouselOptions] to create this state
|
/// The [CarouselOptions] to create this state
|
||||||
CarouselOptions options;
|
CarouselOptions options;
|
||||||
|
|
||||||
|
@ -30,16 +37,13 @@ class CarouselState {
|
||||||
/// Will be called when using [pageController] to go to next page or
|
/// Will be called when using [pageController] to go to next page or
|
||||||
/// previous page. It will clear the autoPlay timer.
|
/// previous page. It will clear the autoPlay timer.
|
||||||
/// Internal use only
|
/// Internal use only
|
||||||
Function onResetTimer;
|
Function() onResetTimer;
|
||||||
|
|
||||||
/// Will be called when using pageController to go to next page or
|
/// Will be called when using pageController to go to next page or
|
||||||
/// previous page. It will restart the autoPlay timer.
|
/// previous page. It will restart the autoPlay timer.
|
||||||
/// Internal use only
|
/// Internal use only
|
||||||
Function onResumeTimer;
|
Function() onResumeTimer;
|
||||||
|
|
||||||
/// The callback to set the Reason Carousel changed
|
/// The callback to set the Reason Carousel changed
|
||||||
Function(CarouselPageChangedReason) changeMode;
|
Function(CarouselPageChangedReason) changeMode;
|
||||||
|
|
||||||
CarouselState(
|
|
||||||
this.options, this.onResetTimer, this.onResumeTimer, this.changeMode);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,26 +2,27 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
/// Converts an index of a set size to the corresponding index of a collection of another size
|
/// Converts an index of a set size to the corresponding index of a collection
|
||||||
/// as if they were circular.
|
/// of another size as if they were circular.
|
||||||
///
|
///
|
||||||
/// Takes a [position] from collection Foo, a [base] from where Foo's index originated
|
/// Takes a [position] from collection Foo, a [base] from where Foo's index
|
||||||
/// and the [length] of a second collection Baa, for which the correlating index is sought.
|
/// 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.
|
/// For example; We have a Carousel of 10000(simulating infinity) but only 6
|
||||||
/// We need to repeat the images to give the illusion of a never ending stream.
|
/// images. We need to repeat the images to give the illusion of a never
|
||||||
/// By calling [getRealIndex] with position and base we get an offset.
|
/// ending stream. By calling [getRealIndex] with position and base we get
|
||||||
/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image
|
/// an offset. This offset modulo our length, 6, will return a number between
|
||||||
/// to be placed in the given position.
|
/// 0 and 5, which represent the image to be placed in the given position.
|
||||||
int getRealIndex(int position, int base, int? length) {
|
int getRealIndex(int position, int base, int? length) {
|
||||||
final int offset = position - base;
|
var offset = position - base;
|
||||||
return remainder(offset, length);
|
return remainder(offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the remainder of the modulo operation [input] % [source], and adjust it for
|
/// Returns the remainder of the modulo operation [input] % [source], and adjust
|
||||||
/// negative values.
|
/// it for negative values.
|
||||||
int remainder(int input, int? source) {
|
int remainder(int input, int? source) {
|
||||||
if (source == 0) return 0;
|
if (source == 0) return 0;
|
||||||
final int result = input % source!;
|
var result = input % source!;
|
||||||
return result < 0 ? source + result : result;
|
return result < 0 ? source + result : result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,13 @@ enum FlutterFormDateTimeType {
|
||||||
|
|
||||||
class FlutterFormInputDateTime extends StatelessWidget {
|
class FlutterFormInputDateTime extends StatelessWidget {
|
||||||
const FlutterFormInputDateTime({
|
const FlutterFormInputDateTime({
|
||||||
this.decoration,
|
|
||||||
this.style,
|
|
||||||
Key? key,
|
|
||||||
this.label,
|
|
||||||
this.showIcon = true,
|
|
||||||
required this.inputType,
|
required this.inputType,
|
||||||
required this.dateFormat,
|
required this.dateFormat,
|
||||||
|
this.decoration,
|
||||||
|
this.style,
|
||||||
|
super.key,
|
||||||
|
this.label,
|
||||||
|
this.showIcon = true,
|
||||||
this.firstDate,
|
this.firstDate,
|
||||||
this.lastDate,
|
this.lastDate,
|
||||||
this.initialDate,
|
this.initialDate,
|
||||||
|
@ -36,9 +36,7 @@ class FlutterFormInputDateTime extends StatelessWidget {
|
||||||
this.timePickerEntryMode = TimePickerEntryMode.dial,
|
this.timePickerEntryMode = TimePickerEntryMode.dial,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.onTapEnabled = true,
|
this.onTapEnabled = true,
|
||||||
}) : super(
|
});
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
final InputDecoration? decoration;
|
final InputDecoration? decoration;
|
||||||
final Widget? label;
|
final Widget? label;
|
||||||
|
@ -61,28 +59,26 @@ class FlutterFormInputDateTime extends StatelessWidget {
|
||||||
final bool onTapEnabled;
|
final bool onTapEnabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => DateTimeInputField(
|
||||||
return DateTimeInputField(
|
style: style,
|
||||||
style: style,
|
decoration: decoration,
|
||||||
decoration: decoration,
|
autovalidateMode: autovalidateMode,
|
||||||
autovalidateMode: autovalidateMode,
|
validator: validator,
|
||||||
validator: validator,
|
label: label,
|
||||||
label: label,
|
icon: icon,
|
||||||
icon: icon,
|
firstDate: firstDate,
|
||||||
firstDate: firstDate,
|
lastDate: lastDate,
|
||||||
lastDate: lastDate,
|
inputType: inputType,
|
||||||
inputType: inputType,
|
dateFormat: dateFormat,
|
||||||
dateFormat: dateFormat,
|
initialDate: initialDate,
|
||||||
initialDate: initialDate,
|
initialDateTimeRange: initialDateTimeRange,
|
||||||
initialDateTimeRange: initialDateTimeRange,
|
initialTime: initialTime,
|
||||||
initialTime: initialTime,
|
initialValue: initialValue,
|
||||||
initialValue: initialValue,
|
onChanged: (value) => onChanged?.call(value),
|
||||||
onChanged: (value) => onChanged?.call(value),
|
onSaved: (value) => onSaved?.call(value),
|
||||||
onSaved: (value) => onSaved?.call(value),
|
showIcon: showIcon,
|
||||||
showIcon: showIcon,
|
timePickerEntryMode: timePickerEntryMode,
|
||||||
timePickerEntryMode: timePickerEntryMode,
|
enabled: enabled,
|
||||||
enabled: enabled,
|
onTapEnabled: onTapEnabled,
|
||||||
onTapEnabled: onTapEnabled,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,18 @@ import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class DateTimeInputField extends StatefulWidget {
|
class DateTimeInputField extends StatefulWidget {
|
||||||
const DateTimeInputField({
|
const DateTimeInputField({
|
||||||
this.decoration,
|
|
||||||
Key? key,
|
|
||||||
required this.inputType,
|
required this.inputType,
|
||||||
required this.autovalidateMode,
|
required this.autovalidateMode,
|
||||||
this.label,
|
|
||||||
this.showIcon = true,
|
|
||||||
this.icon,
|
|
||||||
required this.dateFormat,
|
required this.dateFormat,
|
||||||
required this.firstDate,
|
required this.firstDate,
|
||||||
required this.lastDate,
|
required this.lastDate,
|
||||||
|
required this.timePickerEntryMode,
|
||||||
|
required this.style,
|
||||||
|
this.decoration,
|
||||||
|
super.key,
|
||||||
|
this.label,
|
||||||
|
this.showIcon = true,
|
||||||
|
this.icon,
|
||||||
this.initialDate,
|
this.initialDate,
|
||||||
this.initialTime,
|
this.initialTime,
|
||||||
this.initialDateTimeRange,
|
this.initialDateTimeRange,
|
||||||
|
@ -27,13 +29,9 @@ class DateTimeInputField extends StatefulWidget {
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
this.validator,
|
this.validator,
|
||||||
required this.timePickerEntryMode,
|
|
||||||
required this.style,
|
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.onTapEnabled = true,
|
this.onTapEnabled = true,
|
||||||
}) : super(
|
});
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
final InputDecoration? decoration;
|
final InputDecoration? decoration;
|
||||||
final AutovalidateMode autovalidateMode;
|
final AutovalidateMode autovalidateMode;
|
||||||
|
@ -93,12 +91,14 @@ class _DateInputFieldState extends State<DateTimeInputField> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Future<String> getInputFromUser(FlutterFormDateTimeType inputType,
|
Future<String> getInputFromUser(
|
||||||
[DateFormat? dateFormat]) async {
|
FlutterFormDateTimeType inputType, [
|
||||||
String userInput = '';
|
DateFormat? dateFormat,
|
||||||
|
]) async {
|
||||||
|
var userInput = '';
|
||||||
switch (inputType) {
|
switch (inputType) {
|
||||||
case FlutterFormDateTimeType.date:
|
case FlutterFormDateTimeType.date:
|
||||||
DateTime? unformatted = await showDatePicker(
|
var unformatted = await showDatePicker(
|
||||||
initialDate: initialDate,
|
initialDate: initialDate,
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: firstDate,
|
firstDate: firstDate,
|
||||||
|
@ -112,7 +112,7 @@ class _DateInputFieldState extends State<DateTimeInputField> {
|
||||||
await getInputFromUser(FlutterFormDateTimeType.date)
|
await getInputFromUser(FlutterFormDateTimeType.date)
|
||||||
.then((value) async {
|
.then((value) async {
|
||||||
if (value != '') {
|
if (value != '') {
|
||||||
String secondInput =
|
var secondInput =
|
||||||
await getInputFromUser(FlutterFormDateTimeType.time);
|
await getInputFromUser(FlutterFormDateTimeType.time);
|
||||||
if (secondInput != '') {
|
if (secondInput != '') {
|
||||||
var date = widget.dateFormat.parse(value);
|
var date = widget.dateFormat.parse(value);
|
||||||
|
@ -120,49 +120,51 @@ class _DateInputFieldState extends State<DateTimeInputField> {
|
||||||
? dateFormat.parse('01 01 1970 $secondInput')
|
? dateFormat.parse('01 01 1970 $secondInput')
|
||||||
: DateFormat('dd MM yyyy HH:mm')
|
: DateFormat('dd MM yyyy HH:mm')
|
||||||
.parse('01 01 1970 $secondInput');
|
.parse('01 01 1970 $secondInput');
|
||||||
userInput = widget.dateFormat.format(DateTime(
|
userInput = widget.dateFormat.format(
|
||||||
date.year,
|
DateTime(
|
||||||
date.month,
|
date.year,
|
||||||
date.day,
|
date.month,
|
||||||
time.hour,
|
date.day,
|
||||||
time.minute,
|
time.hour,
|
||||||
));
|
time.minute,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case FlutterFormDateTimeType.range:
|
case FlutterFormDateTimeType.range:
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
userInput = (await showDateRangePicker(
|
userInput = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: firstDate,
|
firstDate: firstDate,
|
||||||
lastDate: lastDate,
|
lastDate: lastDate,
|
||||||
initialDateRange: initialDateRange)
|
initialDateRange: initialDateRange,
|
||||||
.then((value) {
|
).then(
|
||||||
return value != null
|
(value) => value != null
|
||||||
? '${widget.dateFormat.format(value.start)} - ${widget.dateFormat.format(value.end)}'
|
? '${widget.dateFormat.format(value.start)} -'
|
||||||
: '';
|
'${widget.dateFormat.format(value.end)}'
|
||||||
}))
|
: '',
|
||||||
.toString();
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FlutterFormDateTimeType.time:
|
case FlutterFormDateTimeType.time:
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
userInput = await showTimePicker(
|
userInput = await showTimePicker(
|
||||||
initialEntryMode: widget.timePickerEntryMode,
|
initialEntryMode: widget.timePickerEntryMode,
|
||||||
builder: (BuildContext context, Widget? child) {
|
builder: (BuildContext context, Widget? child) => MediaQuery(
|
||||||
return MediaQuery(
|
data: MediaQuery.of(context)
|
||||||
data: MediaQuery.of(context)
|
.copyWith(alwaysUse24HourFormat: true),
|
||||||
.copyWith(alwaysUse24HourFormat: true),
|
child: child!,
|
||||||
child: child!,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
context: context,
|
context: context,
|
||||||
initialTime: initialTimeOfDay,
|
initialTime: initialTimeOfDay,
|
||||||
).then((value) => value == null
|
).then(
|
||||||
? ''
|
(value) => value == null
|
||||||
: MaterialLocalizations.of(context)
|
? ''
|
||||||
.formatTimeOfDay(value, alwaysUse24HourFormat: true));
|
: MaterialLocalizations.of(context)
|
||||||
|
.formatTimeOfDay(value, alwaysUse24HourFormat: true),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return userInput;
|
return userInput;
|
||||||
|
@ -178,7 +180,7 @@ class _DateInputFieldState extends State<DateTimeInputField> {
|
||||||
onSaved: (value) => widget.onSaved?.call(value),
|
onSaved: (value) => widget.onSaved?.call(value),
|
||||||
onTap: widget.onTapEnabled
|
onTap: widget.onTapEnabled
|
||||||
? () async {
|
? () async {
|
||||||
String userInput = await getInputFromUser(
|
var userInput = await getInputFromUser(
|
||||||
widget.inputType,
|
widget.inputType,
|
||||||
DateFormat('dd MM yyyy HH:mm'),
|
DateFormat('dd MM yyyy HH:mm'),
|
||||||
);
|
);
|
||||||
|
@ -194,7 +196,7 @@ class _DateInputFieldState extends State<DateTimeInputField> {
|
||||||
InputDecoration(
|
InputDecoration(
|
||||||
suffixIcon: widget.showIcon ? Icon(widget.icon) : null,
|
suffixIcon: widget.showIcon ? Icon(widget.icon) : null,
|
||||||
focusColor: Theme.of(context).primaryColor,
|
focusColor: Theme.of(context).primaryColor,
|
||||||
label: widget.label ?? const Text("Date"),
|
label: widget.label ?? const Text('Date'),
|
||||||
),
|
),
|
||||||
enabled: widget.enabled,
|
enabled: widget.enabled,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export 'carousel/carousel.dart';
|
export 'carousel/carousel.dart';
|
||||||
|
export 'date_picker/date_picker.dart';
|
||||||
export 'number_picker/number_picker.dart';
|
export 'number_picker/number_picker.dart';
|
||||||
export 'text/password.dart';
|
export 'scroll_picker/scroll_picker.dart';
|
||||||
export 'text/plain_text.dart';
|
|
||||||
export 'slider/slider.dart';
|
export 'slider/slider.dart';
|
||||||
export 'switch/switch.dart';
|
export 'switch/switch.dart';
|
||||||
export 'date_picker/date_picker.dart';
|
export 'text/password.dart';
|
||||||
export 'scroll_picker/scroll_picker.dart';
|
export 'text/plain_text.dart';
|
||||||
|
|
|
@ -6,9 +6,30 @@ import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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 {
|
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 minValue;
|
||||||
final int maxValue;
|
final int maxValue;
|
||||||
final double value;
|
final double value;
|
||||||
|
@ -24,47 +45,26 @@ class DecimalNumberPicker extends StatelessWidget {
|
||||||
final TextMapper? decimalTextMapper;
|
final TextMapper? decimalTextMapper;
|
||||||
final bool integerZeroPad;
|
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;
|
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;
|
final Decoration? decimalDecoration;
|
||||||
|
|
||||||
/// Inidcates how many decimal places to show
|
/// 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...]
|
/// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...]
|
||||||
final int decimalPlaces;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMax = value.floor() == maxValue;
|
var isMax = value.floor() == maxValue;
|
||||||
final decimalValue = isMax
|
var decimalValue = isMax
|
||||||
? 0
|
? 0
|
||||||
: ((value - value.floorToDouble()) * math.pow(10, decimalPlaces))
|
: ((value - value.floorToDouble()) * math.pow(10, decimalPlaces))
|
||||||
.round();
|
.round();
|
||||||
final doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1;
|
var doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -102,15 +102,15 @@ class DecimalNumberPicker extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onIntChanged(int intValue) {
|
void _onIntChanged(int intValue) {
|
||||||
final newValue =
|
var newValue = (value - value.floor() + intValue).clamp(minValue, maxValue);
|
||||||
(value - value.floor() + intValue).clamp(minValue, maxValue);
|
|
||||||
onChanged(newValue.toDouble());
|
onChanged(newValue.toDouble());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDoubleChanged(int doubleValue) {
|
void _onDoubleChanged(int doubleValue) {
|
||||||
final decimalPart = double.parse(
|
var decimalPart = double.parse(
|
||||||
(doubleValue * math.pow(10, -decimalPlaces))
|
(doubleValue * math.pow(10, -decimalPlaces))
|
||||||
.toStringAsFixed(decimalPlaces));
|
.toStringAsFixed(decimalPlaces),
|
||||||
|
);
|
||||||
onChanged(value.floor() + decimalPart);
|
onChanged(value.floor() + decimalPart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
library infinite_listview;
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||||
|
@ -18,14 +16,14 @@ import 'package:flutter/widgets.dart';
|
||||||
class InfiniteListView extends StatefulWidget {
|
class InfiniteListView extends StatefulWidget {
|
||||||
/// See [ListView.builder]
|
/// See [ListView.builder]
|
||||||
const InfiniteListView.builder({
|
const InfiniteListView.builder({
|
||||||
Key? key,
|
required this.itemBuilder,
|
||||||
|
super.key,
|
||||||
this.scrollDirection = Axis.vertical,
|
this.scrollDirection = Axis.vertical,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.physics,
|
this.physics,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.itemExtent,
|
this.itemExtent,
|
||||||
required this.itemBuilder,
|
|
||||||
this.itemCount,
|
this.itemCount,
|
||||||
this.addAutomaticKeepAlives = true,
|
this.addAutomaticKeepAlives = true,
|
||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
|
@ -36,19 +34,18 @@ class InfiniteListView extends StatefulWidget {
|
||||||
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.clipBehavior = Clip.hardEdge,
|
this.clipBehavior = Clip.hardEdge,
|
||||||
}) : separatorBuilder = null,
|
}) : separatorBuilder = null;
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// See [ListView.separated]
|
/// See [ListView.separated]
|
||||||
const InfiniteListView.separated({
|
const InfiniteListView.separated({
|
||||||
Key? key,
|
required this.itemBuilder,
|
||||||
|
required this.separatorBuilder,
|
||||||
|
super.key,
|
||||||
this.scrollDirection = Axis.vertical,
|
this.scrollDirection = Axis.vertical,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.physics,
|
this.physics,
|
||||||
this.padding,
|
this.padding,
|
||||||
required this.itemBuilder,
|
|
||||||
required this.separatorBuilder,
|
|
||||||
this.itemCount,
|
this.itemCount,
|
||||||
this.addAutomaticKeepAlives = true,
|
this.addAutomaticKeepAlives = true,
|
||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
|
@ -59,8 +56,7 @@ class InfiniteListView extends StatefulWidget {
|
||||||
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.clipBehavior = Clip.hardEdge,
|
this.clipBehavior = Clip.hardEdge,
|
||||||
}) : itemExtent = null,
|
}) : itemExtent = null;
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// See: [ScrollView.scrollDirection]
|
/// See: [ScrollView.scrollDirection]
|
||||||
final Axis scrollDirection;
|
final Axis scrollDirection;
|
||||||
|
@ -153,20 +149,20 @@ class InfiniteListViewState extends State<InfiniteListView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<Widget> slivers = _buildSlivers(context, negative: false);
|
var slivers = _buildSlivers(context, negative: false);
|
||||||
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
|
var negativeSlivers = _buildSlivers(context, negative: true);
|
||||||
final AxisDirection axisDirection = _getDirection(context);
|
var axisDirection = _getDirection(context);
|
||||||
final scrollPhysics =
|
var scrollPhysics = widget.physics ?? const AlwaysScrollableScrollPhysics();
|
||||||
widget.physics ?? const AlwaysScrollableScrollPhysics();
|
|
||||||
return Scrollable(
|
return Scrollable(
|
||||||
axisDirection: axisDirection,
|
axisDirection: axisDirection,
|
||||||
controller: _effectiveController,
|
controller: _effectiveController,
|
||||||
physics: scrollPhysics,
|
physics: scrollPhysics,
|
||||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
viewportBuilder: (BuildContext context, ViewportOffset offset) => Builder(
|
||||||
return Builder(builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
/// Build negative [ScrollPosition] for the negative scrolling [Viewport].
|
/// Build negative [ScrollPosition] for the negative scrolling
|
||||||
final state = Scrollable.of(context);
|
/// [Viewport].
|
||||||
final negativeOffset = _InfiniteScrollPosition(
|
var state = Scrollable.of(context);
|
||||||
|
var negativeOffset = _InfiniteScrollPosition(
|
||||||
physics: scrollPhysics,
|
physics: scrollPhysics,
|
||||||
context: state,
|
context: state,
|
||||||
initialPixels: -offset.pixels,
|
initialPixels: -offset.pixels,
|
||||||
|
@ -174,12 +170,14 @@ class InfiniteListViewState extends State<InfiniteListView> {
|
||||||
negativeScroll: true,
|
negativeScroll: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
|
/// Keep the negative scrolling [Viewport] positioned to the
|
||||||
|
/// [ScrollPosition].
|
||||||
offset.addListener(() {
|
offset.addListener(() {
|
||||||
negativeOffset._forceNegativePixels(offset.pixels);
|
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(
|
return Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Viewport(
|
Viewport(
|
||||||
|
@ -198,19 +196,21 @@ class InfiniteListViewState extends State<InfiniteListView> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AxisDirection _getDirection(BuildContext context) {
|
AxisDirection _getDirection(BuildContext context) =>
|
||||||
return getAxisDirectionFromAxisReverseAndDirectionality(
|
getAxisDirectionFromAxisReverseAndDirectionality(
|
||||||
context, widget.scrollDirection, widget.reverse);
|
context,
|
||||||
}
|
widget.scrollDirection,
|
||||||
|
widget.reverse,
|
||||||
|
);
|
||||||
|
|
||||||
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
|
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
|
||||||
final itemExtent = widget.itemExtent;
|
var itemExtent = widget.itemExtent;
|
||||||
final padding = widget.padding ?? EdgeInsets.zero;
|
var padding = widget.padding ?? EdgeInsets.zero;
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: negative
|
padding: negative
|
||||||
|
@ -228,36 +228,35 @@ class InfiniteListViewState extends State<InfiniteListView> {
|
||||||
? negativeChildrenDelegate
|
? negativeChildrenDelegate
|
||||||
: positiveChildrenDelegate,
|
: positiveChildrenDelegate,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
SliverChildDelegate get negativeChildrenDelegate {
|
SliverChildDelegate get negativeChildrenDelegate =>
|
||||||
return SliverChildBuilderDelegate(
|
SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
final separatorBuilder = widget.separatorBuilder;
|
var separatorBuilder = widget.separatorBuilder;
|
||||||
if (separatorBuilder != null) {
|
if (separatorBuilder != null) {
|
||||||
final itemIndex = (-1 - index) ~/ 2;
|
var itemIndex = (-1 - index) ~/ 2;
|
||||||
return index.isOdd
|
return index.isOdd
|
||||||
? widget.itemBuilder(context, itemIndex)
|
? widget.itemBuilder(context, itemIndex)
|
||||||
: separatorBuilder(context, itemIndex);
|
: separatorBuilder(context, itemIndex);
|
||||||
} else {
|
} else {
|
||||||
return widget.itemBuilder(context, -1 - index);
|
return widget.itemBuilder(context, -1 - index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
childCount: widget.itemCount,
|
childCount: widget.itemCount,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
SliverChildDelegate get positiveChildrenDelegate {
|
SliverChildDelegate get positiveChildrenDelegate {
|
||||||
final separatorBuilder = widget.separatorBuilder;
|
var separatorBuilder = widget.separatorBuilder;
|
||||||
final itemCount = widget.itemCount;
|
var itemCount = widget.itemCount;
|
||||||
return SliverChildBuilderDelegate(
|
return SliverChildBuilderDelegate(
|
||||||
(separatorBuilder != null)
|
(separatorBuilder != null)
|
||||||
? (BuildContext context, int index) {
|
? (BuildContext context, int index) {
|
||||||
final itemIndex = index ~/ 2;
|
var itemIndex = index ~/ 2;
|
||||||
return index.isEven
|
return index.isEven
|
||||||
? widget.itemBuilder(context, itemIndex)
|
? widget.itemBuilder(context, itemIndex)
|
||||||
: separatorBuilder(context, itemIndex);
|
: separatorBuilder(context, itemIndex);
|
||||||
|
@ -276,67 +275,82 @@ class InfiniteListViewState extends State<InfiniteListView> {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties
|
properties
|
||||||
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
|
.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(
|
properties.add(
|
||||||
DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null));
|
FlagProperty(
|
||||||
|
'reverse',
|
||||||
|
value: widget.reverse,
|
||||||
|
ifTrue: 'reversed',
|
||||||
|
showName: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
properties.add(
|
properties.add(
|
||||||
DoubleProperty('cacheExtent', widget.cacheExtent, defaultValue: null));
|
DiagnosticsProperty<ScrollController>(
|
||||||
}
|
'controller',
|
||||||
}
|
widget.controller,
|
||||||
|
showName: false,
|
||||||
/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
|
defaultValue: null,
|
||||||
class InfiniteScrollController extends ScrollController {
|
),
|
||||||
/// Creates a new [InfiniteScrollController]
|
);
|
||||||
InfiniteScrollController({
|
properties.add(
|
||||||
double initialScrollOffset = 0.0,
|
DiagnosticsProperty<ScrollPhysics>(
|
||||||
bool keepScrollOffset = true,
|
'physics',
|
||||||
String? debugLabel,
|
widget.physics,
|
||||||
}) : super(
|
showName: false,
|
||||||
initialScrollOffset: initialScrollOffset,
|
defaultValue: null,
|
||||||
keepScrollOffset: keepScrollOffset,
|
),
|
||||||
debugLabel: debugLabel,
|
);
|
||||||
);
|
properties.add(
|
||||||
|
DiagnosticsProperty<EdgeInsetsGeometry>(
|
||||||
@override
|
'padding',
|
||||||
ScrollPosition createScrollPosition(ScrollPhysics physics,
|
widget.padding,
|
||||||
ScrollContext context, ScrollPosition? oldPosition) {
|
defaultValue: null,
|
||||||
return _InfiniteScrollPosition(
|
),
|
||||||
physics: physics,
|
);
|
||||||
context: context,
|
properties.add(
|
||||||
initialPixels: initialScrollOffset,
|
DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null),
|
||||||
keepScrollOffset: keepScrollOffset,
|
);
|
||||||
oldPosition: oldPosition,
|
properties.add(
|
||||||
debugLabel: debugLabel,
|
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 {
|
class _InfiniteScrollPosition extends ScrollPositionWithSingleContext {
|
||||||
_InfiniteScrollPosition({
|
_InfiniteScrollPosition({
|
||||||
required ScrollPhysics physics,
|
required super.physics,
|
||||||
required ScrollContext context,
|
required super.context,
|
||||||
double? initialPixels = 0.0,
|
super.initialPixels,
|
||||||
bool keepScrollOffset = true,
|
super.keepScrollOffset,
|
||||||
ScrollPosition? oldPosition,
|
super.oldPosition,
|
||||||
String? debugLabel,
|
super.debugLabel,
|
||||||
this.negativeScroll = false,
|
this.negativeScroll = false,
|
||||||
}) : super(
|
});
|
||||||
physics: physics,
|
|
||||||
context: context,
|
|
||||||
initialPixels: initialPixels,
|
|
||||||
keepScrollOffset: keepScrollOffset,
|
|
||||||
oldPosition: oldPosition,
|
|
||||||
debugLabel: debugLabel,
|
|
||||||
);
|
|
||||||
|
|
||||||
final bool negativeScroll;
|
final bool negativeScroll;
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,18 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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 {
|
class FlutterFormInputNumberPicker extends StatelessWidget {
|
||||||
const FlutterFormInputNumberPicker({
|
const FlutterFormInputNumberPicker({
|
||||||
Key? key,
|
super.key,
|
||||||
Widget? label,
|
|
||||||
this.minValue = 0,
|
this.minValue = 0,
|
||||||
this.maxValue = 100,
|
this.maxValue = 100,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.initialValue,
|
this.initialValue,
|
||||||
this.validator,
|
this.validator,
|
||||||
}) : assert(minValue < maxValue),
|
}) : assert(minValue < maxValue, 'minValue must be less than maxValue');
|
||||||
super(
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
|
|
||||||
final int minValue;
|
final int minValue;
|
||||||
final int maxValue;
|
final int maxValue;
|
||||||
|
@ -29,45 +25,37 @@ class FlutterFormInputNumberPicker extends StatelessWidget {
|
||||||
final Function(int?)? onChanged;
|
final Function(int?)? onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => NumberPickerFormField(
|
||||||
return NumberPickerFormField(
|
minValue: minValue,
|
||||||
minValue: minValue,
|
maxValue: maxValue,
|
||||||
maxValue: maxValue,
|
onSaved: (value) => onSaved?.call(value),
|
||||||
onSaved: (value) => onSaved?.call(value),
|
validator: (value) => validator?.call(value),
|
||||||
validator: (value) => validator?.call(value),
|
onChanged: (value) => onChanged?.call(value),
|
||||||
onChanged: (value) => onChanged?.call(value),
|
initialValue: initialValue ?? 0,
|
||||||
initialValue: initialValue ?? 0,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NumberPickerFormField extends FormField<int> {
|
class NumberPickerFormField extends FormField<int> {
|
||||||
NumberPickerFormField({
|
NumberPickerFormField({
|
||||||
Key? key,
|
required FormFieldSetter<int> super.onSaved,
|
||||||
required FormFieldSetter<int> onSaved,
|
required FormFieldValidator<int> super.validator,
|
||||||
required FormFieldValidator<int> validator,
|
super.key,
|
||||||
void Function(int value)? onChanged,
|
void Function(int value)? onChanged,
|
||||||
int initialValue = 0,
|
int super.initialValue = 0,
|
||||||
bool autovalidate = false,
|
|
||||||
int minValue = 0,
|
int minValue = 0,
|
||||||
int maxValue = 100,
|
int maxValue = 100,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
builder: (FormFieldState<int> state) => NumberPicker(
|
||||||
onSaved: onSaved,
|
minValue: minValue,
|
||||||
validator: validator,
|
maxValue: maxValue,
|
||||||
initialValue: initialValue,
|
value: initialValue,
|
||||||
builder: (FormFieldState<int> state) {
|
onChanged: (int value) {
|
||||||
return NumberPicker(
|
onChanged?.call(value);
|
||||||
minValue: minValue,
|
|
||||||
maxValue: maxValue,
|
|
||||||
value: initialValue,
|
|
||||||
onChanged: (int value) {
|
|
||||||
onChanged?.call(value);
|
|
||||||
|
|
||||||
state.didChange(value);
|
state.didChange(value);
|
||||||
},
|
},
|
||||||
itemHeight: 35,
|
itemHeight: 35,
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,36 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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);
|
typedef TextMapper = String Function(String numberText);
|
||||||
|
|
||||||
class NumberPicker extends StatefulWidget {
|
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
|
/// Min value user can pick
|
||||||
final int minValue;
|
final int minValue;
|
||||||
|
|
||||||
|
@ -43,7 +66,8 @@ class NumberPicker extends StatefulWidget {
|
||||||
/// Style of non-selected numbers. If null, it uses Theme's bodyText2
|
/// Style of non-selected numbers. If null, it uses Theme's bodyText2
|
||||||
final TextStyle? textStyle;
|
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;
|
final TextStyle? selectedTextStyle;
|
||||||
|
|
||||||
/// Whether to trigger haptic pulses or not
|
/// Whether to trigger haptic pulses or not
|
||||||
|
@ -60,28 +84,6 @@ class NumberPicker extends StatefulWidget {
|
||||||
|
|
||||||
final bool infiniteLoop;
|
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
|
@override
|
||||||
NumberPickerState createState() => NumberPickerState();
|
NumberPickerState createState() => NumberPickerState();
|
||||||
}
|
}
|
||||||
|
@ -97,7 +99,7 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
|
|
||||||
value = widget.value;
|
value = widget.value;
|
||||||
|
|
||||||
final initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent;
|
var initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent;
|
||||||
if (widget.infiniteLoop) {
|
if (widget.infiniteLoop) {
|
||||||
_scrollController =
|
_scrollController =
|
||||||
InfiniteScrollController(initialScrollOffset: initialOffset);
|
InfiniteScrollController(initialScrollOffset: initialOffset);
|
||||||
|
@ -107,14 +109,14 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
_scrollController.addListener(_scrollListener);
|
_scrollController.addListener(_scrollListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollListener() {
|
Future<void> _scrollListener() async {
|
||||||
var indexOfMiddleElement = (_scrollController.offset / itemExtent).round();
|
var indexOfMiddleElement = (_scrollController.offset / itemExtent).round();
|
||||||
if (widget.infiniteLoop) {
|
if (widget.infiniteLoop) {
|
||||||
indexOfMiddleElement %= itemCount;
|
indexOfMiddleElement %= itemCount;
|
||||||
} else {
|
} else {
|
||||||
indexOfMiddleElement = indexOfMiddleElement.clamp(0, itemCount - 1);
|
indexOfMiddleElement = indexOfMiddleElement.clamp(0, itemCount - 1);
|
||||||
}
|
}
|
||||||
final intValueInTheMiddle =
|
var intValueInTheMiddle =
|
||||||
_intValueFromIndex(indexOfMiddleElement + additionalItemsOnEachSide);
|
_intValueFromIndex(indexOfMiddleElement + additionalItemsOnEachSide);
|
||||||
|
|
||||||
if (value != intValueInTheMiddle) {
|
if (value != intValueInTheMiddle) {
|
||||||
|
@ -124,12 +126,12 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
|
|
||||||
widget.onChanged(intValueInTheMiddle);
|
widget.onChanged(intValueInTheMiddle);
|
||||||
if (widget.haptics) {
|
if (widget.haptics) {
|
||||||
HapticFeedback.selectionClick();
|
await HapticFeedback.selectionClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
const Duration(milliseconds: 100),
|
const Duration(milliseconds: 100),
|
||||||
() => _maybeCenterValue(),
|
_maybeCenterValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +139,7 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
void didUpdateWidget(NumberPicker oldWidget) {
|
void didUpdateWidget(NumberPicker oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.value != value) {
|
if (oldWidget.value != value) {
|
||||||
_maybeCenterValue();
|
unawaited(_maybeCenterValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,78 +161,77 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
int get additionalItemsOnEachSide => (widget.itemCount - 1) ~/ 2;
|
int get additionalItemsOnEachSide => (widget.itemCount - 1) ~/ 2;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => SizedBox(
|
||||||
return SizedBox(
|
width: widget.axis == Axis.vertical
|
||||||
width: widget.axis == Axis.vertical
|
? widget.itemWidth
|
||||||
? widget.itemWidth
|
: widget.itemCount * widget.itemWidth,
|
||||||
: widget.itemCount * widget.itemWidth,
|
height: widget.axis == Axis.vertical
|
||||||
height: widget.axis == Axis.vertical
|
? widget.itemCount * widget.itemHeight
|
||||||
? widget.itemCount * widget.itemHeight
|
: widget.itemHeight,
|
||||||
: widget.itemHeight,
|
child: NotificationListener<ScrollEndNotification>(
|
||||||
child: NotificationListener<ScrollEndNotification>(
|
onNotification: (not) {
|
||||||
onNotification: (not) {
|
if (not.dragDetails?.primaryVelocity == 0) {
|
||||||
if (not.dragDetails?.primaryVelocity == 0) {
|
Future.microtask(_maybeCenterValue);
|
||||||
Future.microtask(() => _maybeCenterValue());
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
},
|
||||||
},
|
child: ScrollConfiguration(
|
||||||
child: ScrollConfiguration(
|
behavior:
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 45,
|
height: 45,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
color: const Color(0xFFD8D8D8).withOpacity(0.50),
|
color: const Color(0xFFD8D8D8).withOpacity(0.50),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (widget.infiniteLoop)
|
||||||
if (widget.infiniteLoop)
|
InfiniteListView.builder(
|
||||||
InfiniteListView.builder(
|
scrollDirection: widget.axis,
|
||||||
scrollDirection: widget.axis,
|
controller: _scrollController as InfiniteScrollController,
|
||||||
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,
|
itemExtent: itemExtent,
|
||||||
itemBuilder: _itemBuilder,
|
decoration: widget.decoration,
|
||||||
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) {
|
Widget _itemBuilder(BuildContext context, int index) {
|
||||||
final themeData = Theme.of(context);
|
var themeData = Theme.of(context);
|
||||||
final defaultStyle = widget.textStyle ?? themeData.textTheme.bodyMedium;
|
var defaultStyle = widget.textStyle ?? themeData.textTheme.bodyMedium;
|
||||||
final selectedStyle = widget.selectedTextStyle ??
|
var selectedStyle = widget.selectedTextStyle ??
|
||||||
themeData.textTheme.headlineSmall
|
themeData.textTheme.headlineSmall
|
||||||
?.copyWith(color: themeData.highlightColor);
|
?.copyWith(color: themeData.highlightColor);
|
||||||
|
|
||||||
final valueFromIndex = _intValueFromIndex(index % itemCount);
|
var valueFromIndex = _intValueFromIndex(index % itemCount);
|
||||||
final isExtra = !widget.infiniteLoop &&
|
var isExtra = !widget.infiniteLoop &&
|
||||||
(index < additionalItemsOnEachSide ||
|
(index < additionalItemsOnEachSide ||
|
||||||
index >= listItemsCount - 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()
|
? const SizedBox.shrink()
|
||||||
: Text(
|
: Text(
|
||||||
_getDisplayedValue(valueFromIndex),
|
_getDisplayedValue(valueFromIndex),
|
||||||
|
@ -246,7 +247,7 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getDisplayedValue(int value) {
|
String _getDisplayedValue(int value) {
|
||||||
final text = widget.zeroPad
|
var text = widget.zeroPad
|
||||||
? value.toString().padLeft(widget.maxValue.toString().length, '0')
|
? value.toString().padLeft(widget.maxValue.toString().length, '0')
|
||||||
: value.toString();
|
: value.toString();
|
||||||
if (widget.textMapper != null) {
|
if (widget.textMapper != null) {
|
||||||
|
@ -257,21 +258,22 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
int _intValueFromIndex(int index) {
|
int _intValueFromIndex(int index) {
|
||||||
index -= additionalItemsOnEachSide;
|
var newIndex = index;
|
||||||
index %= itemCount;
|
newIndex -= additionalItemsOnEachSide;
|
||||||
return widget.minValue + index * widget.step;
|
newIndex %= itemCount;
|
||||||
|
return widget.minValue + newIndex * widget.step;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _maybeCenterValue() {
|
Future<void> _maybeCenterValue() async {
|
||||||
if (_scrollController.hasClients && !isScrolling) {
|
if (_scrollController.hasClients && !isScrolling) {
|
||||||
int diff = value - widget.minValue;
|
var diff = value - widget.minValue;
|
||||||
int index = diff ~/ widget.step;
|
var index = diff ~/ widget.step;
|
||||||
if (widget.infiniteLoop) {
|
if (widget.infiniteLoop) {
|
||||||
final offset = _scrollController.offset + 0.5 * itemExtent;
|
var offset = _scrollController.offset + 0.5 * itemExtent;
|
||||||
final cycles = (offset / (itemCount * itemExtent)).floor();
|
var cycles = (offset / (itemCount * itemExtent)).floor();
|
||||||
index += cycles * itemCount;
|
index += cycles * itemCount;
|
||||||
}
|
}
|
||||||
_scrollController.animateTo(
|
await _scrollController.animateTo(
|
||||||
index * itemExtent,
|
index * itemExtent,
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
|
@ -281,29 +283,25 @@ class NumberPickerState extends State<NumberPicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NumberPickerSelectedItemDecoration extends StatelessWidget {
|
class _NumberPickerSelectedItemDecoration extends StatelessWidget {
|
||||||
|
const _NumberPickerSelectedItemDecoration({
|
||||||
|
required this.axis,
|
||||||
|
required this.itemExtent,
|
||||||
|
required this.decoration,
|
||||||
|
});
|
||||||
final Axis axis;
|
final Axis axis;
|
||||||
final double itemExtent;
|
final double itemExtent;
|
||||||
final Decoration? decoration;
|
final Decoration? decoration;
|
||||||
|
|
||||||
const _NumberPickerSelectedItemDecoration({
|
|
||||||
Key? key,
|
|
||||||
required this.axis,
|
|
||||||
required this.itemExtent,
|
|
||||||
required this.decoration,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Center(
|
||||||
return Center(
|
child: IgnorePointer(
|
||||||
child: IgnorePointer(
|
child: Container(
|
||||||
child: Container(
|
width: isVertical ? double.infinity : itemExtent,
|
||||||
width: isVertical ? double.infinity : itemExtent,
|
height: isVertical ? itemExtent : double.infinity,
|
||||||
height: isVertical ? itemExtent : double.infinity,
|
decoration: decoration,
|
||||||
decoration: decoration,
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isVertical => axis == Axis.vertical;
|
bool get isVertical => axis == Axis.vertical;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export 'scroll_picker_widget.dart';
|
|
||||||
export 'scroll_picker_decoration.dart';
|
export 'scroll_picker_decoration.dart';
|
||||||
export 'scroll_picker_type_extensions.dart';
|
export 'scroll_picker_type_extensions.dart';
|
||||||
|
export 'scroll_picker_widget.dart';
|
||||||
|
|
|
@ -22,57 +22,87 @@ class ScrollPickerDecoration {
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Ability to provide your own builder for the scroll items
|
/// Ability to provide your own builder for the scroll items
|
||||||
|
// ignore: avoid_annotating_with_dynamic
|
||||||
final Widget Function(BuildContext context, int index, dynamic value)?
|
final Widget Function(BuildContext context, int index, dynamic value)?
|
||||||
scrollItemBuilder;
|
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;
|
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;
|
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;
|
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;
|
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.
|
/// Defaults to an arbitrary but aesthetically reasonable number of 2.0.
|
||||||
///
|
///
|
||||||
/// Must not be null and must be positive.
|
/// Must not be null and must be positive.
|
||||||
final double diameterRatio;
|
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;
|
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.
|
/// 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.
|
/// Must be greater than or equal to 0, and less than or equal to 1.
|
||||||
final double overAndUnderCenterOpacity;
|
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;
|
final double offAxisFraction;
|
||||||
|
|
||||||
/// Whether to use the magnifier for the center item of the wheel.
|
/// 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 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.
|
/// Must be positive.
|
||||||
final double magnification;
|
final double magnification;
|
||||||
|
|
||||||
/// The angular compactness of the children on the wheel.
|
/// 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.
|
/// Must not be null and must be positive.
|
||||||
///
|
///
|
||||||
|
@ -100,8 +140,11 @@ class ScrollPickerDecoration {
|
||||||
|
|
||||||
/// Whether to paint children inside the viewport only.
|
/// 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;
|
final bool renderChildrenOutsideViewport;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,8 @@ class _ScrollPickerState extends State<ScrollPicker> {
|
||||||
|
|
||||||
if (newIndex != selectedIndex) {
|
if (newIndex != selectedIndex) {
|
||||||
widget.onChanged.call(
|
widget.onChanged.call(
|
||||||
(scrollController.offset / widget.decoration.itemHeight).round());
|
(scrollController.offset / widget.decoration.itemHeight).round(),
|
||||||
|
);
|
||||||
|
|
||||||
selectedIndex = newIndex;
|
selectedIndex = newIndex;
|
||||||
}
|
}
|
||||||
|
@ -73,54 +74,52 @@ class _ScrollPickerState extends State<ScrollPicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Stack(
|
||||||
return Stack(
|
children: [
|
||||||
children: [
|
Positioned.fill(
|
||||||
Positioned.fill(
|
child: Center(
|
||||||
child: Center(
|
child: widget.decoration.highlightWidget ??
|
||||||
child: widget.decoration.highlightWidget ??
|
Container(
|
||||||
Container(
|
height: widget.decoration.itemHeight,
|
||||||
height: widget.decoration.itemHeight,
|
decoration: ShapeDecoration(
|
||||||
decoration: ShapeDecoration(
|
color: Colors.grey.shade300,
|
||||||
color: Colors.grey.shade300,
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.circular(5),
|
||||||
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,8 @@ enum Month {
|
||||||
}
|
}
|
||||||
|
|
||||||
class TypeUtils {
|
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<DateTime> createWeekDays(
|
List<DateTime> createWeekDays(
|
||||||
WeekDay start,
|
WeekDay start,
|
||||||
WeekDay end,
|
WeekDay end,
|
||||||
|
@ -37,8 +38,8 @@ class TypeUtils {
|
||||||
throw ArgumentError('Start month must be before or equal to end month.');
|
throw ArgumentError('Start month must be before or equal to end month.');
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DateTime> result = [];
|
var result = <DateTime>[];
|
||||||
for (int i = start.index; i <= end.index; i++) {
|
for (var i = start.index; i <= end.index; i++) {
|
||||||
result.add(DateTime(2024, 1, WeekDay.values[i].index + 1));
|
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.');
|
throw ArgumentError('Start month must be before or equal to end month.');
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DateTime> result = [];
|
var result = <DateTime>[];
|
||||||
for (int i = start.index; i <= end.index; i++) {
|
for (var i = start.index; i <= end.index; i++) {
|
||||||
result.add(
|
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;
|
return result;
|
||||||
|
@ -66,8 +68,8 @@ class TypeUtils {
|
||||||
throw ArgumentError('Start year must be before or equal to year month.');
|
throw ArgumentError('Start year must be before or equal to year month.');
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DateTime> result = [];
|
var result = <DateTime>[];
|
||||||
for (int i = 0; i <= end - start; i++) {
|
for (var i = 0; i <= end - start; i++) {
|
||||||
result.add(DateTime(start + i, 1, 1));
|
result.add(DateTime(start + i, 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,8 @@ class FlutterFormInputScrollPicker<T> extends StatelessWidget {
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.initialIndex = 0,
|
this.initialIndex = 0,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
/// Values that will be shown in the scroll picker.
|
/// Values that will be shown in the scroll picker.
|
||||||
final List<T> values;
|
final List<T> values;
|
||||||
|
@ -36,42 +36,36 @@ class FlutterFormInputScrollPicker<T> extends StatelessWidget {
|
||||||
final ScrollPickerDecoration decoration;
|
final ScrollPickerDecoration decoration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => ScrollPickerFormField<T>(
|
||||||
return ScrollPickerFormField<T>(
|
values: values,
|
||||||
values: values,
|
initialIndex: initialIndex,
|
||||||
initialIndex: initialIndex,
|
decoration: decoration,
|
||||||
decoration: decoration,
|
childToString: childToString,
|
||||||
childToString: childToString,
|
onSaved: (value) => onSaved?.call(value),
|
||||||
onSaved: (value) => onSaved?.call(value),
|
onChanged: (value) => onChanged?.call(value),
|
||||||
onChanged: (value) => onChanged?.call(value),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScrollPickerFormField<T> extends FormField<T> {
|
class ScrollPickerFormField<T> extends FormField<T> {
|
||||||
ScrollPickerFormField({
|
ScrollPickerFormField({
|
||||||
required List<T> values,
|
required List<T> values,
|
||||||
int? initialIndex,
|
|
||||||
required ScrollPickerDecoration decoration,
|
required ScrollPickerDecoration decoration,
|
||||||
required FormFieldSetter<T> onSaved,
|
required FormFieldSetter<T> super.onSaved,
|
||||||
void Function(T value)? onChanged,
|
|
||||||
required String Function(T) childToString,
|
required String Function(T) childToString,
|
||||||
Key? key,
|
int? initialIndex,
|
||||||
|
void Function(T value)? onChanged,
|
||||||
|
super.key,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
|
||||||
onSaved: onSaved,
|
|
||||||
initialValue: values[initialIndex ?? (values.length / 2).floor()],
|
initialValue: values[initialIndex ?? (values.length / 2).floor()],
|
||||||
builder: (FormFieldState<T> state) {
|
builder: (FormFieldState<T> state) => ScrollPicker(
|
||||||
return ScrollPicker(
|
list: values.map((e) => childToString(e)).toList(),
|
||||||
list: values.map((e) => childToString(e)).toList(),
|
decoration: decoration,
|
||||||
decoration: decoration,
|
initialIndex: initialIndex,
|
||||||
initialIndex: initialIndex,
|
onChanged: (int index) {
|
||||||
onChanged: (int index) {
|
onChanged?.call(values[index]);
|
||||||
onChanged?.call(values[index]);
|
|
||||||
|
|
||||||
state.didChange(values[index]);
|
state.didChange(values[index]);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:flutter_input_library/src/inputs/slider/slider_field.dart';
|
||||||
|
|
||||||
class FlutterFormInputSlider extends StatelessWidget {
|
class FlutterFormInputSlider extends StatelessWidget {
|
||||||
const FlutterFormInputSlider({
|
const FlutterFormInputSlider({
|
||||||
Key? key,
|
super.key,
|
||||||
this.minValue = 0,
|
this.minValue = 0,
|
||||||
this.maxValue = 100,
|
this.maxValue = 100,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
|
@ -15,10 +15,7 @@ class FlutterFormInputSlider extends StatelessWidget {
|
||||||
this.initialValue,
|
this.initialValue,
|
||||||
this.validator,
|
this.validator,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
}) : assert(minValue < maxValue),
|
}) : assert(minValue < maxValue, 'minValue must be less than maxValue');
|
||||||
super(
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
|
|
||||||
final int minValue;
|
final int minValue;
|
||||||
final int maxValue;
|
final int maxValue;
|
||||||
|
@ -29,13 +26,11 @@ class FlutterFormInputSlider extends StatelessWidget {
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => SliderFormField(
|
||||||
return SliderFormField(
|
onSaved: (value) => onSaved?.call(value),
|
||||||
onSaved: (value) => onSaved?.call(value),
|
validator: (value) => validator?.call(value),
|
||||||
validator: (value) => validator?.call(value),
|
onChanged: (value) => onChanged?.call(value),
|
||||||
onChanged: (value) => onChanged?.call(value),
|
initialValue: initialValue ?? 0.5,
|
||||||
initialValue: initialValue ?? 0.5,
|
focusNode: focusNode,
|
||||||
focusNode: focusNode,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,26 +7,21 @@ import 'package:flutter/material.dart';
|
||||||
/// Creates a slider with the given input parameters
|
/// Creates a slider with the given input parameters
|
||||||
class SliderFormField extends FormField<double> {
|
class SliderFormField extends FormField<double> {
|
||||||
SliderFormField({
|
SliderFormField({
|
||||||
Key? key,
|
required FormFieldSetter<double> super.onSaved,
|
||||||
required FormFieldSetter<double> onSaved,
|
required FormFieldValidator<double> super.validator,
|
||||||
required FormFieldValidator<double> validator,
|
super.key,
|
||||||
void Function(double value)? onChanged,
|
void Function(double value)? onChanged,
|
||||||
FocusNode? focusNode,
|
FocusNode? focusNode,
|
||||||
double initialValue = 0.5,
|
double super.initialValue = 0.5,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
builder: (FormFieldState<double> state) => Slider(
|
||||||
onSaved: onSaved,
|
value: state.value ?? initialValue,
|
||||||
validator: validator,
|
focusNode: focusNode,
|
||||||
initialValue: initialValue,
|
onChanged: (double value) {
|
||||||
builder: (FormFieldState<double> state) {
|
onChanged?.call(value);
|
||||||
return Slider(
|
|
||||||
value: state.value ?? initialValue,
|
|
||||||
focusNode: focusNode,
|
|
||||||
onChanged: (double value) {
|
|
||||||
onChanged?.call(value);
|
|
||||||
|
|
||||||
state.didChange(value);
|
state.didChange(value);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,22 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// ignore_for_file: avoid_positional_boolean_parameters
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_input_library/src/inputs/switch/switch_field.dart';
|
import 'package:flutter_input_library/src/inputs/switch/switch_field.dart';
|
||||||
|
|
||||||
class FlutterFormInputSwitch extends StatelessWidget {
|
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 Widget? label;
|
||||||
final Function(bool?)? onSaved;
|
final Function(bool?)? onSaved;
|
||||||
final String? Function(bool?)? validator;
|
final String? Function(bool?)? validator;
|
||||||
|
@ -14,26 +25,12 @@ class FlutterFormInputSwitch extends StatelessWidget {
|
||||||
final bool? initialValue;
|
final bool? initialValue;
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
const FlutterFormInputSwitch({
|
|
||||||
Key? key,
|
|
||||||
this.label,
|
|
||||||
this.onSaved,
|
|
||||||
this.validator,
|
|
||||||
this.onChanged,
|
|
||||||
this.focusNode,
|
|
||||||
this.initialValue = false,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => SwitchFormField(
|
||||||
return SwitchFormField(
|
onSaved: (value) => onSaved?.call(value),
|
||||||
onSaved: (value) => onSaved?.call(value),
|
onChanged: (value) => onChanged?.call(value),
|
||||||
onChanged: (value) => onChanged?.call(value),
|
validator: (value) => validator?.call(value),
|
||||||
validator: (value) => validator?.call(value),
|
initialValue: initialValue ?? false,
|
||||||
initialValue: initialValue ?? false,
|
focusNode: focusNode,
|
||||||
focusNode: focusNode,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,32 +6,27 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SwitchFormField extends FormField<bool> {
|
class SwitchFormField extends FormField<bool> {
|
||||||
SwitchFormField({
|
SwitchFormField({
|
||||||
Key? key,
|
required FormFieldSetter<bool> super.onSaved,
|
||||||
required FormFieldSetter<bool> onSaved,
|
required FormFieldValidator<bool> super.validator,
|
||||||
required FormFieldValidator<bool> validator,
|
super.key,
|
||||||
FocusNode? focusNode,
|
FocusNode? focusNode,
|
||||||
bool initialValue = false,
|
bool super.initialValue = false,
|
||||||
bool autovalidate = false,
|
// ignore: avoid_positional_boolean_parameters
|
||||||
void Function(bool? value)? onChanged,
|
void Function(bool? value)? onChanged,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
builder: (FormFieldState<bool> state) => SwitchWidget(
|
||||||
onSaved: onSaved,
|
|
||||||
validator: validator,
|
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
builder: (FormFieldState<bool> state) {
|
state: state,
|
||||||
return SwitchWidget(
|
focusNode: focusNode,
|
||||||
initialValue: initialValue,
|
onChanged: onChanged,
|
||||||
state: state,
|
),
|
||||||
focusNode: focusNode,
|
);
|
||||||
onChanged: onChanged,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SwitchWidget extends StatefulWidget {
|
class SwitchWidget extends StatefulWidget {
|
||||||
const SwitchWidget({
|
const SwitchWidget({
|
||||||
this.initialValue = false,
|
|
||||||
required this.state,
|
required this.state,
|
||||||
|
this.initialValue = false,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -40,6 +35,7 @@ class SwitchWidget extends StatefulWidget {
|
||||||
final bool initialValue;
|
final bool initialValue;
|
||||||
final FormFieldState<bool> state;
|
final FormFieldState<bool> state;
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
final void Function(bool? value)? onChanged;
|
final void Function(bool? value)? onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -50,19 +46,17 @@ class _SwitchWidgetState extends State<SwitchWidget> {
|
||||||
late bool value = widget.initialValue;
|
late bool value = widget.initialValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Switch(
|
||||||
return Switch(
|
value: value,
|
||||||
value: value,
|
focusNode: widget.focusNode,
|
||||||
focusNode: widget.focusNode,
|
onChanged: (bool value) {
|
||||||
onChanged: (bool value) {
|
widget.onChanged?.call(value);
|
||||||
widget.onChanged?.call(value);
|
|
||||||
|
|
||||||
widget.state.didChange(value);
|
widget.state.didChange(value);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,23 +5,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// Generates a [TextFormField] for passwords. It requires a [FlutterFormInputController]
|
/// Generates a [TextFormField] for passwords. It requires a
|
||||||
/// as the [controller] parameter and an optional [Widget] as [label]
|
/// [FlutterFormInputController] as the [controller] parameter and an
|
||||||
|
/// optional [Widget] as [label]
|
||||||
class FlutterFormInputPassword extends StatefulWidget {
|
class FlutterFormInputPassword extends StatefulWidget {
|
||||||
final Widget? label;
|
|
||||||
final FocusNode? focusNode;
|
|
||||||
final TextStyle? style;
|
|
||||||
final String? initialValue;
|
|
||||||
final List<TextInputFormatter>? 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({
|
const FlutterFormInputPassword({
|
||||||
Key? key,
|
super.key,
|
||||||
this.label,
|
this.label,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.style,
|
this.style,
|
||||||
|
@ -33,7 +22,18 @@ class FlutterFormInputPassword extends StatefulWidget {
|
||||||
this.onFieldSubmitted,
|
this.onFieldSubmitted,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.decoration,
|
this.decoration,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
final Widget? label;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final TextStyle? style;
|
||||||
|
final String? initialValue;
|
||||||
|
final List<TextInputFormatter>? 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
|
@override
|
||||||
State<FlutterFormInputPassword> createState() => _PasswordTextFieldState();
|
State<FlutterFormInputPassword> createState() => _PasswordTextFieldState();
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class FlutterFormInputPlainText extends StatelessWidget {
|
class FlutterFormInputPlainText extends StatelessWidget {
|
||||||
const FlutterFormInputPlainText({
|
const FlutterFormInputPlainText({
|
||||||
Key? key,
|
super.key,
|
||||||
this.label,
|
this.label,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.decoration,
|
this.decoration,
|
||||||
|
@ -27,9 +27,7 @@ class FlutterFormInputPlainText extends StatelessWidget {
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.textCapitalization = TextCapitalization.none,
|
this.textCapitalization = TextCapitalization.none,
|
||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
}) : super(
|
});
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
|
|
||||||
final InputDecoration? decoration;
|
final InputDecoration? decoration;
|
||||||
final TextAlignVertical? textAlignVertical;
|
final TextAlignVertical? textAlignVertical;
|
||||||
|
@ -53,9 +51,9 @@ class FlutterFormInputPlainText extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
InputDecoration inputDecoration = decoration ??
|
var inputDecoration = decoration ??
|
||||||
InputDecoration(
|
InputDecoration(
|
||||||
label: label ?? const Text("Plain text"),
|
label: label ?? const Text('Plain text'),
|
||||||
);
|
);
|
||||||
|
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
|
@ -83,7 +81,7 @@ class FlutterFormInputPlainText extends StatelessWidget {
|
||||||
|
|
||||||
class FlutterFormInputMultiLine extends StatelessWidget {
|
class FlutterFormInputMultiLine extends StatelessWidget {
|
||||||
const FlutterFormInputMultiLine({
|
const FlutterFormInputMultiLine({
|
||||||
Key? key,
|
super.key,
|
||||||
this.label,
|
this.label,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.hint,
|
this.hint,
|
||||||
|
@ -98,7 +96,7 @@ class FlutterFormInputMultiLine extends StatelessWidget {
|
||||||
this.validator,
|
this.validator,
|
||||||
this.onFieldSubmitted,
|
this.onFieldSubmitted,
|
||||||
this.textCapitalization = TextCapitalization.sentences,
|
this.textCapitalization = TextCapitalization.sentences,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final Widget? label;
|
final Widget? label;
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
@ -117,42 +115,40 @@ class FlutterFormInputMultiLine extends StatelessWidget {
|
||||||
final TextCapitalization textCapitalization;
|
final TextCapitalization textCapitalization;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Column(
|
||||||
return Column(
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
Expanded(
|
child: FlutterFormInputPlainText(
|
||||||
child: FlutterFormInputPlainText(
|
label: label,
|
||||||
label: label,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
expands: true,
|
||||||
expands: true,
|
maxLines: null,
|
||||||
maxLines: null,
|
focusNode: focusNode,
|
||||||
focusNode: focusNode,
|
maxLength: maxCharacters,
|
||||||
maxLength: maxCharacters,
|
initialValue: initialValue,
|
||||||
initialValue: initialValue,
|
scrollPadding: scrollPadding,
|
||||||
scrollPadding: scrollPadding,
|
keyboardType: keyboardType,
|
||||||
keyboardType: keyboardType,
|
onSaved: onSaved,
|
||||||
onSaved: onSaved,
|
validator: validator,
|
||||||
validator: validator,
|
onChanged: onChanged,
|
||||||
onChanged: onChanged,
|
onFieldSubmitted: onFieldSubmitted,
|
||||||
onFieldSubmitted: onFieldSubmitted,
|
decoration: decoration ??
|
||||||
decoration: decoration ??
|
InputDecoration(
|
||||||
InputDecoration(
|
hintText: hint,
|
||||||
hintText: hint,
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
isDense: true,
|
||||||
isDense: true,
|
border: const OutlineInputBorder(
|
||||||
border: const OutlineInputBorder(
|
borderSide: BorderSide(color: Color(0xFF979797)),
|
||||||
borderSide: BorderSide(color: Color(0xFF979797)),
|
),
|
||||||
|
focusedBorder: const OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Color(0xFF979797)),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
enabled: enabled,
|
||||||
borderSide: BorderSide(color: Color(0xFF979797)),
|
textCapitalization: textCapitalization,
|
||||||
),
|
),
|
||||||
filled: true,
|
|
||||||
),
|
|
||||||
enabled: enabled,
|
|
||||||
textCapitalization: textCapitalization,
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
class EmailValidator {
|
mixin EmailValidator {
|
||||||
static bool isValid(String? email) {
|
static bool isValid(String? email) {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: flutter_input_library
|
name: flutter_input_library
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 2.7.0
|
version: 2.7.1
|
||||||
repository: https://github.com/Iconica-Development/flutter_input_library
|
repository: https://github.com/Iconica-Development/flutter_input_library
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@ -15,6 +15,9 @@ dependencies:
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
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:
|
flutter:
|
||||||
|
|
Loading…
Reference in a new issue