feat: port of form next-shell to appshell

This commit is contained in:
TimIconica 2022-09-20 11:04:00 +02:00
parent 9c9bb2d936
commit 16262dc75d
45 changed files with 3759 additions and 122 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:flutter_form/next_shell/form.dart';
import 'package:form_example/template_page.dart';
class AgePage {
ShellFormPage returnPage(
Size size,
double fontSize,
int pageNumber,
int amountOfPages,
) {
return ShellFormPage(
child: TemplatePage(
size: size,
fontSize: fontSize,
title: "What is your age?",
pageNumber: pageNumber,
amountOfPages: amountOfPages,
shellFormWidgets: [
ShellFormInputNumberPicker(
controller: ShellFormInputNumberPickerController(
id: "age",
checkPageTitle: (dynamic amount) {
return "Age: $amount years";
},
),
),
],
),
);
}
}

View file

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:flutter_form/next_shell/form.dart';
import 'package:form_example/template_page.dart';
class CarouselPage {
final List<Map<String, dynamic>> cars = [
{
"title": "Mercedes",
"description": "Mercedes is a car",
},
{
"title": "BMW",
"description": "BMW is a car",
},
{
"title": "Mazda",
'description': "Mazda is a car",
},
];
List<Widget> getCars() {
return cars.map((car) {
return Builder(
builder: (BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
width: MediaQuery.of(context).size.width,
decoration: const BoxDecoration(
color: Color(0xFFD8D8D8),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
child: Image.asset('assets/images/${car['title']}.png'),
),
),
const SizedBox(
height: 14,
),
Text(
car["title"],
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(
height: 5,
),
Text(
car["description"],
style: const TextStyle(fontSize: 16),
),
],
);
},
);
}).toList();
}
ShellFormPage returnPage(
Size size,
double fontSize,
int pageNumber,
int amountOfPages,
) {
return ShellFormPage(
child: TemplatePage(
size: size,
fontSize: fontSize,
title: "What's your favorite car?",
pageNumber: pageNumber,
amountOfPages: amountOfPages,
shellFormWidgets: [
ShellFormInputCarousel(
controller: ShellFormInputCarouselController(
id: 'carCarousel',
checkPageTitle: (dynamic index) {
return cars[index]["title"];
},
checkPageDescription: (dynamic index) {
return cars[index]["description"];
},
),
items: getCars())
],
),
);
}
}

View file

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/next_shell/form.dart';
class CheckPageExample {
CheckPage showCheckpage(
BuildContext context,
Size size,
double fontSize,
String checkPageText,
) {
return CheckPage(
title: Container(
margin: const EdgeInsets.only(
top: 60,
bottom: 10,
),
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Text(
checkPageText,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w900,
),
),
),
inputCheckWidget:
(String title, String? description, Function onPressed) {
return GestureDetector(
onTap: () async {
await onPressed();
},
child: Container(
width: size.width * 0.9,
padding: const EdgeInsets.only(
top: 18,
bottom: 16,
right: 18,
left: 27,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: const Color(0xFF000000).withOpacity(0.20),
blurRadius: 5,
),
],
),
child: Column(
children: [
Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFFD8D8D8),
borderRadius: BorderRadius.circular(5),
),
),
const SizedBox(
width: 16,
),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const Spacer(),
const Icon(Icons.arrow_forward),
],
),
if (description != null)
const SizedBox(
height: 9,
),
if (description != null)
Text(
description,
style: TextStyle(fontSize: fontSize / 1.3),
)
],
),
),
);
},
);
}
}

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:flutter_form/next_shell/form.dart';
import 'package:form_example/template_page.dart';
class NamePage {
ShellFormPage returnPage(
Size size,
double fontSize,
int pageNumber,
int amountOfPages,
) {
return ShellFormPage(
child: TemplatePage(
size: size,
fontSize: fontSize,
pageNumber: pageNumber,
amountOfPages: amountOfPages,
title: "Please enter your name",
shellFormWidgets: [
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 40),
child: ShellFormInputPlainText(
label: const Text("First Name"),
controller: ShellFormInputPlainTextController(
mandatory: true,
id: "firstName",
checkPageTitle: (dynamic firstName) {
return "First Name: $firstName";
},
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: ShellFormInputPlainText(
label: const Text("Last Name"),
controller: ShellFormInputPlainTextController(
mandatory: true,
id: "lastName",
checkPageTitle: (dynamic lastName) {
return "Last Name: $lastName";
},
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class ThanksPage extends StatelessWidget {
const ThanksPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Thanks for filling in the form!",
style: TextStyle(fontSize: 20),
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed('/'),
child: const Text("Next"))
],
),
),
);
}
}

View file

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:flutter_form/next_shell/form.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_example/example_pages/age_page.dart';
import 'package:form_example/example_pages/carousel_page.dart';
import 'package:form_example/example_pages/check_page.dart';
import 'package:form_example/example_pages/name_page.dart';
class FormExample extends ConsumerStatefulWidget {
const FormExample({Key? key}) : super(key: key);
@override
ConsumerState<ConsumerStatefulWidget> createState() => _FormExampleState();
}
class _FormExampleState extends ConsumerState<FormExample> {
final ShellFormController formController = ShellFormController();
final String checkPageText = "All entered info: ";
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var fontSize = size.height / 40;
return Scaffold(
body: Center(
child: ShellForm(
formController: formController,
options: ShellFormOptions(
onFinished: (Map<int, Map<String, dynamic>> results) {
print("Totale resultaten: $results");
Navigator.of(context).pushNamed('/thanks');
},
onNext: (int pageNumber, Map<String, dynamic> results) {
print("Resultaten pagina $pageNumber: $results");
},
nextButton: (int pageNumber, bool checkingPages) {
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.only(
bottom: size.height / 20,
),
child: SizedBox(
height: size.height / 15,
width: size.width / 1.5,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
backgroundColor: Colors.black,
textStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
onPressed: () {
formController.autoNextStep();
},
child: Text(checkingPages ? "Save" : "Next Page"),
),
),
),
);
},
backButton: (int pageNumber, bool checkingPages, int pageAmount) {
if (pageNumber != 0) {
if (!checkingPages || pageNumber >= pageAmount) {
return Align(
alignment: Alignment.topLeft,
child: Container(
margin: EdgeInsets.only(
top: size.height / 20,
left: size.width / 15,
),
width: 30,
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
color: const Color(0xFFD8D8D8).withOpacity(0.50),
),
child: IconButton(
padding: EdgeInsets.zero,
splashRadius: 29,
onPressed: () {
formController.previousStep();
},
icon: const Icon(Icons.chevron_left),
)),
);
}
}
return Container();
},
pages: [
AgePage().returnPage(size, fontSize, 1, 3),
NamePage().returnPage(size, fontSize, 2, 3),
CarouselPage().returnPage(size, fontSize, 3, 3),
],
checkPage: CheckPageExample()
.showCheckpage(context, size, fontSize, checkPageText),
),
),
),
);
}
}

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_example/route.dart';
void main() { void main() {
runApp(const FormsExample()); runApp(const ProviderScope(child: FormsExample()));
} }
class FormsExample extends StatelessWidget { class FormsExample extends StatelessWidget {
@ -15,7 +17,22 @@ class FormsExample extends StatelessWidget {
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
), ),
home: const FormsHomePage(title: 'Flutter Demo Home Page'), home: const FormsHomePage(title: 'Flutter Forms'),
initialRoute: '/',
onGenerateRoute: (settings) {
var routes = getRoutes();
if (routes.containsKey(settings.name)) {
return PageRouteBuilder(
pageBuilder: (_, __, ___) => routes[settings.name]!(context),
settings: settings,
);
} else {
return PageRouteBuilder(
settings: settings,
pageBuilder: (_, __, ___) => const Text('Page not found'),
);
}
},
); );
} }
} }
@ -38,12 +55,10 @@ class _FormsHomePageState extends State<FormsHomePage> {
), ),
body: Center( body: Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: (() => createForm()), onPressed: (() => Navigator.of(context).pushNamed('/form')),
child: const Text('Create form'), child: const Text('Create form'),
), ),
), ),
); );
} }
void createForm() {}
} }

12
example/lib/route.dart Normal file
View file

@ -0,0 +1,12 @@
import 'package:flutter/widgets.dart';
import 'package:form_example/example_pages/thanks_page.dart';
import 'package:form_example/form_example.dart';
import 'package:form_example/main.dart';
Map<String, WidgetBuilder> getRoutes() {
return {
'/': (context) => const FormsExample(),
'/form': (context) => const FormExample(),
'/thanks': (context) => const ThanksPage(),
};
}

View file

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
class TemplatePage extends StatelessWidget {
const TemplatePage({
super.key,
required this.size,
required this.fontSize,
required this.title,
required this.pageNumber,
required this.amountOfPages,
required this.shellFormWidgets,
});
final Size size;
final double fontSize;
final String title;
final int pageNumber;
final int amountOfPages;
final List<Widget> shellFormWidgets;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: size.width / 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size.height / 10,
),
Text(
"$pageNumber / $amountOfPages",
style: TextStyle(
fontSize: fontSize,
),
),
SizedBox(
height: size.height / 80,
),
Text(
title,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w900,
),
),
],
),
),
),
const Spacer(),
for (var widget in shellFormWidgets) ...[
widget,
],
const Spacer(
flex: 2,
),
],
),
);
}
}

View file

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -43,6 +50,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
equatable:
dependency: transitive
description:
name: equatable
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -55,6 +69,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_form:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -62,11 +83,30 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -74,6 +114,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
localization:
dependency: transitive
description:
name: localization
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -102,11 +149,34 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.8.2"
riverpod:
dependency: transitive
description:
name: riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
shell_model:
dependency: transitive
description:
path: "packages/shell_model"
ref: dbf7155ab18e79b5ff1da73f31c7fe3b06c4c82a
resolved-ref: dbf7155ab18e79b5ff1da73f31c7fe3b06c4c82a
url: "git@bitbucket.org:iconicadevs/next_shell.git"
source: git
version: "0.0.3"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
sliding_up_panel:
dependency: transitive
description:
name: sliding_up_panel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0+1"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -121,6 +191,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
state_notifier:
dependency: transitive
description:
name: state_notifier
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.2+1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -149,6 +226,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.12" version: "0.4.12"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -158,3 +249,4 @@ packages:
version: "2.1.2" version: "2.1.2"
sdks: sdks:
dart: ">=2.18.0 <3.0.0" dart: ">=2.18.0 <3.0.0"
flutter: ">=3.0.0"

View file

@ -1,91 +1,30 @@
name: example name: form_example
description: A new Flutter project. description: Form example made with Flutter Form Package.
# The following line prevents the package from being accidentally published to publish_to: 'none'
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=2.18.0 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
flutter_riverpod: ^1.0.4
flutter_form:
path: ../
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
assets:
# To add assets to your application, add an assets section, like this: - assets/images/
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View file

@ -1,30 +1,30 @@
// This is a basic Flutter widget test. // // This is a basic Flutter widget test.
// // //
// To perform an interaction with a widget in your test, use the WidgetTester // // To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll // // utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget // // gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct. // // tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; // import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart'; // import 'package:example/main.dart';
void main() { // void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { // testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // // Build our app and trigger a frame.
await tester.pumpWidget(const FormsExample()); // await tester.pumpWidget(const FormsExample());
// Verify that our counter starts at 0. // // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget); // expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing); // expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame. // // Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add)); // await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // await tester.pump();
// Verify that our counter has incremented. // // Verify that our counter has incremented.
expect(find.text('0'), findsNothing); // expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget); // expect(find.text('1'), findsOneWidget);
}); // });
} // }

View file

@ -1,7 +1,4 @@
library flutter_form; export 'shell_form.dart';
export 'src/widgets/input/abstractions.dart';
/// A Calculator. export 'src/widgets/input/input_types/input_types.dart';
class Calculator { export 'src/widgets/page_indicator/page_indicators.dart';
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}

72
lib/next_shell/form.dart Normal file
View file

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
/// The options used to set parameters to a [ShellForm].
///
/// The pages determine what pages the pageview will contain via a [List] of [ShellFormPage]s.
///
/// Using a checkpage gives the ability for the user to check all input values before commiting by [CheckPage].
/// If [checkpage] is null no check page will be shown.
///
/// [nextButton] and [backButton] are both a way to give controls to user.
/// Both are just plain widgets used in a [Stack]. So the widgets can be aligned where ever.
/// The formcontroller of [ShellForm] should be used to give control to the widgets/buttons.
///
/// [onFinished] and [onNext] are both callbacks which give the users results.
/// onNext is called when the user goes to the next page.
/// onFinished is called when the form is finished. When checkpage is set onFinished is called when the checkpage is finished.
class ShellFormOptions {
final List<ShellFormPage> pages;
final CheckPage? checkPage;
final Widget Function(int pageNumber, bool checkingPages)? nextButton;
final Widget Function(int pageNumber, bool checkingPages, int pageAmount)?
backButton;
final void Function(Map<int, Map<String, dynamic>>) onFinished;
final void Function(int pageNumber, Map<String, dynamic>) onNext;
const ShellFormOptions({
required this.pages,
this.checkPage,
this.nextButton,
this.backButton,
required this.onFinished,
required this.onNext,
});
}
/// The defines every page in a [ShellForm].
class ShellFormPage {
final Widget child;
ShellFormPage({
required this.child,
});
}
/// CheckPage is used to set a check page at the end of a [ShellForm].
/// A CheckPage is a page where the user can check all input values before commiting.
///
/// [title] is the widget shown at the top of the page.
///
/// [mainAxisAlignment] is the alignment of the check widgets.
///
/// [inputCheckWidget] determines how every input is represented on the page.
/// [title] is the value given in the input.
/// This input can be modified by setting the [checkPageTitle] of that input controller.
///
/// Same for the [description] but if the description is not set in the input controller no description will be given.
///
/// [onPressed] can be set so that when the user triggers it the user will be sent back to the page including the input.
/// Here the user can modify the input and save it. Afterwards the user will be sent back to the check page.
class CheckPage {
final Widget? title;
final MainAxisAlignment mainAxisAlignment;
final Widget Function(String title, String? description, Function onPressed)?
inputCheckWidget;
const CheckPage({
this.title,
this.inputCheckWidget,
this.mainAxisAlignment = MainAxisAlignment.start,
});
}

View file

@ -0,0 +1,5 @@
import 'package:flutter_form/next_shell/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final translationServiceProvider =
Provider<TranslationService>((ref) => ShellTranslationService());

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/next_shell/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
abstract class TranslationService {
TranslationService._();
String translate(
BuildContext context,
String key, {
List<String>? params,
});
String number(double value);
}
typedef Translator = String Function(
String, {
List<String>? params,
});
class ShellTranslationService implements TranslationService {
@override
String number(double value) {
return value.toStringAsFixed(2);
}
@override
String translate(BuildContext context, String key, {List<String>? params}) {
return key;
}
}
Translator getTranslator(BuildContext context, WidgetRef ref) {
try {
var translator = ref.read(translationServiceProvider).translate;
return (
String key, {
List<String>? params,
}) {
return translator(context, key, params: params);
};
} catch (e) {
return (
String key, {
List<String>? params,
}) {
return key;
};
}
}

558
lib/shell_form.dart Normal file
View file

