mirror of
https://github.com/Iconica-Development/flutter_form_wizard.git
synced 2025-05-19 19:03:47 +02:00
feat: port of form next-shell to appshell
This commit is contained in:
parent
9c9bb2d936
commit
16262dc75d
45 changed files with 3759 additions and 122 deletions
BIN
example/assets/images/BMW.png
Normal file
BIN
example/assets/images/BMW.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
example/assets/images/Mazda.png
Normal file
BIN
example/assets/images/Mazda.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 802 KiB |
BIN
example/assets/images/Mercedes.png
Normal file
BIN
example/assets/images/Mercedes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 103 KiB |
33
example/lib/example_pages/age_page.dart
Normal file
33
example/lib/example_pages/age_page.dart
Normal 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";
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
94
example/lib/example_pages/carousel_page.dart
Normal file
94
example/lib/example_pages/carousel_page.dart
Normal 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())
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
92
example/lib/example_pages/check_page.dart
Normal file
92
example/lib/example_pages/check_page.dart
Normal 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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
51
example/lib/example_pages/name_page.dart
Normal file
51
example/lib/example_pages/name_page.dart
Normal 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";
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
28
example/lib/example_pages/thanks_page.dart
Normal file
28
example/lib/example_pages/thanks_page.dart
Normal 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"))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
111
example/lib/form_example.dart
Normal file
111
example/lib/form_example.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:form_example/route.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const FormsExample());
|
||||
runApp(const ProviderScope(child: FormsExample()));
|
||||
}
|
||||
|
||||
class FormsExample extends StatelessWidget {
|
||||
|
@ -15,7 +17,22 @@ class FormsExample extends StatelessWidget {
|
|||
theme: ThemeData(
|
||||
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(
|
||||
child: ElevatedButton(
|
||||
onPressed: (() => createForm()),
|
||||
onPressed: (() => Navigator.of(context).pushNamed('/form')),
|
||||
child: const Text('Create form'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void createForm() {}
|
||||
}
|
||||
|
|
12
example/lib/route.dart
Normal file
12
example/lib/route.dart
Normal 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(),
|
||||
};
|
||||
}
|
69
example/lib/template_page.dart
Normal file
69
example/lib/template_page.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -36,6 +36,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -43,6 +50,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -55,6 +69,13 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_form:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -62,11 +83,30 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -74,6 +114,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
localization:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: localization
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -102,11 +149,34 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -121,6 +191,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -149,6 +226,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -158,3 +249,4 @@ packages:
|
|||
version: "2.1.2"
|
||||
sdks:
|
||||
dart: ">=2.18.0 <3.0.0"
|
||||
flutter: ">=3.0.0"
|
||||
|
|
|
@ -1,91 +1,30 @@
|
|||
name: example
|
||||
description: A new Flutter project.
|
||||
name: form_example
|
||||
description: Form example made with Flutter Form Package.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# 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
|
||||
publish_to: 'none'
|
||||
|
||||
# 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
|
||||
|
||||
environment:
|
||||
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:
|
||||
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
|
||||
flutter_riverpod: ^1.0.4
|
||||
flutter_form:
|
||||
path: ../
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
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
|
||||
|
||||
# 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:
|
||||
|
||||
# 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
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# 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
|
||||
assets:
|
||||
- assets/images/
|
|
@ -1,30 +1,30 @@
|
|||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
// // This is a basic Flutter widget test.
|
||||
// //
|
||||
// // 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
|
||||
// // 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.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:example/main.dart';
|
||||
// import 'package:example/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const FormsExample());
|
||||
// void main() {
|
||||
// testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// // Build our app and trigger a frame.
|
||||
// await tester.pumpWidget(const FormsExample());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
// // Verify that our counter starts at 0.
|
||||
// expect(find.text('0'), findsOneWidget);
|
||||
// expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
// // Tap the '+' icon and trigger a frame.
|
||||
// await tester.tap(find.byIcon(Icons.add));
|
||||
// await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
// // Verify that our counter has incremented.
|
||||
// expect(find.text('0'), findsNothing);
|
||||
// expect(find.text('1'), findsOneWidget);
|
||||
// });
|
||||
// }
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
library flutter_form;
|
||||
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
||||
export 'shell_form.dart';
|
||||
export 'src/widgets/input/abstractions.dart';
|
||||
export 'src/widgets/input/input_types/input_types.dart';
|
||||
export 'src/widgets/page_indicator/page_indicators.dart';
|
||||
|
|
72
lib/next_shell/form.dart
Normal file
72
lib/next_shell/form.dart
Normal 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,
|
||||
});
|
||||
}
|
5
lib/next_shell/providers.dart
Normal file
5
lib/next_shell/providers.dart
Normal 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());
|
51
lib/next_shell/translation_service.dart
Normal file
51
lib/next_shell/translation_service.dart
Normal 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
558
lib/shell_form.dart
Normal 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;
|
||||
}
|
||||
}
|
32
lib/src/utils/form_page_controller.dart
Normal file
32
lib/src/utils/form_page_controller.dart
Normal 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;
|
||||
}
|
||||
}
|
23
lib/src/utils/formstate.dart
Normal file
23
lib/src/utils/formstate.dart
Normal 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;
|
||||
}
|
63
lib/src/widgets/input/abstractions.dart
Normal file
63
lib/src/widgets/input/abstractions.dart
Normal 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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
92
lib/src/widgets/input/input_types/input_email.dart
Normal file
92
lib/src/widgets/input/input_types/input_email.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
120
lib/src/widgets/input/input_types/input_plain_text.dart
Normal file
120
lib/src/widgets/input/input_types/input_plain_text.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
22
lib/src/widgets/input/input_types/input_slider/slider.dart
Normal file
22
lib/src/widgets/input/input_types/input_slider/slider.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
9
lib/src/widgets/input/input_types/input_types.dart
Normal file
9
lib/src/widgets/input/input_types/input_types.dart
Normal 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';
|
94
lib/src/widgets/page_indicator/page_indicator.dart
Normal file
94
lib/src/widgets/page_indicator/page_indicator.dart
Normal 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,
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
1
lib/src/widgets/page_indicator/page_indicators.dart
Normal file
1
lib/src/widgets/page_indicator/page_indicators.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'page_indicator.dart';
|
17
pubspec.yaml
17
pubspec.yaml
|
@ -3,6 +3,8 @@ description: A new Flutter package project.
|
|||
version: 0.0.1
|
||||
homepage:
|
||||
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.0 <3.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
@ -10,16 +12,23 @@ environment:
|
|||
dependencies:
|
||||
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:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
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:
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue