features_tour 0.7.0-rc.3
features_tour: ^0.7.0-rc.3 copied to clipboard
Features Tour is a package that enables you to easily create tours to introduce your widget features in your app with order.
import 'package:features_tour/features_tour.dart';
import 'package:flutter/material.dart';
void main() {
FeaturesTour.setGlobalConfig(
preDialogConfig: PreDialogConfig(
enabled: true,
title: 'Welcome to Features Tour',
content: 'This tour will guide you through the main features of the app.',
applyToAllCheckboxLabel: 'Apply to all pages',
acceptButtonLabel: 'Start Tour',
laterButtonLabel: 'Later',
dismissButtonLabel: 'Dismiss',
),
introduceConfig: RoundedRectIntroduceConfig(),
childConfig: ChildConfig(isAnimateChild: false),
skipConfig: SkipConfig(
builder: (context, onPressed) => ElevatedButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.skip_next),
label: const Text('SKIP'),
),
),
nextConfig: NextConfig(
builder: (context, onPressed) => FilledButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.arrow_forward),
label: const Text('NEXT'),
),
),
previousConfig: PreviousConfig(
builder: (context, onPressed) => FilledButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.arrow_back),
label: const Text('PREVIOUS'),
),
),
doneConfig: DoneConfig(
builder: (context, onPressed) => FilledButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.done),
label: const Text('DONE'),
),
),
debugLog: true,
);
runApp(const ChangeableThemeMaterialApp());
}
enum MainTourStep {
drawer,
buttonOnDrawer,
settingAction,
list,
firstItem,
item90,
dialogButton,
restartTourButton,
floatingButton,
}
class ChangeableThemeMaterialApp extends StatefulWidget {
const ChangeableThemeMaterialApp({super.key});
@override
State<ChangeableThemeMaterialApp> createState() =>
_ChangeableThemeMaterialAppState();
}
class _ChangeableThemeMaterialAppState
extends State<ChangeableThemeMaterialApp> {
bool isDark = false;
void toggleTheme() {
setState(() {
isDark = !isDark;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Features Tour Example',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: isDark ? ThemeMode.dark : ThemeMode.light,
home: const App(),
);
}
}
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
final tourController = FeaturesTourController<MainTourStep>('App');
final scrollController = ScrollController();
final scaffoldKey = GlobalKey<ScaffoldState>();
void _openDrawer() => scaffoldKey.currentState?.openDrawer();
void _closeDrawer() => scaffoldKey.currentState?.closeDrawer();
void _showTourDialogIfNeeded() {
if (!mounted) return;
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: const Text('A Dialog'),
actions: [
FeaturesTour(
controller: tourController,
step: MainTourStep.dialogButton,
introduce: const Text('Tap here to close the dialog'),
onAfterAction: (action) {
Navigator.of(dialogContext).pop();
},
child: TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: const Text('Ok'),
),
),
],
);
},
);
}
@override
void initState() {
tourController.start(context);
super.initState();
}
@override
Widget build(BuildContext context) {
final changeableState =
context.findAncestorStateOfType<_ChangeableThemeMaterialAppState>()!;
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: const Text('App'),
leading: FeaturesTour(
controller: tourController,
step: MainTourStep.drawer,
nextStep: MainTourStep.buttonOnDrawer,
onAfterAction: (action) {
if (action != TourAction.next && action != TourAction.done) {
return;
}
_openDrawer();
},
introduce: const Text('Tap here to open the drawer'),
child: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
_openDrawer();
},
),
),
actions: [
FeaturesTour(
controller: tourController,
step: MainTourStep.settingAction,
onAfterAction: (action) {
if (action == TourAction.previous) {
_openDrawer();
}
},
introduce: const Text(
'Tap here to change the brightness and reset the tour'),
child: IconButton(
icon: changeableState.isDark
? const Icon(Icons.dark_mode)
: const Icon(Icons.light_mode),
onPressed: () async {
changeableState.toggleTheme();
tourController.start(context, force: true);
},
),
)
],
),
drawer: Drawer(
child: Center(
child: FeaturesTour(
controller: tourController,
step: MainTourStep.buttonOnDrawer,
introduce: const Text('Tap here to close the drawer'),
onAfterAction: (action) {
if (action
case TourAction.next ||
TourAction.done ||
TourAction.previous ||
TourAction.skip) {
_closeDrawer();
}
},
child: ElevatedButton(
child: const Text('Close Drawer'),
onPressed: () {
_closeDrawer();
},
),
),
),
),
body: FeaturesTour(
controller: tourController,
step: MainTourStep.list,
introduce: Text(
'This is a list of items',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
),
),
introduceConfig: IntroduceConfig(
builder: (context, childRect, introduce) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorScheme.of(context).primary.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(8),
),
child: introduce,
);
},
quadrantAlignment: QuadrantAlignment.inside,
),
childConfig: ChildConfig(
enableAnimation: false,
),
child: Stack(
children: [
SingleChildScrollView(
controller: scrollController,
child: SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FeaturesTour(
controller: tourController,
step: MainTourStep.firstItem,
nextStep: MainTourStep.item90,
introduce: const Text('This is the item 0'),
onBeforeAction: (action) async {
if (action case TourAction.previous) {
await scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text('Item 0'),
),
),
for (var index = 1; index <= 100; index++)
FeaturesTour(
enabled: index == 95,
controller: tourController,
step: MainTourStep.item90,
nextStep: MainTourStep.dialogButton,
introduce: Text('This is the item $index'),
onBeforeAction: (action) async {
if (action
case TourAction.next || TourAction.previous) {
// Scroll to the last item when the first item is tapped
await scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
onAfterAction: (action) async {
if (action case TourAction.previous) {
// Scroll to the first item when item 90 is tapped
await scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
return;
}
if (action case TourAction.next) {
_showTourDialogIfNeeded();
}
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Item $index'),
),
),
],
),
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FeaturesTour(
controller: tourController,
step: MainTourStep.restartTourButton,
onAfterAction: (action) {
if (action case TourAction.previous) {
_showTourDialogIfNeeded();
}
},
introduce:
const Text('Tap here to run the tour again'),
childConfig: ChildConfig(
shapeBorder: const CircleBorder(),
borderSizeInflate: 10.0,
),
child: FloatingActionButton(
onPressed: () {
tourController.start(
context,
force: true,
preDialogConfig:
PreDialogConfig(enabled: false),
);
},
child: const Icon(Icons.restart_alt_rounded),
),
),
FeaturesTour(
controller: tourController,
step: MainTourStep.floatingButton,
introduce: const Text('Tap here to add a new item'),
childConfig: ChildConfig(
shapeBorder: const CircleBorder(),
borderSizeInflate: 10.0,
),
child: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
),
],
),
FeaturesTourPadding(
controller: tourController,
steps: const {
MainTourStep.floatingButton,
MainTourStep.restartTourButton
},
),
],
),
),
),
],
),
),
);
}
}