@ -0,0 +1,558 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'flutter_form.dart';
import 'next_shell/form.dart';
import 'next_shell/translation_service.dart';
import 'src/utils/form_page_controller.dart';
import 'src/utils/formstate.dart' as fs;
/// A wrapper for flutters [Form] that can be controlled by a controller and provides multiple pre-defined input types/fields
/// [ShellForm] also provides multi page forms and a check page for validation.
///
/// A [ShellFormController] has to be given to control what happens to values and pages within the ShellForm.
///
/// [ShellFormOptions] have to be provided to control the appearance of the form.
/// ``` dart
/// ShellFormInputEmailController emailController =
/// ShellFormInputEmailController(id: 'email');
/// ShellFormInputPasswordController passwordController =
/// ShellFormInputPasswordController(id: 'password');
///
/// ShellForm(
/// formController: shellFormController,
/// options: ShellFormOptions(
/// onFinished: (Map<int, Map<String, dynamic>> results) {
/// // print(results);
/// },
/// onNext: (int pageNumber, Map<String, dynamic> results) {
/// // print("Resultaten pagina $pageNumber: $results");
/// },
/// nextButton: (int pageNumber, bool checkingPages) {
/// return Align(
/// alignment: Alignment.bottomCenter,
/// child: Padding(
/// padding: const EdgeInsets.only(
/// bottom: 25,
/// ),
/// child: ElevatedButton(
/// onPressed: () {
/// shellFormController.autoNextStep();
/// },
/// child: Text(checkingPages ? "Opslaan" : "Volgende stap"),
/// ),
/// ),
/// );
/// },
/// backButton: (int pageNumber, bool checkingPages, int pageAmount) {
/// if (pageNumber != 0) {
/// if (!checkingPages || pageNumber >= pageAmount) {
/// return Align(
/// alignment: Alignment.topLeft,
/// child: IconButton(
/// padding: EdgeInsets.zero,
/// splashRadius: 29,
/// onPressed: () {
/// shellFormController.previousStep();
/// },
/// icon: const Icon(Icons.chevron_left),
/// ),
/// );
/// }
/// }
/// return Container();
/// },
/// pages: [
/// ShellFormPage(
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: [
/// Align(
/// alignment: Alignment.centerLeft,
/// child: Padding(
/// padding: const EdgeInsets.symmetric(horizontal: 46),
/// child: Column(
/// crossAxisAlignment: CrossAxisAlignment.start,
/// children: const [
/// SizedBox(
/// height: 60,
/// ),
/// Text(
/// 'Inloggen',
/// style: TextStyle(
/// fontSize: 25,
/// fontWeight: FontWeight.w900,
/// ),
/// ),
/// ],
/// ),
/// ),
/// ),
/// const Spacer(),
/// ShellFormInputEmail(controller: emailController),
/// const SizedBox(
/// height: 25,
/// ),
/// ShellFormInputPassword(controller: passwordController),
/// const Spacer(),
/// ],
/// ),
/// ),
/// ],
/// checkPage: CheckPage(
/// title: const Text(
/// "Hier zijn je wensen voor het afscheidsfeestje",
/// style: TextStyle(
/// fontSize: 25,
/// fontWeight: FontWeight.w900,
/// ),
/// ),
/// inputCheckWidget:
/// (String title, String? description, Function onPressed) {
/// return GestureDetector(
/// onTap: () async {
/// await onPressed();
/// },
/// child: Container(
/// width: MediaQuery.of(context).size.width * 0.9,
/// padding: const EdgeInsets.only(
/// top: 18,
/// bottom: 16,
/// right: 18,
/// left: 27,
/// ),
/// decoration: BoxDecoration(
/// color: Colors.white,
/// borderRadius: BorderRadius.circular(10),
//// boxShadow: [
/// BoxShadow(
/// color: const Color(0xFF000000).withOpacity(0.20),
/// blurRadius: 5,
/// ),
/// ],
/// ),
/// child: Column(
/// children: [
/// Row(
/// children: [
/// Container(
/// width: 30,
/// height: 30,
/// decoration: BoxDecoration(
/// color: const Color(0xFFD8D8D8),
/// borderRadius: BorderRadius.circular(5),
/// ),
/// ),
/// const SizedBox(
/// width: 16,
/// ),
/// Text(
/// title,
/// style: const TextStyle(
/// fontWeight: FontWeight.w900,
/// fontSize: 20,
/// ),
/// ),
/// const Spacer(),
/// const Icon(Icons.arrow_forward),
/// ],
/// ),
/// if (description != null)
/// const SizedBox(
/// height: 9,
/// ),
/// if (description != null)
/// Text(
/// description,
/// style: const TextStyle(fontSize: 16),
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// mainAxisAlignment: MainAxisAlignment.start,
/// ),
/// ),
/// ),
/// ```
class ShellForm extends ConsumerStatefulWidget {
const ShellForm({
Key? key,
required this.options,
required this.formController,
}) : super(key: key);
final ShellFormOptions options;
final ShellFormController formController;
@override
ConsumerState<ShellForm> createState() => _ShellFormState();
}
class _ShellFormState extends ConsumerState<ShellForm> {
late ShellFormController _formController;
@override
void initState() {
super.initState();
_formController = widget.formController;
_formController.setShellFormOptions(widget.options);
List<GlobalKey<FormState>> keys = [];
for (ShellFormPage _ in widget.options.pages) {
keys.add(GlobalKey<FormState>());
}
_formController.setKeys(keys);
_formController.addListener(() {
setState(() {});
});
List<ShellFormPageController> controllers = [];
for (int i = 0; i < widget.options.pages.length; i++) {
controllers.add(ShellFormPageController());
}
_formController.setFormPageControllers(controllers);
}
@override
Widget build(BuildContext context) {
var _ = getTranslator(context, ref);
return Stack(
children: [
PageView(
controller: _formController.getPageController(),
physics: const NeverScrollableScrollPhysics(),
children: [
for (int i = 0; i < widget.options.pages.length; i++) ...[
Form(
key: _formController.getKeys()[i],
child: fs.FormState(
formController: _formController.getFormPageControllers()[i],
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: widget.options.pages[i].child,
),
],
),
),
),
],
if (widget.options.checkPage != null)
Column(
children: [
if (widget.options.checkPage!.title != null)
widget.options.checkPage!.title!,
Expanded(
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment:
widget.options.checkPage!.mainAxisAlignment,
children: getResultWidgets(),
),
),
],
),
),
],
),
],
),
widget.options.nextButton != null
? widget.options.nextButton!(_formController.getCurrentStep(),
_formController.getCheckpages())
: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 40, vertical: 15),
textStyle: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
onPressed: () => _formController.autoNextStep(),
child: Text(_formController.getCurrentStep() >=
widget.options.pages.length - 1
? "Finish"
: "Next"),
),
if (widget.options.backButton != null)
widget.options.backButton!(
_formController.getCurrentStep(),
_formController.getCheckpages(),
widget.options.pages.length,
),
],
);
}
List<Widget> getResultWidgets() {
List<Widget> widgets = [];
_formController.getAllResults().forEach(
(pageNumber, pageResults) {
pageResults.forEach((inputId, inputResult) {
ShellFormInputController? inputController = _formController
.getFormPageControllers()[pageNumber]
.getController(inputId);
if (inputController != null) {
if (widget.options.checkPage!.inputCheckWidget != null) {
widgets.add(
widget.options.checkPage!.inputCheckWidget!(
inputController.checkPageTitle != null
? inputController.checkPageTitle!(inputController.value)
: inputController.value.toString(),
inputController.checkPageDescription != null
? inputController
.checkPageDescription!(inputController.value)
: null,
() async {
await _formController.jumpToPage(pageNumber);
},
),
);
} else {
widgets.add(
GestureDetector(
onTap: () async {
await _formController.jumpToPage(pageNumber);
},
child: Container(
width: 390,
padding: const EdgeInsets.only(
top: 18,
bottom: 16,
right: 18,
left: 27,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: const Color(0xFF000000).withOpacity(0.20),
blurRadius: 5,
),
],
),
child: Column(
children: [
Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFFD8D8D8),
borderRadius: BorderRadius.circular(5),
),
),
const SizedBox(
width: 16,
),
Text(
inputController.checkPageTitle != null
? inputController.checkPageTitle!(inputResult)
: inputResult.toString(),
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const Spacer(),
const Icon(Icons.arrow_forward),
],
),
if (inputController.checkPageDescription != null)
const SizedBox(
height: 9,
),
if (inputController.checkPageDescription != null)
Text(
inputController.checkPageDescription!(inputResult),
style: const TextStyle(fontSize: 16),
)
],
),
),
),
);
}
}
widgets.add(
const SizedBox(
height: 25,
),
);
});
},
);
return widgets;
}
}
class ShellFormController extends ChangeNotifier {
late ShellFormOptions _options;
int _currentStep = 0;
late List<GlobalKey<FormState>> _keys;
bool _checkingPages = false;
final PageController _pageController = PageController();
late List<ShellFormPageController> _formPageControllers;
List<ShellFormPageController> getFormPageControllers() {
return _formPageControllers;
}
setFormPageControllers(List<ShellFormPageController> controllers) {
_formPageControllers = controllers;
}
Future<void> autoNextStep() async {
if (_currentStep >= _options.pages.length && _options.checkPage != null) {
_options.onFinished(getAllResults());
} else {
if (validateAndSaveCurrentStep()) {
_options.onNext(
_currentStep, _formPageControllers[_currentStep].getAllValues());
if (_currentStep >= _options.pages.length - 1 &&
_options.checkPage == null ||
_currentStep >= _options.pages.length &&
_options.checkPage != null) {
_options.onFinished(getAllResults());
} else {
if (_checkingPages) {
_currentStep = _options.pages.length;
notifyListeners();
await _pageController.animateToPage(_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease);
} else {
_currentStep += 1;
if (_currentStep >= _options.pages.length &&
_options.checkPage != null) {
_checkingPages = true;
}
notifyListeners();
await _pageController.animateToPage(_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease);
}
}
}
}
}
Future<void> previousStep() async {
_currentStep -= 1;
_checkingPages = false;
notifyListeners();
await _pageController.animateToPage(
_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
Future<void> jumpToPage(int pageNumber) async {
_currentStep = pageNumber;
notifyListeners();
await _pageController.animateToPage(
_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
bool validateAndSaveCurrentStep() {
if (_keys[_currentStep].currentState!.validate()) {
_keys[_currentStep].currentState!.save();
return true;
}
return false;
}
Map<String, dynamic> getCurrentStepResults() {
return _formPageControllers[_currentStep].getAllValues();
}
Future<void> nextStep() async {
_currentStep += 1;
if (_currentStep >= _options.pages.length && _options.checkPage != null) {
_checkingPages = true;
}
notifyListeners();
await _pageController.animateToPage(_currentStep,
duration: const Duration(milliseconds: 250), curve: Curves.ease);
}
finishForm() {
_options.onFinished(getAllResults());
}
Map<int, Map<String, dynamic>> getAllResults() {
Map<int, Map<String, dynamic>> allValues = {};
for (var i = 0; i < _options.pages.length; i++) {
allValues.addAll({i: _formPageControllers[i].getAllValues()});
}
return allValues;
}
setShellFormOptions(ShellFormOptions options) {
_options = options;
}
setKeys(List<GlobalKey<FormState>> keys) {
_keys = keys;
}
List<GlobalKey<FormState>> getKeys() {
return _keys;
}
int getCurrentStep() {
return _currentStep;
}
bool getCheckpages() {
return _checkingPages;
}
PageController getPageController() {
return _pageController;
}
}

View file

@ -0,0 +1,32 @@
import 'package:flutter_form/flutter_form.dart';
class ShellFormPageController {
final List<ShellFormInputController> _controllers = [];
void register(ShellFormInputController inputController) {
_controllers.add(inputController);
}
bool _isRegisteredById(String id) {
return _controllers.any((element) => (element.id == id));
}
ShellFormInputController? getController(String key) {
if (_isRegisteredById(key)) {
return _controllers.firstWhere((element) => element.id == key);
}
return null;
}
Map<String, dynamic> getAllValues() {
Map<String, dynamic> values = {};
for (ShellFormInputController controller in _controllers) {
if (controller.value != null) {
values.addAll({controller.id!: controller.value});
}
}
return values;
}
}

View file

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'form_page_controller.dart';
class FormState extends InheritedWidget {
const FormState({
Key? key,
required Widget child,
required this.formController,
}) : super(key: key, child: child);
final ShellFormPageController formController;
static FormState of(BuildContext context) {
final FormState? result =
context.dependOnInheritedWidgetOfExactType<FormState>();
assert(result != null, 'No FormStat found in context');
return result!;
}
@override
bool updateShouldNotify(FormState oldWidget) =>
formController != oldWidget.formController;
}

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '/src/utils/formstate.dart' as fs;
/// Abstract class for the input widgets used in a [ShellForm].
///
/// The controller [ShellFormInputController] has to be given to the widget.
/// Whicht controller is used determines how to value will be handled.
///
/// label is a standard parameter to normally sets the label of the input.
abstract class ShellFormInputWidget extends ConsumerWidget {
const ShellFormInputWidget({
Key? key,
required this.controller,
this.label,
String? hintText,
}) : super(key: key);
final ShellFormInputController controller;
final Widget? label;
registerController(BuildContext context) {
ShellFormInputController? localController =
fs.FormState.of(context).formController.getController(controller.id!);
if (localController == null) {
fs.FormState.of(context).formController.register(controller);
}
}
}
/// Abstract class for the controller for inputs used in a [ShellForm].
///
/// The id determines the key in the [Map] returned by the [ShellForm].
///
/// value is a way to set a initial value.
///
/// mandatory determines if the input is mandatory.
///
/// checkPageTitle is a function where you can transform the value from the input into something representable.
/// This value will be given when defining the check page widgets.
/// If this function is not set, the value will be used as is.
/// Example:
/// ``` dart
/// checkPageTitle: (dynamic amount) {
/// return "$amount persons";
/// },
/// ```
///
/// checkPageDescription is the same as checkPageTitle but for the description.
/// If null no description will be shown.
abstract class ShellFormInputController<T> {
String? id;
T? value;
bool mandatory = false;
String Function(T value)? checkPageTitle;
String Function(T value)? checkPageDescription;
void onSaved(T value);
String? onValidate(
T value, String Function(String, {List<String>? params}) translator);
}

View file

@ -0,0 +1,144 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'carousel_utils.dart';
import 'carousel_options.dart';
import 'carousel_state.dart';
abstract class CarouselController {
bool get ready;
Future<void> get onReady;
Future<void> nextPage({Duration? duration, Curve? curve});
Future<void> previousPage({Duration? duration, Curve? curve});
void jumpToPage(int page);
Future<void> animateToPage(int page, {Duration? duration, Curve? curve});
void startAutoPlay();
void stopAutoPlay();
factory CarouselController() => CarouselControllerImpl();
}
class CarouselControllerImpl implements CarouselController {
final Completer<void> _readyCompleter = Completer<void>();
CarouselState? _state;
set state(CarouselState? state) {
_state = state;
if (!_readyCompleter.isCompleted) {
_readyCompleter.complete();
}
}
void _setModeController() =>
_state!.changeMode(CarouselPageChangedReason.controller);
@override
bool get ready => _state != null;
@override
Future<void> get onReady => _readyCompleter.future;
/// Animates the controlled [CarouselSlider] to the next page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> nextPage(
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
_setModeController();
await _state!.pageController!.nextPage(duration: duration!, curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Animates the controlled [CarouselSlider] to the previous page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> previousPage(
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
_setModeController();
await _state!.pageController!
.previousPage(duration: duration!, curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Changes which page is displayed in the controlled [CarouselSlider].
///
/// Jumps the page position from its current value to the given value,
/// without animation, and without checking if the new value is in range.
@override
void jumpToPage(int page) {
final index = getRealIndex(_state!.pageController!.page!.toInt(),
_state!.realPage - _state!.initialPage, _state!.itemCount);
_setModeController();
final int pageToJump = _state!.pageController!.page!.toInt() + page - index;
return _state!.pageController!.jumpToPage(pageToJump);
}
/// Animates the controlled [CarouselSlider] from the current page to the given page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> animateToPage(int page,
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
final index = getRealIndex(_state!.pageController!.page!.toInt(),
_state!.realPage - _state!.initialPage, _state!.itemCount);
_setModeController();
await _state!.pageController!.animateToPage(
_state!.pageController!.page!.toInt() + page - index,
duration: duration!,
curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Starts the controlled [CarouselSlider] autoplay.
///
/// The carousel will only autoPlay if the [autoPlay] parameter
/// in [CarouselOptions] is true.
@override
void startAutoPlay() {
_state!.onResumeTimer();
}
/// Stops the controlled [CarouselSlider] from autoplaying.
///
/// This is a more on-demand way of doing this. Use the [autoPlay]
/// parameter in [CarouselOptions] to specify the autoPlay behaviour of the carousel.
@override
void stopAutoPlay() {
_state!.onResetTimer();
}
}

View file

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'carousel_slider.dart';
class CarouselFormField extends FormField<int> {
CarouselFormField({
Key? key,
required FormFieldSetter<int> onSaved,
required FormFieldValidator<int> validator,
int initialValue = 0,
bool autovalidate = false,
required List<Widget> items,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<int> state) {
return CarouselSlider(
options: CarouselOptions(
initialPage: initialValue,
onPageChanged: (index, reason) {
state.didChange(index);
},
height: 425,
aspectRatio: 2.0,
enlargeCenterPage: true,
enableInfiniteScroll: false,
),
items: items.map((Widget item) {
return item;
}).toList(),
);
});
}

View file

@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
enum CarouselPageChangedReason { timed, manual, controller }
enum CenterPageEnlargeStrategy { scale, height }
class CarouselOptions {
/// Set carousel height and overrides any existing [aspectRatio].
final double? height;
/// Aspect ratio is used if no height have been declared.
///
/// Defaults to 16:9 aspect ratio.
final double aspectRatio;
/// The fraction of the viewport that each page should occupy.
///
/// Defaults to 0.8, which means each page fills 80% of the carousel.
final double viewportFraction;
/// The initial page to show when first creating the [CarouselSlider].
///
/// Defaults to 0.
final int initialPage;
///Determines if carousel should loop infinitely or be limited to item length.
///
///Defaults to true, i.e. infinite loop.
final bool enableInfiniteScroll;
/// Reverse the order of items if set to true.
///
/// Defaults to false.
final bool reverse;
/// Enables auto play, sliding one page at a time.
///
/// Use [autoPlayInterval] to determent the frequency of slides.
/// Defaults to false.
final bool autoPlay;
/// Sets Duration to determent the frequency of slides when
///
/// [autoPlay] is set to true.
/// Defaults to 4 seconds.
final Duration autoPlayInterval;
/// The animation duration between two transitioning pages while in auto playback.
///
/// Defaults to 800 ms.
final Duration autoPlayAnimationDuration;
/// Determines the animation curve physics.
///
/// Defaults to [Curves.fastOutSlowIn].
final Curve autoPlayCurve;
/// Determines if current page should be larger than the side images,
/// creating a feeling of depth in the carousel.
///
/// Defaults to false.
final bool? enlargeCenterPage;
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Called whenever the page in the center of the viewport changes.
final Function(int index, CarouselPageChangedReason reason)? onPageChanged;
/// Called whenever the carousel is scrolled
final ValueChanged<double?>? onScrolled;
/// How the carousel should respond to user input.
///
/// For example, determines how the items continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// Defaults to matching platform conventions.
final ScrollPhysics? scrollPhysics;
/// Set to false to disable page snapping, useful for custom scroll behavior.
///
/// Default to `true`.
final bool pageSnapping;
/// If `true`, the auto play function will be paused when user is interacting with
/// the carousel, and will be resumed when user finish interacting.
/// Default to `true`.
final bool pauseAutoPlayOnTouch;
/// If `true`, the auto play function will be paused when user is calling
/// pageController's `nextPage` or `previousPage` or `animateToPage` method.
/// And after the animation complete, the auto play will be resumed.
/// Default to `true`.
final bool pauseAutoPlayOnManualNavigate;
/// If `enableInfiniteScroll` is `false`, and `autoPlay` is `true`, this option
/// decide the carousel should go to the first item when it reach the last item or not.
/// If set to `true`, the auto play will be paused when it reach the last item.
/// If set to `false`, the auto play function will animate to the first item when it was
/// in the last item.
final bool pauseAutoPlayInFiniteScroll;
/// Pass a `PageStoragekey` if you want to keep the pageview's position when it was recreated.
final PageStorageKey? pageViewKey;
/// Use `enlargeStrategy` to determine which method to enlarge the center page.
final CenterPageEnlargeStrategy enlargeStrategy;
/// Whether or not to disable the `Center` widget for each slide.
final bool disableCenter;
/// Whether to add padding to both ends of the list.
/// If this is set to true and [viewportFraction] < 1.0, padding will be added such that the first and last child slivers will be in the center of the viewport when scrolled all the way to the start or end, respectively.
/// If [viewportFraction] >= 1.0, this property has no effect.
/// This property defaults to true and must not be null.
final bool padEnds;
/// Exposed clipBehavior of PageView
final Clip clipBehavior;
CarouselOptions({
this.height,
this.aspectRatio = 16 / 9,
this.viewportFraction = 0.8,
this.initialPage = 0,
this.enableInfiniteScroll = true,
this.reverse = false,
this.autoPlay = false,
this.autoPlayInterval = const Duration(seconds: 4),
this.autoPlayAnimationDuration = const Duration(milliseconds: 800),
this.autoPlayCurve = Curves.fastOutSlowIn,
this.enlargeCenterPage = false,
this.onPageChanged,
this.onScrolled,
this.scrollPhysics,
this.pageSnapping = true,
this.scrollDirection = Axis.horizontal,
this.pauseAutoPlayOnTouch = true,
this.pauseAutoPlayOnManualNavigate = true,
this.pauseAutoPlayInFiniteScroll = false,
this.pageViewKey,
this.enlargeStrategy = CenterPageEnlargeStrategy.scale,
this.disableCenter = false,
this.padEnds = true,
this.clipBehavior = Clip.hardEdge,
});
///Generate new [CarouselOptions] based on old ones.
CarouselOptions copyWith(
{double? height,
double? aspectRatio,
double? viewportFraction,
int? initialPage,
bool? enableInfiniteScroll,
bool? reverse,
bool? autoPlay,
Duration? autoPlayInterval,
Duration? autoPlayAnimationDuration,
Curve? autoPlayCurve,
bool? enlargeCenterPage,
Function(int index, CarouselPageChangedReason reason)? onPageChanged,
ValueChanged<double?>? onScrolled,
ScrollPhysics? scrollPhysics,
bool? pageSnapping,
Axis? scrollDirection,
bool? pauseAutoPlayOnTouch,
bool? pauseAutoPlayOnManualNavigate,
bool? pauseAutoPlayInFiniteScroll,
PageStorageKey? pageViewKey,
CenterPageEnlargeStrategy? enlargeStrategy,
bool? disableCenter,
Clip? clipBehavior,
bool? padEnds}) =>
CarouselOptions(
height: height ?? this.height,
aspectRatio: aspectRatio ?? this.aspectRatio,
viewportFraction: viewportFraction ?? this.viewportFraction,
initialPage: initialPage ?? this.initialPage,
enableInfiniteScroll: enableInfiniteScroll ?? this.enableInfiniteScroll,
reverse: reverse ?? this.reverse,
autoPlay: autoPlay ?? this.autoPlay,
autoPlayInterval: autoPlayInterval ?? this.autoPlayInterval,
autoPlayAnimationDuration:
autoPlayAnimationDuration ?? this.autoPlayAnimationDuration,
autoPlayCurve: autoPlayCurve ?? this.autoPlayCurve,
enlargeCenterPage: enlargeCenterPage ?? this.enlargeCenterPage,
onPageChanged: onPageChanged ?? this.onPageChanged,
onScrolled: onScrolled ?? this.onScrolled,
scrollPhysics: scrollPhysics ?? this.scrollPhysics,
pageSnapping: pageSnapping ?? this.pageSnapping,
scrollDirection: scrollDirection ?? this.scrollDirection,
pauseAutoPlayOnTouch: pauseAutoPlayOnTouch ?? this.pauseAutoPlayOnTouch,
pauseAutoPlayOnManualNavigate:
pauseAutoPlayOnManualNavigate ?? this.pauseAutoPlayOnManualNavigate,
pauseAutoPlayInFiniteScroll:
pauseAutoPlayInFiniteScroll ?? this.pauseAutoPlayInFiniteScroll,
pageViewKey: pageViewKey ?? this.pageViewKey,
enlargeStrategy: enlargeStrategy ?? this.enlargeStrategy,
disableCenter: disableCenter ?? this.disableCenter,
clipBehavior: clipBehavior ?? this.clipBehavior,
padEnds: padEnds ?? this.padEnds,
);
}

View file

@ -0,0 +1,354 @@
library carousel_slider;
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'carousel_state.dart';
import 'carousel_utils.dart';
import 'carousel_controller.dart';
import 'carousel_options.dart';
export 'carousel_controller.dart';
export 'carousel_options.dart';
typedef ExtendedIndexedWidgetBuilder = Widget Function(
BuildContext context, int index, int realIndex);
class CarouselSlider extends StatefulWidget {
/// [CarouselOptions] to create a [CarouselState] with
final CarouselOptions options;
/// The widgets to be shown in the carousel of default constructor
final List<Widget>? items;
/// The widget item builder that will be used to build item on demand
/// The third argument is the PageView's real index, can be used to cooperate
/// with Hero.
final ExtendedIndexedWidgetBuilder? itemBuilder;
/// A [MapController], used to control the map.
final CarouselControllerImpl _carouselController;
final int? itemCount;
CarouselSlider(
{required this.items,
required this.options,
CarouselController? carouselController,
Key? key})
: itemBuilder = null,
itemCount = items != null ? items.length : 0,
_carouselController = carouselController != null
? carouselController as CarouselControllerImpl
: CarouselController() as CarouselControllerImpl,
super(key: key);
/// The on demand item builder constructor
CarouselSlider.builder(
{required this.itemCount,
required this.itemBuilder,
required this.options,
CarouselController? carouselController,
Key? key})
: items = null,
_carouselController = carouselController != null
? carouselController as CarouselControllerImpl
: CarouselController() as CarouselControllerImpl,
super(key: key);
@override
CarouselSliderState createState() => CarouselSliderState();
}
class CarouselSliderState extends State<CarouselSlider>
with TickerProviderStateMixin {
late CarouselControllerImpl carouselController;
Timer? timer;
CarouselOptions get options => widget.options;
CarouselState? carouselState;
PageController? pageController;
/// mode is related to why the page is being changed
CarouselPageChangedReason mode = CarouselPageChangedReason.controller;
CarouselSliderState();
void changeMode(CarouselPageChangedReason mode) {
this.mode = mode;
}
@override
void didUpdateWidget(CarouselSlider oldWidget) {
carouselState!.options = options;
carouselState!.itemCount = widget.itemCount;
// pageController needs to be re-initialized to respond to state changes
pageController = PageController(
viewportFraction: options.viewportFraction,
initialPage: carouselState!.realPage,
);
carouselState!.pageController = pageController;
// handle autoplay when state changes
handleAutoPlay();
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
super.initState();
carouselController = widget._carouselController;
carouselState = CarouselState(options, clearTimer, resumeTimer, changeMode);
carouselState!.itemCount = widget.itemCount;
carouselController.state = carouselState;
carouselState!.initialPage = widget.options.initialPage;
carouselState!.realPage = options.enableInfiniteScroll
? carouselState!.realPage + carouselState!.initialPage
: carouselState!.initialPage;
handleAutoPlay();
pageController = PageController(
viewportFraction: options.viewportFraction,
initialPage: carouselState!.realPage,
);
carouselState!.pageController = pageController;
}
Timer? getTimer() {
return widget.options.autoPlay
? Timer.periodic(widget.options.autoPlayInterval, (_) {
final route = ModalRoute.of(context);
if (route?.isCurrent == false) {
return;
}
CarouselPageChangedReason previousReason = mode;
changeMode(CarouselPageChangedReason.timed);
int nextPage = carouselState!.pageController!.page!.round() + 1;
int itemCount = widget.itemCount ?? widget.items!.length;
if (nextPage >= itemCount &&
widget.options.enableInfiniteScroll == false) {
if (widget.options.pauseAutoPlayInFiniteScroll) {
clearTimer();
return;
}
nextPage = 0;
}
carouselState!.pageController!
.animateToPage(nextPage,
duration: widget.options.autoPlayAnimationDuration,
curve: widget.options.autoPlayCurve)
.then((_) => changeMode(previousReason));
})
: null;
}
void clearTimer() {
if (timer != null) {
timer?.cancel();
timer = null;
}
}
void resumeTimer() {
timer ??= getTimer();
}
void handleAutoPlay() {
bool autoPlayEnabled = widget.options.autoPlay;
if (autoPlayEnabled && timer != null) return;
clearTimer();
if (autoPlayEnabled) {
resumeTimer();
}
}
Widget getGestureWrapper(Widget child) {
Widget wrapper;
if (widget.options.height != null) {
wrapper = SizedBox(height: widget.options.height, child: child);
} else {
wrapper =
AspectRatio(aspectRatio: widget.options.aspectRatio, child: child);
}
return RawGestureDetector(
gestures: {
_MultipleGestureRecognizer:
GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>(
() => _MultipleGestureRecognizer(),
(_MultipleGestureRecognizer instance) {
instance.onStart = (_) {
onStart();
};
instance.onDown = (_) {
onPanDown();
};
instance.onEnd = (_) {
onPanUp();
};
instance.onCancel = () {
onPanUp();
};
}),
},
child: NotificationListener(
onNotification: (Notification notification) {
if (widget.options.onScrolled != null &&
notification is ScrollUpdateNotification) {
widget.options.onScrolled!(carouselState!.pageController!.page);
}
return false;
},
child: wrapper,
),
);
}
Widget getCenterWrapper(Widget child) {
if (widget.options.disableCenter) {
return Container(
child: child,
);
}
return Center(child: child);
}
Widget getEnlargeWrapper(Widget? child,
{double? width, double? height, double? scale}) {
if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.height) {
return SizedBox(width: width, height: height, child: child);
}
return Transform.scale(
scale: scale!,
child: SizedBox(width: width, height: height, child: child));
}
void onStart() {
changeMode(CarouselPageChangedReason.manual);
}
void onPanDown() {
if (widget.options.pauseAutoPlayOnTouch) {
clearTimer();
}
changeMode(CarouselPageChangedReason.manual);
}
void onPanUp() {
if (widget.options.pauseAutoPlayOnTouch) {
resumeTimer();
}
}
@override
void dispose() {
super.dispose();
clearTimer();
}
@override
Widget build(BuildContext context) {
return getGestureWrapper(PageView.builder(
padEnds: widget.options.padEnds,
scrollBehavior: ScrollConfiguration.of(context).copyWith(
scrollbars: false,
overscroll: false,
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
),
clipBehavior: widget.options.clipBehavior,
physics: widget.options.scrollPhysics,
scrollDirection: widget.options.scrollDirection,
pageSnapping: widget.options.pageSnapping,
controller: carouselState!.pageController,
reverse: widget.options.reverse,
itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount,
key: widget.options.pageViewKey,
onPageChanged: (int index) {
int currentPage = getRealIndex(index + carouselState!.initialPage,
carouselState!.realPage, widget.itemCount);
if (widget.options.onPageChanged != null) {
widget.options.onPageChanged!(currentPage, mode);
}
},
itemBuilder: (BuildContext context, int idx) {
final int index = getRealIndex(idx + carouselState!.initialPage,
carouselState!.realPage, widget.itemCount);
return AnimatedBuilder(
animation: carouselState!.pageController!,
child: (widget.items != null)
? (widget.items!.isNotEmpty ? widget.items![index] : Container())
: widget.itemBuilder!(context, index, idx),
builder: (BuildContext context, child) {
double distortionValue = 1.0;
// if `enlargeCenterPage` is true, we must calculate the carousel item's height
// to display the visual effect
if (widget.options.enlargeCenterPage != null &&
widget.options.enlargeCenterPage == true) {
// pageController.page can only be accessed after the first build,
// so in the first build we calculate the itemoffset manually
double itemOffset = 0;
var position = carouselState?.pageController?.position;
if (position != null &&
position.hasPixels &&
position.hasContentDimensions) {
var page = carouselState?.pageController?.page;
if (page != null) {
itemOffset = page - idx;
}
} else {
BuildContext storageContext = carouselState!
.pageController!.position.context.storageContext;
final double? previousSavedPosition =
PageStorage.of(storageContext)?.readState(storageContext)
as double?;
if (previousSavedPosition != null) {
itemOffset = previousSavedPosition - idx.toDouble();
} else {
itemOffset =
carouselState!.realPage.toDouble() - idx.toDouble();
}
}
final num distortionRatio =
(1 - (itemOffset.abs() * 0.3)).clamp(0.0, 1.0);
distortionValue =
Curves.easeOut.transform(distortionRatio as double);
}
final double height = widget.options.height ??
MediaQuery.of(context).size.width *
(1 / widget.options.aspectRatio);
if (widget.options.scrollDirection == Axis.horizontal) {
return getCenterWrapper(getEnlargeWrapper(child,
height: distortionValue * height, scale: distortionValue));
} else {
return getCenterWrapper(getEnlargeWrapper(child,
width: distortionValue * MediaQuery.of(context).size.width,
scale: distortionValue));
}
},
);
},
));
}
}
class _MultipleGestureRecognizer extends PanGestureRecognizer {}

View file

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'carousel_slider.dart';
class CarouselState {
/// The [CarouselOptions] to create this state
CarouselOptions options;
/// [pageController] is created using the properties passed to the constructor
/// and can be used to control the [PageView] it is passed to.
PageController? pageController;
/// The actual index of the [PageView].
///
/// This value can be ignored unless you know the carousel will be scrolled
/// backwards more then 10000 pages.
/// Defaults to 10000 to simulate infinite backwards scrolling.
int realPage = 10000;
/// The initial index of the [PageView] on [CarouselSlider] init.
///
int initialPage = 0;
/// The widgets count that should be shown at carousel
int? itemCount;
/// Will be called when using pageController to go to next page or
/// previous page. It will clear the autoPlay timer.
/// Internal use only
Function onResetTimer;
/// Will be called when using pageController to go to next page or
/// previous page. It will restart the autoPlay timer.
/// Internal use only
Function onResumeTimer;
/// The callback to set the Reason Carousel changed
Function(CarouselPageChangedReason) changeMode;
CarouselState(
this.options, this.onResetTimer, this.onResumeTimer, this.changeMode);
}

View file

@ -0,0 +1,23 @@
/// Converts an index of a set size to the corresponding index of a collection of another size
/// as if they were circular.
///
/// Takes a [position] from collection Foo, a [base] from where Foo's index originated
/// and the [length] of a second collection Baa, for which the correlating index is sought.
///
/// For example; We have a Carousel of 10000(simulating infinity) but only 6 images.
/// We need to repeat the images to give the illusion of a never ending stream.
/// By calling _getRealIndex with position and base we get an offset.
/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image
/// to be placed in the given position.
int getRealIndex(int position, int base, int? length) {
final int offset = position - base;
return remainder(offset, length);
}
/// Returns the remainder of the modulo operation [input] % [source], and adjust it for
/// negative values.
int remainder(int input, int? source) {
if (source == 0) return 0;
final int result = input % source!;
return result < 0 ? source + result : result;
}

View file

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:flutter_form/next_shell/translation_service.dart';
import 'carousel_form.dart';
/// Input for a carousel of items used in a [ShellForm].
///
/// items will be the [Widget]s to be displayed in the carousel.
///
/// Standard controller is [ShellFormInputCarouselController].
class ShellFormInputCarousel extends ShellFormInputWidget {
const ShellFormInputCarousel({
Key? key,
required ShellFormInputController controller,
Widget? label,
required this.items,
}) : super(key: key, controller: controller, label: label);
final List<Widget> items;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return CarouselFormField(
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
initialValue: controller.value ?? 0,
items: items,
);
}
}
/// Controller for the carousel used by a [ShellFormInputWidget] used in a [ShellFrom].
///
/// Mainly used by [ShellFormInputCarousel].
class ShellFormInputCarouselController
implements ShellFormInputController<int> {
ShellFormInputCarouselController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
int? value;
@override
bool mandatory;
@override
String Function(int value)? checkPageTitle;
@override
String Function(int value)? checkPageDescription;
@override
void onSaved(int value) {
this.value = value;
}
@override
String? onValidate(
int value, String Function(String, {List<String>? params}) translator) {
if (mandatory) {}
return null;
}
}

View file

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_form/next_shell/translation_service.dart';
import '../../../../flutter_form.dart';
/// Input for an email used in a [ShellForm].
///
/// Standard controller is [ShellFormInputEmailController].
class ShellFormInputEmail extends ShellFormInputWidget {
const ShellFormInputEmail({
Key? key,
required ShellFormInputController controller,
Widget? label,
}) : super(
key: key,
controller: controller,
label: label,
);
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return TextFormField(
initialValue: controller.value,
onSaved: (value) {
controller.onSaved(value);
},
validator: (value) => controller.onValidate(value, _),
decoration: InputDecoration(
focusColor: Theme.of(context).primaryColor,
label: label ?? const Text("Email"),
),
);
}
}
/// Controller for emails used by a [ShellFormInputWidget] used in a [ShellFrom].
///
/// Mainly used by [ShellFormInputEmail].
class ShellFormInputEmailController
implements ShellFormInputController<String> {
ShellFormInputEmailController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
String? value;
@override
bool mandatory;
@override
String Function(String value)? checkPageTitle;
@override
String Function(String value)? checkPageDescription;
@override
void onSaved(dynamic value) {
this.value = value;
}
@override
String? onValidate(String? value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {
if (value == null || value.isEmpty) {
return translator('shell.form.error.empty');
}
if (!RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return translator('shell.form.error.email.notValid');
}
}
return null;
}
}

View file

@ -0,0 +1,112 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'numberpicker.dart';
class DecimalNumberPicker extends StatelessWidget {
final int minValue;
final int maxValue;
final double value;
final ValueChanged<double> onChanged;
final int itemCount;
final double itemHeight;
final double itemWidth;
final Axis axis;
final TextStyle? textStyle;
final TextStyle? selectedTextStyle;
final bool haptics;
final TextMapper? integerTextMapper;
final TextMapper? decimalTextMapper;
final bool integerZeroPad;
/// Decoration to apply to central box where the selected integer value is placed
final Decoration? integerDecoration;
/// Decoration to apply to central box where the selected decimal value is placed
final Decoration? decimalDecoration;
/// Inidcates how many decimal places to show
/// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...]
final int decimalPlaces;
const DecimalNumberPicker({
Key? key,
required this.minValue,
required this.maxValue,
required this.value,
required this.onChanged,
this.itemCount = 3,
this.itemHeight = 50,
this.itemWidth = 100,
this.axis = Axis.vertical,
this.textStyle,
this.selectedTextStyle,
this.haptics = false,
this.decimalPlaces = 1,
this.integerTextMapper,
this.decimalTextMapper,
this.integerZeroPad = false,
this.integerDecoration,
this.decimalDecoration,
}) : assert(minValue <= value),
assert(value <= maxValue),
super(key: key);
@override
Widget build(BuildContext context) {
final isMax = value.floor() == maxValue;
final decimalValue = isMax
? 0
: ((value - value.floorToDouble()) * math.pow(10, decimalPlaces))
.round();
final doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
NumberPicker(
minValue: minValue,
maxValue: maxValue,
value: value.floor(),
onChanged: _onIntChanged,
itemCount: itemCount,
itemHeight: itemHeight,
itemWidth: itemWidth,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
haptics: haptics,
zeroPad: integerZeroPad,
textMapper: integerTextMapper,
decoration: integerDecoration,
),
NumberPicker(
minValue: 0,
maxValue: doubleMaxValue,
value: decimalValue,
onChanged: _onDoubleChanged,
itemCount: itemCount,
itemHeight: itemHeight,
itemWidth: itemWidth,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
haptics: haptics,
textMapper: decimalTextMapper,
decoration: decimalDecoration,
),
],
);
}
void _onIntChanged(int intValue) {
final newValue =
(value - value.floor() + intValue).clamp(minValue, maxValue);
onChanged(newValue.toDouble());
}
void _onDoubleChanged(int doubleValue) {
final decimalPart = double.parse(
(doubleValue * math.pow(10, -decimalPlaces))
.toStringAsFixed(decimalPlaces));
onChanged(value.floor() + decimalPart);
}
}

View file

@ -0,0 +1,362 @@
library infinite_listview;
import 'dart:math' as math;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class InfiniteListView extends StatefulWidget {
/// See [ListView.builder]
const InfiniteListView.builder({
Key? key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.physics,
this.padding,
this.itemExtent,
required this.itemBuilder,
this.itemCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.anchor = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : separatorBuilder = null,
super(key: key);
/// See [ListView.separated]
const InfiniteListView.separated({
Key? key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.physics,
this.padding,
required this.itemBuilder,
required this.separatorBuilder,
this.itemCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.anchor = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : itemExtent = null,
super(key: key);
/// See: [ScrollView.scrollDirection]
final Axis scrollDirection;
/// See: [ScrollView.reverse]
final bool reverse;
/// See: [ScrollView.controller]
final InfiniteScrollController? controller;
/// See: [ScrollView.physics]
final ScrollPhysics? physics;
/// See: [BoxScrollView.padding]
final EdgeInsets? padding;
/// See: [ListView.builder]
final IndexedWidgetBuilder itemBuilder;
/// See: [ListView.separated]
final IndexedWidgetBuilder? separatorBuilder;
/// See: [SliverChildBuilderDelegate.childCount]
final int? itemCount;
/// See: [ListView.itemExtent]
final double? itemExtent;
/// See: [ScrollView.cacheExtent]
final double? cacheExtent;
/// See: [ScrollView.anchor]
final double anchor;
/// See: [SliverChildBuilderDelegate.addAutomaticKeepAlives]
final bool addAutomaticKeepAlives;
/// See: [SliverChildBuilderDelegate.addRepaintBoundaries]
final bool addRepaintBoundaries;
/// See: [SliverChildBuilderDelegate.addSemanticIndexes]
final bool addSemanticIndexes;
/// See: [ScrollView.dragStartBehavior]
final DragStartBehavior dragStartBehavior;
/// See: [ScrollView.keyboardDismissBehavior]
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// See: [ScrollView.restorationId]
final String? restorationId;
/// See: [ScrollView.clipBehavior]
final Clip clipBehavior;
@override
InfiniteListViewState createState() => InfiniteListViewState();
}
class InfiniteListViewState extends State<InfiniteListView> {
InfiniteScrollController? _controller;
InfiniteScrollController get _effectiveController =>
widget.controller ?? _controller!;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = InfiniteScrollController();
}
}
@override
void didUpdateWidget(InfiniteListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null) {
_controller = InfiniteScrollController();
} else if (widget.controller != null && oldWidget.controller == null) {
_controller!.dispose();
_controller = null;
}
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final List<Widget> slivers = _buildSlivers(context, negative: false);
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
final AxisDirection axisDirection = _getDirection(context);
final scrollPhysics =
widget.physics ?? const AlwaysScrollableScrollPhysics();
return Scrollable(
axisDirection: axisDirection,
controller: _effectiveController,
physics: scrollPhysics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return Builder(builder: (BuildContext context) {
/// Build negative [ScrollPosition] for the negative scrolling [Viewport].
final state = Scrollable.of(context)!;
final negativeOffset = _InfiniteScrollPosition(
physics: scrollPhysics,
context: state,
initialPixels: -offset.pixels,
keepScrollOffset: _effectiveController.keepScrollOffset,
negativeScroll: true,
);
/// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
offset.addListener(() {
negativeOffset._forceNegativePixels(offset.pixels);
});
/// Stack the two [Viewport]s on top of each other so they move in sync.
return Stack(
children: <Widget>[
Viewport(
axisDirection: flipAxisDirection(axisDirection),
anchor: 1.0 - widget.anchor,
offset: negativeOffset,
slivers: negativeSlivers,
cacheExtent: widget.cacheExtent,
),
Viewport(
axisDirection: axisDirection,
anchor: widget.anchor,
offset: offset,
slivers: slivers,
cacheExtent: widget.cacheExtent,
),
],
);
});
},
);
}
AxisDirection _getDirection(BuildContext context) {
return getAxisDirectionFromAxisReverseAndDirectionality(
context, widget.scrollDirection, widget.reverse);
}
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
final itemExtent = widget.itemExtent;
final padding = widget.padding ?? EdgeInsets.zero;
return <Widget>[
SliverPadding(
padding: negative
? padding - EdgeInsets.only(bottom: padding.bottom)
: padding - EdgeInsets.only(top: padding.top),
sliver: (itemExtent != null)
? SliverFixedExtentList(
delegate: negative
? negativeChildrenDelegate
: positiveChildrenDelegate,
itemExtent: itemExtent,
)
: SliverList(
delegate: negative
? negativeChildrenDelegate
: positiveChildrenDelegate,
),
)
];
}
SliverChildDelegate get negativeChildrenDelegate {
return SliverChildBuilderDelegate(
(BuildContext context, int index) {
final separatorBuilder = widget.separatorBuilder;
if (separatorBuilder != null) {
final itemIndex = (-1 - index) ~/ 2;
return index.isOdd
? widget.itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
} else {
return widget.itemBuilder(context, -1 - index);
}
},
childCount: widget.itemCount,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
);
}
SliverChildDelegate get positiveChildrenDelegate {
final separatorBuilder = widget.separatorBuilder;
final itemCount = widget.itemCount;
return SliverChildBuilderDelegate(
(separatorBuilder != null)
? (BuildContext context, int index) {
final itemIndex = index ~/ 2;
return index.isEven
? widget.itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
}
: widget.itemBuilder,
childCount: separatorBuilder == null
? itemCount
: (itemCount != null ? math.max(0, itemCount * 2 - 1) : null),
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
properties.add(FlagProperty('reverse',
value: widget.reverse, ifTrue: 'reversed', showName: true));
properties.add(DiagnosticsProperty<ScrollController>(
'controller', widget.controller,
showName: false, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics,
showName: false, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
'padding', widget.padding,
defaultValue: null));
properties.add(
DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null));
properties.add(
DoubleProperty('cacheExtent', widget.cacheExtent, defaultValue: null));
}
}
/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
class InfiniteScrollController extends ScrollController {
/// Creates a new [InfiniteScrollController]
InfiniteScrollController({
double initialScrollOffset = 0.0,
bool keepScrollOffset = true,
String? debugLabel,
}) : super(
initialScrollOffset: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
debugLabel: debugLabel,
);
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition? oldPosition) {
return _InfiniteScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class _InfiniteScrollPosition extends ScrollPositionWithSingleContext {
_InfiniteScrollPosition({
required ScrollPhysics physics,
required ScrollContext context,
double? initialPixels = 0.0,
bool keepScrollOffset = true,
ScrollPosition? oldPosition,
String? debugLabel,
this.negativeScroll = false,
}) : super(
physics: physics,
context: context,
initialPixels: initialPixels,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
final bool negativeScroll;
void _forceNegativePixels(double value) {
super.forcePixels(-value);
}
@override
void saveScrollOffset() {
if (!negativeScroll) {
super.saveScrollOffset();
}
}
@override
void restoreScrollOffset() {
if (!negativeScroll) {
super.restoreScrollOffset();
}
}
@override
double get minScrollExtent => double.negativeInfinity;
@override
double get maxScrollExtent => double.infinity;
}

View file

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../flutter_form.dart';
import 'package:flutter_form/next_shell/translation_service.dart';
import 'numberpicker.dart';
class ShellFormInputNumberPicker extends ShellFormInputWidget {
const ShellFormInputNumberPicker({
Key? key,
required ShellFormInputController controller,
Widget? label,
this.minValue = 0,
this.maxValue = 100,
}) : assert(minValue < maxValue),
super(key: key, controller: controller, label: label);
final int minValue;
final int maxValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return NumberPickerFormField(
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
initialValue: controller.value ?? 0,
);
}
}
class NumberPickerFormField extends FormField<int> {
NumberPickerFormField({
Key? key,
required FormFieldSetter<int> onSaved,
required FormFieldValidator<int> validator,
int initialValue = 0,
bool autovalidate = false,
int minValue = 0,
int maxValue = 100,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<int> state) {
return NumberPicker(
minValue: minValue,
maxValue: maxValue,
value: initialValue,
onChanged: (int value) {
state.didChange(value);
},
itemHeight: 35,
itemCount: 5,
);
});
}
class ShellFormInputNumberPickerController
implements ShellFormInputController<int> {
ShellFormInputNumberPickerController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
int? value;
@override
bool mandatory;
@override
String Function(int value)? checkPageTitle;
@override
String Function(int value)? checkPageDescription;
@override
void onSaved(int value) {
this.value = value;
}
@override
String? onValidate(
int value, String Function(String, {List<String>? params}) translator) {
if (mandatory) {}
return null;
}
}

View file

@ -0,0 +1,305 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'infinite_listview.dart';
typedef TextMapper = String Function(String numberText);
class NumberPicker extends StatefulWidget {
/// Min value user can pick
final int minValue;
/// Max value user can pick
final int maxValue;
/// Currently selected value
final int value;
/// Called when selected value changes
final ValueChanged<int> onChanged;
/// Specifies how many items should be shown - defaults to 3
final int itemCount;
/// Step between elements. Only for integer datePicker
/// Examples:
/// if step is 100 the following elements may be 100, 200, 300...
/// if min=0, max=6, step=3, then items will be 0, 3 and 6
/// if min=0, max=5, step=3, then items will be 0 and 3.
final int step;
/// height of single item in pixels
final double itemHeight;
/// width of single item in pixels
final double itemWidth;
/// Direction of scrolling
final Axis axis;
/// Style of non-selected numbers. If null, it uses Theme's bodyText2
final TextStyle? textStyle;
/// Style of non-selected numbers. If null, it uses Theme's headline5 with accentColor
final TextStyle? selectedTextStyle;
/// Whether to trigger haptic pulses or not
final bool haptics;
/// Build the text of each item on the picker
final TextMapper? textMapper;
/// Pads displayed integer values up to the length of maxValue
final bool zeroPad;
/// Decoration to apply to central box where the selected value is placed
final Decoration? decoration;
final bool infiniteLoop;
const NumberPicker({
Key? key,
required this.minValue,
required this.maxValue,
required this.value,
required this.onChanged,
this.itemCount = 3,
this.step = 1,
this.itemHeight = 50,
this.itemWidth = 100,
this.axis = Axis.vertical,
this.textStyle,
this.selectedTextStyle,
this.haptics = false,
this.decoration,
this.zeroPad = false,
this.textMapper,
this.infiniteLoop = false,
}) : assert(minValue <= value),
assert(value <= maxValue),
super(key: key);
@override
NumberPickerState createState() => NumberPickerState();
}
class NumberPickerState extends State<NumberPicker> {
late ScrollController _scrollController;
late int value;
@override
void initState() {
super.initState();
value = widget.value;
final initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent;
if (widget.infiniteLoop) {
_scrollController =
InfiniteScrollController(initialScrollOffset: initialOffset);
} else {
_scrollController = ScrollController(initialScrollOffset: initialOffset);
}
_scrollController.addListener(_scrollListener);
}
void _scrollListener() {
var indexOfMiddleElement = (_scrollController.offset / itemExtent).round();
if (widget.infiniteLoop) {
indexOfMiddleElement %= itemCount;
} else {
indexOfMiddleElement = indexOfMiddleElement.clamp(0, itemCount - 1);
}
final intValueInTheMiddle =
_intValueFromIndex(indexOfMiddleElement + additionalItemsOnEachSide);
if (value != intValueInTheMiddle) {
setState(() {
value = intValueInTheMiddle;
});
widget.onChanged(intValueInTheMiddle);
if (widget.haptics) {
HapticFeedback.selectionClick();
}
}
Future.delayed(
const Duration(milliseconds: 100),
() => _maybeCenterValue(),
);
}
@override
void didUpdateWidget(NumberPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != value) {
_maybeCenterValue();
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
bool get isScrolling => _scrollController.position.isScrollingNotifier.value;
double get itemExtent =>
widget.axis == Axis.vertical ? widget.itemHeight : widget.itemWidth;
int get itemCount => (widget.maxValue - widget.minValue) ~/ widget.step + 1;
int get listItemsCount => itemCount + 2 * additionalItemsOnEachSide;
int get additionalItemsOnEachSide => (widget.itemCount - 1) ~/ 2;
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.axis == Axis.vertical
? widget.itemWidth
: widget.itemCount * widget.itemWidth,
height: widget.axis == Axis.vertical
? widget.itemCount * widget.itemHeight
: widget.itemHeight,
child: NotificationListener<ScrollEndNotification>(
onNotification: (not) {
if (not.dragDetails?.primaryVelocity == 0) {
Future.microtask(() => _maybeCenterValue());
}
return true;
},
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: Stack(
children: [
Center(
child: Container(
width: 300,
height: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: const Color(0xFFD8D8D8).withOpacity(0.50),
),
),
),
if (widget.infiniteLoop)
InfiniteListView.builder(
scrollDirection: widget.axis,
controller: _scrollController as InfiniteScrollController,
itemExtent: itemExtent,
itemBuilder: _itemBuilder,
padding: EdgeInsets.zero,
)
else
ListView.builder(
itemCount: listItemsCount,
scrollDirection: widget.axis,
controller: _scrollController,
itemExtent: itemExtent,
itemBuilder: _itemBuilder,
padding: EdgeInsets.zero,
),
_NumberPickerSelectedItemDecoration(
axis: widget.axis,
itemExtent: itemExtent,
decoration: widget.decoration,
),
],
),
),
),
);
}
Widget _itemBuilder(BuildContext context, int index) {
final themeData = Theme.of(context);
final defaultStyle = widget.textStyle ?? themeData.textTheme.bodyText2;
final selectedStyle = widget.selectedTextStyle ??
themeData.textTheme.headline5
?.copyWith(color: themeData.highlightColor);
final valueFromIndex = _intValueFromIndex(index % itemCount);
final isExtra = !widget.infiniteLoop &&
(index < additionalItemsOnEachSide ||
index >= listItemsCount - additionalItemsOnEachSide);
final itemStyle = valueFromIndex == value ? selectedStyle : defaultStyle;
final child = isExtra
? const SizedBox.shrink()
: Text(
_getDisplayedValue(valueFromIndex),
style: itemStyle,
);
return Container(
width: widget.itemWidth,
height: widget.itemHeight,
alignment: Alignment.center,
child: child,
);
}
String _getDisplayedValue(int value) {
final text = widget.zeroPad
? value.toString().padLeft(widget.maxValue.toString().length, '0')
: value.toString();
if (widget.textMapper != null) {
return widget.textMapper!(text);
} else {
return text;
}
}
int _intValueFromIndex(int index) {
index -= additionalItemsOnEachSide;
index %= itemCount;
return widget.minValue + index * widget.step;
}
void _maybeCenterValue() {
if (_scrollController.hasClients && !isScrolling) {
int diff = value - widget.minValue;
int index = diff ~/ widget.step;
if (widget.infiniteLoop) {
final offset = _scrollController.offset + 0.5 * itemExtent;
final cycles = (offset / (itemCount * itemExtent)).floor();
index += cycles * itemCount;
}
_scrollController.animateTo(
index * itemExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
);
}
}
}
class _NumberPickerSelectedItemDecoration extends StatelessWidget {
final Axis axis;
final double itemExtent;
final Decoration? decoration;
const _NumberPickerSelectedItemDecoration({
Key? key,
required this.axis,
required this.itemExtent,
required this.decoration,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: IgnorePointer(
child: Container(
width: isVertical ? double.infinity : itemExtent,
height: isVertical ? itemExtent : double.infinity,
decoration: decoration,
),
),
);
}
bool get isVertical => axis == Axis.vertical;
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/src/widgets/input/input_types/input_password/password.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../flutter_form.dart';
/// Input for a password used in a [ShellForm].
///
/// Standard controller is [ShellFormInputEmailController].
class ShellFormInputPassword extends ShellFormInputWidget {
const ShellFormInputPassword({
Key? key,
required ShellFormInputController controller,
Widget? label,
}) : super(key: key, controller: controller, label: label);
@override
Widget build(BuildContext context, WidgetRef ref) {
super.registerController(context);
return PasswordTextfield(
label: label,
controller: controller,
);
}
}
/// Controller for passwords used by a [ShellFormInputWidget] used in a [ShellFrom].
///
/// Mainly used by [ShellFormInputPassword].
class ShellFormInputPasswordController
implements ShellFormInputController<String> {
ShellFormInputPasswordController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
String? value;
@override
bool mandatory;
@override
String Function(String value)? checkPageTitle;
@override
String Function(String value)? checkPageDescription;
@override
void onSaved(dynamic value) {
this.value = value;
}
@override
String? onValidate(String? value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {
if (value == null || value.isEmpty) {
return translator('shell.form.error.empty');
}
if (value.length < 6) {
return translator('shell.form.error.empty');
}
}
return null;
}
}

View file

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../flutter_form.dart';
import 'package:flutter_form/next_shell/translation_service.dart';
class PasswordTextfield extends ConsumerStatefulWidget {
final Widget? label;
final ShellFormInputController controller;
const PasswordTextfield({
Key? key,
required this.controller,
this.label,
}) : super(key: key);
@override
ConsumerState<PasswordTextfield> createState() => _PasswordTextfieldState();
}
class _PasswordTextfieldState extends ConsumerState<PasswordTextfield> {
bool obscured = true;
@override
Widget build(BuildContext context) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
return TextFormField(
initialValue: widget.controller.value,
obscureText: obscured,
onSaved: (value) => widget.controller.onSaved(value),
validator: (value) => widget.controller.onValidate(value, _),
decoration: InputDecoration(
label: widget.label ?? const Text("Password"),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscured = !obscured;
});
},
icon: Icon(obscured ? Icons.visibility_off : Icons.visibility),
),
),
);
}
}

