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 回复

更多关于Flutter引导教程插件tutorial_stage的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


tutorial_stage 是一个用于在 Flutter 应用中创建引导教程的插件。它可以帮助你逐步引导用户了解应用的功能和界面。以下是如何使用 tutorial_stage 插件的基本步骤:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 tutorial_stage 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  tutorial_stage: ^1.0.0 # 请使用最新版本

然后运行 flutter pub get 来获取依赖。

2. 导入插件

在你的 Dart 文件中导入 tutorial_stage

import 'package:tutorial_stage/tutorial_stage.dart';

3. 创建引导步骤

你可以通过创建 TutorialStage 对象来定义引导步骤。每个步骤可以包含一个目标组件、标题、描述等信息。

List<TutorialStage> tutorialStages = [
  TutorialStage(
    targetKey: GlobalKey(), // 目标组件的 GlobalKey
    title: "Welcome",
    description: "This is the first step of the tutorial.",
  ),
  TutorialStage(
    targetKey: GlobalKey(),
    title: "Next Step",
    description: "This is the second step of the tutorial.",
  ),
];

4. 创建 TutorialController

TutorialController 用于控制引导流程的开始、结束和步骤切换。

TutorialController tutorialController = TutorialController();

5. 在 UI 中使用 TutorialWidget

你需要将 TutorialWidget 包裹在你的应用 UI 中,并将 TutorialControllertutorialStages 传递给它。

[@override](/user/override)
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Tutorial Example"),
    ),
    body: TutorialWidget(
      controller: tutorialController,
      stages: tutorialStages,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              key: tutorialStages[0].targetKey, // 绑定第一个步骤的目标组件
              onPressed: () {},
              child: Text("Button 1"),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              key: tutorialStages[1].targetKey, // 绑定第二个步骤的目标组件
              onPressed: () {},
              child: Text("Button 2"),
            ),
          ],
        ),
      ),
    ),
  );
}

6. 启动引导教程

你可以通过调用 TutorialControllerstart 方法来启动引导教程。

tutorialController.start();

7. 处理引导结束

你可以通过监听 TutorialController 的事件来处理引导结束或其他自定义逻辑。

tutorialController.addListener(() {
  if (tutorialController.isCompleted) {
    print("Tutorial completed!");
  }
});

8. 自定义引导样式

你可以通过 TutorialWidget 的参数来自定义引导样式,例如背景颜色、文本样式等。

TutorialWidget(
  controller: tutorialController,
  stages: tutorialStages,
  backgroundColor: Colors.black54,
  titleStyle: TextStyle(color: Colors.white, fontSize: 20),
  descriptionStyle: TextStyle(color: Colors.white, fontSize: 16),
  child: // Your UI
);

9. 完整示例

以下是一个完整的示例代码:

import 'package:flutter/material.dart';
import 'package:tutorial_stage/tutorial_stage.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TutorialExample(),
    );
  }
}

class TutorialExample extends StatefulWidget {
  [@override](/user/override)
  _TutorialExampleState createState() => _TutorialExampleState();
}

class _TutorialExampleState extends State<TutorialExample> {
  TutorialController tutorialController = TutorialController();
  List<TutorialStage> tutorialStages = [
    TutorialStage(
      targetKey: GlobalKey(),
      title: "Welcome",
      description: "This is the first step of the tutorial.",
    ),
    TutorialStage(
      targetKey: GlobalKey(),
      title: "Next Step",
      description: "This is the second step of the tutorial.",
    ),
  ];

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      tutorialController.start();
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Tutorial Example"),
      ),
      body: TutorialWidget(
        controller: tutorialController,
        stages: tutorialStages,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                key: tutorialStages[0].targetKey,
                onPressed: () {},
                child: Text("Button 1"),
              ),
              SizedBox(height: 20),
              ElevatedButton(
                key: tutorialStages[1].targetKey,
                onPressed: () {},
                child: Text("Button 2"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
回到顶部