Flutter引导教程插件tutorial_stage的使用
Flutter引导教程插件tutorial_stage的使用
tutorial_stage
是一个用于创建高度自定义的应用内教程的 Flutter 包。通过本指南,我们将了解如何使用 tutorial_stage
插件来实现应用内的引导教程。
开始使用
1. 添加依赖到 pubspec.yaml
在你的项目中,打开 pubspec.yaml
文件并添加 tutorial_stage
的依赖项:
dependencies:
tutorial_stage: <最新版本>
2. 导入包
在需要使用 tutorial_stage
的 Dart 文件中导入该包:
import 'package:tutorial_stage/tutorial_stage.dart';
3. 将 TutorialStage
添加到你的组件树中
将 TutorialStage
包装在你的根组件中,以便它可以管理教程流程:
TutorialStage(
child: SomeWidget(),
)
4. 添加 TutorialContent
(教程内容)
首先定义一些基本的教程内容类,这些类继承自 AnimatedTutorialContent
:
enum TutorialIdentifier {
button,
title,
counter,
}
class TutorialContentExample extends StatelessWidget {
const TutorialContentExample({
super.key,
required this.key,
required this.text,
});
final GlobalKey key;
final String text;
[@override](/user/override)
Widget build(BuildContext context) {
final Rect rect = key.boxPosition!.rect.withPadding(const EdgeInsets.all(4));
return SpotlightStage(
rect: rect,
borderRadius: const BorderRadius.all(Radius.circular(4)),
children: [
AlignRect(
rect: rect,
alignment: const Alignment(0.0, 2.0),
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: ElevatedButton(
onPressed: () => TutorialStage.of(context).next(),
child: Text(text),
),
),
),
],
);
}
}
class ButtonTutorialContent extends AnimatedTutorialContent {
ButtonTutorialContent(this._buttonKey)
: super(identifier: TutorialIdentifier.button);
final GlobalKey _buttonKey;
[@override](/user/override)
Widget buildContent(BuildContext context) {
return TutorialContentExample(
key: _buttonKey,
text: 'Button',
);
}
}
class TitleTutorialContent extends AnimatedTutorialContent {
TitleTutorialContent(this._titleKey)
: super(identifier: TutorialIdentifier.title);
final GlobalKey _titleKey;
[@override](/user/override)
Widget buildContent(BuildContext context) {
return TutorialContentExample(
key: _titleKey,
text: 'Title',
);
}
}
class CounterTutorialContent extends AnimatedTutorialContent {
CounterTutorialContent(this._counterKey)
: super(identifier: TutorialIdentifier.counter);
final GlobalKey _counterKey;
[@override](/user/override)
Future<void> start() async {
await Scrollable.ensureVisible(
_counterKey.currentContext!,
duration: const Duration(milliseconds: 300),
);
}
[@override](/user/override)
Widget buildContent(BuildContext context) {
return TutorialContentExample(
key: _counterKey,
text: 'Counter',
);
}
}
5. 启动教程
接下来,将教程启动逻辑添加到你的组件中。例如,在一个按钮的点击事件中启动教程:
final GlobalKey _buttonKey = GlobalKey();
final GlobalKey _titleKey = GlobalKey();
final GlobalKey _counterKey = GlobalKey();
[@override](/user/override)
Widget build(BuildContext context) {
return TutorialStage(
child: Scaffold(
appBar: AppBar(
title: Text(
widget.title,
key: _titleKey,
),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('This is text'),
Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).size.height,
),
),
Text(
'This the target text',
key: _counterKey,
style: Theme.of(context).textTheme.headline4,
),
const SizedBox(height: 200),
],
),
),
floatingActionButton: FloatingActionButton(
key: _buttonKey,
onPressed: _startTutorial,
child: const Text('Start'),
),
),
);
}
void _startTutorial() {
TutorialStage.build(
context: context,
contents: [
ButtonTutorialContent(_buttonKey),
TitleTutorialContent(_titleKey),
CounterTutorialContent(_counterKey),
],
).start();
}
完整示例代码
以下是一个完整的示例代码,展示了如何使用 tutorial_stage
创建一个简单的教程:
// 忽略对文件的检查
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:tutorial_stage/tutorial_stage.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tutorial Stage Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Tutorial Stage Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
[@override](/user/override)
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey _buttonKey = GlobalKey();
final GlobalKey _titleKey = GlobalKey();
final GlobalKey _counterKey = GlobalKey();
StreamSubscription<TutorialStateUpdate>? _tutorialStateSubscription;
[@override](/user/override)
Widget build(BuildContext context) {
return TutorialStage(
child: Scaffold(
appBar: AppBar(
title: Text(
widget.title,
key: _titleKey,
),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).size.height,
),
),
Text(
'1',
key: _counterKey,
style: Theme.of(context).textTheme.headline4,
),
const SizedBox(height: 200),
],
),
),
floatingActionButton: FloatingActionButton(
key: _buttonKey,
onPressed: _startTutorial,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
);
}
[@override](/user/override)
void dispose() {
_unlistenToTutorialStateUpdate();
super.dispose();
}
void _listenToTutorialState() {
_tutorialStateSubscription ??=
TutorialStage.of(context).state.listen(_onTutorialStateUpdate);
}
void _onTutorialStateUpdate(TutorialStateUpdate update) {
log(
'[${update.current.type.name}]\n'
'Previous Tutorial: ${update.previous?.identifier}\n'
'Current Tutorial: ${update.current.identifier}',
);
}
void _onPrestartTutorialContent(TutorialState currentState) {
if (currentState.type == TutorialStateType.finished) {
TutorialStage.of(context).reset();
}
}
void _unlistenToTutorialStateUpdate() {
_tutorialStateSubscription?.cancel();
_tutorialStateSubscription = null;
}
void _startTutorial() {
TutorialStage.build(
context: context,
onPrestart: _onPrestartTutorialContent,
contents: [
ButtonTutorialContent(),
BodyTutorialContent(),
CounterTutorialContent(_counterKey, _goToNextPage),
TitleTutorialContent(_titleKey),
],
).start();
_listenToTutorialState();
}
Future<void> _goToNextPage() async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const NextPage()),
);
// 等待过渡动画完成,以免影响目标小部件的位置
await Future<void>.delayed(const Duration(milliseconds: 300));
TutorialStage.of(context).next();
}
}
enum _TutorialIdentifier {
button,
body,
counter,
title,
}
class _ButtonTutorialContent extends AnimatedTutorialContent {
_ButtonTutorialContent()
: super(
identifier: _TutorialIdentifier.button,
reverseTransitionDuration: Duration.zero,
);
[@override](/user/override)
Widget buildContent(BuildContext context) {
return _DialogTutorialContentBuilder(
content: this,
direction: AxisDirection.down,
text: 'Dialog Down',
);
}
}
class _BodyTutorialContent extends AnimatedTutorialContent {
_BodyTutorialContent()
: super(
identifier: _TutorialIdentifier.body,
transitionDuration: Duration.zero,
);
[@override](/user/override)
Widget buildContent(BuildContext context) {
return _DialogTutorialContentBuilder(
content: this,
direction: AxisDirection.up,
text: 'Dialog Up',
);
}
}
class _CounterTutorialContent extends AnimatedTutorialContent {
_CounterTutorialContent(this._counterKey, this._onNextPage)
: super(identifier: _TutorialIdentifier.counter);
final GlobalKey _counterKey;
final VoidCallback _onNextPage;
[@override](/user/override)
Future<void> start() async {
await Scrollable.ensureVisible(
_counterKey.currentContext!,
duration: const Duration(milliseconds: 300),
);
}
[@override](/user/override)
Widget buildContent(BuildContext context) {
final Rect rect =
_counterKey.boxPosition!.rect.withPadding(const EdgeInsets.all(4));
return SpotlightStage(
rect: rect,
borderRadius: const BorderRadius.all(Radius.circular(4)),
children: [
AlignRect(
rect: rect,
alignment: const Alignment(0.0, 2.0),
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: ElevatedButton(
key: const ValueKey<_TutorialIdentifier>(
_TutorialIdentifier.title,
),
onPressed: () => TutorialStage.of(context).pause(),
child: const Text('Counter'),
),
),
),
],
);
}
[@override](/user/override)
void didFinish() {
_onNextPage();
super.didFinish();
}
}
class _TitleTutorialContent extends AnimatedTutorialContent {
_TitleTutorialContent(this._titleKey)
: super(identifier: _TutorialIdentifier.title);
final GlobalKey _titleKey;
[@override](/user/override)
Widget buildContent(BuildContext context) {
final Rect rect =
_titleKey.boxPosition!.rect.withPadding(const EdgeInsets.all(6));
return SpotlightStage(
rect: rect,
borderRadius: const BorderRadius.all(Radius.circular(4)),
children: [
AlignRect(
rect: rect,
alignment: const Alignment(0.0, 2.25),
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: ElevatedButton(
key: const ValueKey<_TutorialIdentifier>(
_TutorialIdentifier.title),
style: ElevatedButton.styleFrom(backgroundColor: Colors.amber),
onPressed: () => TutorialStage.of(context).next(),
child: const Text('Title'),
),
),
),
],
);
}
}
class _DialogTutorialContentBuilder extends FinishableTutorialWidget {
const _DialogTutorialContentBuilder({
required this.content,
required this.direction,
required this.text,
});
[@override](/user/override)
final FinishableTutorialContent content;
final AxisDirection direction;
final String text;
[@override](/user/override)
_DialogTutorialContentBuilderState createState() =>
_DialogTutorialContentBuilderState();
}
class _DialogTutorialContentBuilderState
extends FinishableTutorialWidgetState<_DialogTutorialContentBuilder> {
final TheTooltipKey _tooltipKey = TheTooltipKey();
[@override](/user/override)
void initState() {
super.initState();
_showTooltip();
}
[@override](/user/override)
void didUpdateContent(covariant FinishableTutorialContent oldContent) {
super.didUpdateContent(oldContent);
_showTooltip();
}
[@override](/user/override)
Widget build(BuildContext context) {
return TheTooltip(
key: _tooltipKey,
direction: widget.direction,
content: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Bacon ipsum dolor amet kevin turducken brisket pastrami, '
'salami ribeye spare ribs tri-tip sirloin shoulder venison '
'shank burgdoggen chicken pork belly. Short loin filet mignon '
'shoulder rump beef ribs meatball kevin.',
),
),
child: Dialog(
insetPadding: const EdgeInsets.symmetric(
horizontal: 40.0,
vertical: 12.0,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.text),
ElevatedButton(
onPressed: _next,
child: const Text('Next'),
),
],
),
),
),
);
}
void _showTooltip() {
SchedulerBinding.instance.addPostFrameCallback((_) {
_tooltipKey.currentState?.showTooltip();
});
}
void _next() {
TutorialStage.of(context).next();
}
[@override](/user/override)
Future<void> finish() async {
await _tooltipKey.currentState?.hideTooltip();
}
}
class NextPage extends StatefulWidget {
const NextPage({super.key});
[@override](/user/override)
State<NextPage> createState() => _NextPageState();
}
class _NextPageState extends State<NextPage> {
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
child: const Text('Next Tutorial on Previous Page'),
onPressed: () => Navigator.of(context).pop(),
),
),
);
}
}
更多关于Flutter引导教程插件tutorial_stage的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
1 回复