View file

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../flutter_form.dart';
import 'package:flutter_form/next_shell/translation_service.dart';
/// Input for an plain text used in a [ShellForm].
///
/// Standard controller is [ShellFormInputPlainTextController].
class ShellFormInputPlainText extends ShellFormInputWidget {
const ShellFormInputPlainText({
Key? key,
required ShellFormInputController controller,
Widget? label,
}) : super(key: key, controller: controller, label: label);
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return TextFormField(
initialValue: controller.value,
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
decoration: InputDecoration(
label: label ?? const Text("Plain text"),
),
);
}
}
/// Input for an plain text with extra styling used in a [ShellForm].
///
/// Standard controller is [ShellFormInputPlainTextController].
class ShellFormInputPlainTextWhiteWithBorder extends ShellFormInputWidget {
const ShellFormInputPlainTextWhiteWithBorder({
Key? key,
required ShellFormInputController controller,
Widget? label,
this.hint,
}) : super(key: key, controller: controller, label: label);
final String? hint;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return TextFormField(
initialValue: controller.value,
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
decoration: InputDecoration(
hintText: hint,
floatingLabelBehavior: FloatingLabelBehavior.never,
isDense: true,
border: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFF979797)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFF979797)),
),
fillColor: Colors.white,
filled: true,
),
);
}
}
/// Controller for plain text used by a [ShellFormInputWidget] used in a [ShellFrom].
///
/// Mainly used by [ShellFormInputPlainText].
class ShellFormInputPlainTextController
implements ShellFormInputController<String> {
ShellFormInputPlainTextController({
required this.id,
this.mandatory = false,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
String? value;
@override
bool mandatory;
@override
String Function(String value)? checkPageTitle;
@override
String Function(String value)? checkPageDescription;
@override
void onSaved(String value) {
this.value = value;
}
@override
String? onValidate(String? value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {
if (value == null || value.isEmpty) {
return translator('Field cannot be empty');
}
}
return null;
}
}

View file

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/src/widgets/input/input_types/input_slider/slider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_form/next_shell/translation_service.dart';
import '../../../../../flutter_form.dart';
/// Input for a number value between two values via a slider. Used in a [ShellForm].
///
/// Standard controller is [ShellFormInputSliderController].
class ShellFormInputSlider extends ShellFormInputWidget {
const ShellFormInputSlider({
Key? key,
required ShellFormInputController controller,
Widget? label,
this.minValue = 0,
this.maxValue = 100,
}) : assert(minValue < maxValue),
super(key: key, controller: controller, label: label);
final int minValue;
final int maxValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return SliderFormField(
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
);
}
}
/// Controller for slider used by a [ShellFormInputWidget] used in a [ShellFrom].
///
/// Mainly used by [ShellFormInputSlider].
class ShellFormInputSliderController
implements ShellFormInputController<double> {
ShellFormInputSliderController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
double? value;
@override
bool mandatory;
@override
String Function(double value)? checkPageTitle;
@override
String Function(double value)? checkPageDescription;
@override
void onSaved(double value) {
this.value = value;
}
@override
String? onValidate(double value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {}
return null;
}
}

View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class SliderFormField extends FormField<double> {
SliderFormField({
Key? key,
required FormFieldSetter<double> onSaved,
required FormFieldValidator<double> validator,
double initialValue = 0.5,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<double> state) {
return Slider(
value: state.value ?? initialValue,
onChanged: (double value) {
state.didChange(value);
},
);
});
}

View file

@ -0,0 +1,9 @@
export 'input_carousel/input_carousel.dart';
export 'input_carousel/input_carousel.dart';
export 'input_email.dart';
export 'input_number_picker/input_number_picker.dart';
export 'input_number_picker/input_number_picker.dart';
export 'input_password/input_password.dart';
export 'input_plain_text.dart';
export 'input_plain_text.dart';
export 'input_slider/input_slider.dart';

View file

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
abstract class PageIndicator extends StatelessWidget {
const PageIndicator({
this.steps = 3,
required this.currentStep,
Key? key,
}) : super(key: key);
final int steps;
final int currentStep;
}
class PageIndicatorCirlesLine extends PageIndicator {
const PageIndicatorCirlesLine({
steps = 3,
required currentStep,
Key? key,
}) : super(key: key, steps: steps, currentStep: currentStep);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < steps; i++) ...[
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: i <= currentStep
? Colors.black.withOpacity(0.80)
: const Color(0xFFF3F2F2),
borderRadius: BorderRadius.circular(45),
boxShadow: [
BoxShadow(
color:
Colors.black.withOpacity(i <= currentStep ? 0.40 : 0.10),
offset: const Offset(0, 2),
blurRadius: 5,
),
],
),
child: i == currentStep
? Center(
child: Padding(
padding: const EdgeInsets.only(left: 1.5),
child: Text(
(i + 1).toString(),
style: Theme.of(context).textTheme.overline!.copyWith(
fontSize: 12,
fontWeight: FontWeight.w900,
color: const Color(0xFFF3F2F2),
),
),
),
)
: i < currentStep
? const Center(
child: Padding(
padding: EdgeInsets.only(left: 1.5),
child: Icon(
Icons.check,
color: Colors.white,
size: 20,
),
),
)
: Container(),
),
if (i + 1 < steps)
const SizedBox(
width: 4,
),
if (i + 1 < steps)
Container(
width: 15,
height: 7,
decoration: BoxDecoration(
color: i + 1 <= currentStep
? Colors.black.withOpacity(0.80)
: const Color(0xFFF3F2F2),
borderRadius: BorderRadius.circular(3.5),
),
),
if (i + 1 < steps)
const SizedBox(
width: 4,
),
]
],
);
}
}

View file

@ -0,0 +1 @@
export 'page_indicator.dart';

View file

@ -3,6 +3,8 @@ description: A new Flutter package project.
version: 0.0.1 version: 0.0.1
homepage: homepage:
publish_to: none
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=2.18.0 <3.0.0'
flutter: ">=1.17.0" flutter: ">=1.17.0"
@ -10,16 +12,23 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
flutter_riverpod: ^1.0.4
localization: ^2.1.0
shell_model:
git:
url: git@bitbucket.org:iconicadevs/next_shell.git
ref: dbf7155ab18e79b5ff1da73f31c7fe3b06c4c82a
path: packages/shell_model
sliding_up_panel: ^2.0.0+1
uuid: ^3.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# To add assets to your package, add an assets section, like this: # To add assets to your package, add an assets section, like this:

View file

@ -1,12 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_form/flutter_form.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6);
expect(calculator.addOne(0), 1);
});
}