Flutter功能引导插件feature_discovery_fork的使用

发布于 1周前 作者 h691938207 来自 Flutter

Flutter功能引导插件feature_discovery_fork的使用

1. 简介

feature_discovery_fork 是一个遵循 Material Design 指南的 Flutter 插件,用于在应用程序中实现功能引导。通过这个插件,你可以为任何 UI 元素(即任何 Widget)添加上下文信息,帮助用户更好地理解应用程序的功能。

2. 安装

要使用此插件,请按照以下步骤进行安装:

  1. pubspec.yaml 文件中添加依赖:

    dependencies:
      feature_discovery_fork: ^latest_version
    
  2. 运行 flutter pub get 以安装依赖。

3. 使用方法

3.1 FeatureDiscovery

为了使用 feature_discovery_fork 提供的全局函数,你需要将你的 widget 树包裹在一个 FeatureDiscovery widget 中。最简单的方法是将其包裹在 MaterialApp 外面:

const FeatureDiscovery(
  child: MaterialApp(
    // 你的应用配置
  ),
)
3.2 DescribedFeatureOverlay

对于每个你想要描述的 UI 元素(Widget),你需要添加一个 DescribedFeatureOverlay。这个 widget 包含了所有关于覆盖层的参数,并且将你要描述的 Widget 作为其 child

DescribedFeatureOverlay(
  featureId: 'add_item_feature_id', // 唯一标识符
  tapTarget: const Icon(Icons.add), // 要显示为点击目标的 widget
  title: Text('Add item'),
  description: Text('Tap the plus icon to add an item to your list.'),
  backgroundColor: Theme.of(context).primaryColor,
  targetColor: Colors.white,
  textColor: Colors.white,
  child: IconButton( // 实际参与 UI 的 widget
    icon: const Icon(Icons.add),
    onPressed: addItem,
  ),
);
3.3 FeatureDiscovery.discoverFeatures

当你想要展示功能引导时,可以调用 FeatureDiscovery.discoverFeatures,并传入你想要展示的功能 ID。这些功能将按顺序依次展示,除非用户关闭了它们。

FeatureDiscovery.discoverFeatures(
  context,
  const <String>{ // 你想要展示的功能 ID
    'add_item_feature_id',
  },
);

如果你希望在页面加载后立即展示功能引导,可以在 StatefulWidgetinitState 方法中使用 SchedulerBinding.addPostFrameCallback

[@override](/user/override)
void initState() {
  SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
    FeatureDiscovery.discoverFeatures(
      context,
      const <String>{ // 你想要展示的功能 ID
        'add_item_feature_id',
      },
    ); 
  });
  super.initState();
}
3.4 FeatureDiscovery.clearPreferences

如果你想清除功能引导的完成标志,可以使用 FeatureDiscovery.clearPreferences

FeatureDiscovery.clearPreferences(context, <String>{ 'add_item_feature_id', });
3.5 FeatureDiscovery.hasPreviouslyCompleted

如果你想检查某个功能引导是否已经展示过,可以使用 FeatureDiscovery.hasPreviouslyCompleted

FeatureDiscovery.hasPreviouslyCompleted(context, 'desired_feature_id');
3.6 EnsureVisible

你可以使用 EnsureVisible widget 来自动滚动到位于可滚动视口中的 widget,当它们在功能引导中被描述时:

GlobalKey<EnsureVisibleState> ensureVisibleGlobalKey = GlobalKey<EnsureVisibleState>();

DescribedFeatureOverlay(
  featureId: 'list_item_feature_id',
  tapTarget: const Icon(Icons.cake),
  onOpen: () async {
    WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
      ensureVisibleGlobalKey.currentState.ensureVisible();
      return true;
    });
  },
  title: Text('Cake'),
  description: Text('This is your reward for making it this far.'),
  child: EnsureVisible(
    key: ensureVisibleGlobalKey,
    child: const Icon(Icons.cake),
  ),
)

4. 示例代码

以下是一个完整的示例代码,展示了如何使用 feature_discovery_fork 插件来实现功能引导:

import 'package:feature_discovery_fork/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

const String feature1 = 'feature1',
    feature2 = 'feature2',
    feature3 = 'feature3',
    feature4 = 'feature4',
    feature5 = 'feature5',
    feature6 = 'feature6',
    feature7 = 'feature7';

void main() {
  // 你可以增加 timeDilation 的值以更慢地查看动画效果
  timeDilation = 1.0;

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) => MaterialApp(
        title: 'Feature Discovery',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        // 必须:这个 widget 作为继承 widget 工作
        home: const FeatureDiscovery.withProvider(
          persistenceProvider: NoPersistenceProvider(),
          child: MyHomePage(title: 'Flutter Feature Discovery'),
        ),
      );
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, this.title}) : super(key: key);

  final String? title;

  [@override](/user/override)
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  [@override](/user/override)
  Widget build(BuildContext context) {
    final action = () async {
      print('IconButton of $feature7 tapped.');
      return true;
    };
    const icon1 = Icon(Icons.drive_eta);
    const icon2 = Icon(Icons.menu);
    const icon3 = Icon(Icons.search);
    const icon4 = Icon(Icons.add);

    var feature1OverflowMode = OverflowMode.clipContent;
    var feature1EnablePulsingAnimation = false;

    var feature3ItemCount = 15;

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(80),
          child: Column(
            children: <Widget>[
              DescribedFeatureOverlay(
                featureId: feature7,
                tapTarget: icon1,
                backgroundColor: Colors.blue,
                contentLocation: ContentLocation.below,
                title: const Text('Find the fastest route'),
                description: const Text(
                    'Get car, walking, cycling, or public transit directions to this place'),
                onComplete: action,
                onOpen: () async {
                  print('The $feature7 overlay is about to be displayed.');
                  return true;
                },
                child: IconButton(
                  icon: icon1,
                  onPressed: action,
                ),
              ),
            ],
          ),
        ),
        leading: StatefulBuilder(
          builder: (BuildContext context, void Function(void Function()) setState) =>
              DescribedFeatureOverlay(
            featureId: feature1,
            tapTarget: icon2,
            backgroundColor: Colors.teal,
            title: const Text(
                'This is overly long on purpose to test OverflowMode.clip!'),
            overflowMode: feature1OverflowMode,
            enablePulsingAnimation: feature1EnablePulsingAnimation,
            description: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text(
                    'Also, notice how the pulsing animation is not playing because it is deactivated for this feature.'),
                TextButton(
                  onPressed: () => setState(() {
                    feature1EnablePulsingAnimation =
                        !feature1EnablePulsingAnimation;
                  }),
                  child: Text('Toggle enablePulsingAnimation',
                      style: Theme.of(context)
                          .textTheme
                          .button!
                          .copyWith(color: Colors.white)),
                ),
                const Text(
                    'Ignore the items below or tap the button to toggle between OverflowMode.clip and OverflowMode.doNothing!'),
                TextButton(
                  onPressed: () => setState(() {
                    feature1OverflowMode =
                        feature1OverflowMode == OverflowMode.clipContent
                            ? OverflowMode.ignore
                            : OverflowMode.clipContent;
                  }),
                  child: Text('Toggle overflowMode',
                      style: Theme.of(context)
                          .textTheme
                          .button!
                          .copyWith(color: Colors.white)),
                ),
                for (int n = 42; n > 0; n--)
                  const Text('Testing clipping (ignore or toggle)',
                      style: TextStyle(backgroundColor: Colors.black)),
              ],
            ),
            child: IconButton(
              icon: icon2,
              onPressed: () {},
            ),
          ),
        ),
        actions: <Widget>[
          DescribedFeatureOverlay(
            featureId: feature2,
            tapTarget: icon3,
            backgroundColor: Colors.green,
            title: const Text('Search your compounds'),
            description: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text(
                    'Tap the magnifying glass to quickly scan your compounds'),
                TextButton(
                  onPressed: () async =>
                      FeatureDiscovery.completeCurrentStep(context),
                  child: Text(
                    'Understood',
                    style: Theme.of(context)
                        .textTheme
                        .button!
                        .copyWith(color: Colors.white),
                  ),
                ),
                TextButton(
                  onPressed: () => FeatureDiscovery.dismissAll(context),
                  child: Text(
                    'Dismiss',
                    style: Theme.of(context)
                        .textTheme
                        .button!
                        .copyWith(color: Colors.white),
                  ),
                ),
              ],
            ),
            child: IconButton(
              icon: icon3,
              onPressed: () {},
            ),
          ),
        ],
      ),
      body: const Content(),
      floatingActionButton: StatefulBuilder(
        builder: (BuildContext context, void Function(void Function()) setState) =>
            DescribedFeatureOverlay(
          featureId: feature3,
          tapTarget: icon4,
          backgroundColor: Colors.green,
          overflowMode: OverflowMode.extendBackground,
          title: const Text('FAB feature'),
          description: Column(children: <Widget>[
            const Text(
                'This is overly long to test OverflowMode.extendBackground. The green circle should be large enough to cover all of the text.'),
            TextButton(
              onPressed: () => setState(() {
                feature3ItemCount++;
              }),
              child: Text('Add another item',
                  style: Theme.of(context)
                      .textTheme
                      .button!
                      .copyWith(color: Colors.white)),
            ),
            for (int n = feature3ItemCount; n > 0; n--)
              const Text('Testing OverflowMode.extendBackground'),
          ]),
          child: FloatingActionButton(
            onPressed: () {},
            tooltip: 'Increment',
            child: icon4,
          ),
        ),
      ),
    );
  }
}

class Content extends StatefulWidget {
  const Content({Key? key}) : super(key: key);

  [@override](/user/override)
  _ContentState createState() => _ContentState();
}

class _ContentState extends State<Content> {
  GlobalKey<EnsureVisibleState>? ensureKey;
  GlobalKey<EnsureVisibleState>? ensureKey2;

  [@override](/user/override)
  void initState() {
    ensureKey = GlobalKey<EnsureVisibleState>();
    ensureKey2 = GlobalKey<EnsureVisibleState>();

    WidgetsBinding.instance!.addPostFrameCallback((_) {
      FeatureDiscovery.discoverFeatures(
        context,
        const <String>{
          feature7,
          feature1,
          feature2,
          feature3,
          feature4,
          feature6,
          feature5
        },
      );
    });
    super.initState();
  }

  [@override](/user/override)
  void dispose() {
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    var feature6ItemCount = 0;

    return Stack(
      children: <Widget>[
        SingleChildScrollView(
          child: Column(
            children: <Widget>[
              Container(
                height: 200,
                width: double.infinity,
                child: const Text(
                    'Imagine there would be a beautiful picture here.'),
              ),
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(16.0),
                color: Colors.blue,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    const Padding(
                      padding: EdgeInsets.only(bottom: 8.0),
                      child: Text(
                        'DISH REPUBLIC',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 24.0,
                        ),
                      ),
                    ),
                    const Text(
                      'Eat',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 16.0,
                      ),
                    ),
                  ],
                ),
              ),
              Container(
                height: 600.0,
                color: Colors.orangeAccent,
              ),
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(16.0),
                child: DescribedFeatureOverlay(
                  featureId: feature5,
                  tapTarget: const Icon(Icons.drive_eta),
                  backgroundColor: Colors.green,
                  onComplete: () async {
                    print('Tapped tap target of $feature5.');
                    return true;
                  },
                  onOpen: () async {
                    WidgetsBinding.instance!.addPostFrameCallback((_) {
                      ensureKey!.currentState!.ensureVisible(
                        preciseAlignment: 0.5,
                        duration: const Duration(milliseconds: 400),
                      );
                    });
                    return true;
                  },
                  title: const Text('Discover Features'),
                  description: const Text(
                      'Find all available features in this application with this button.'),
                  contentLocation: ContentLocation.below,
                  child: EnsureVisible(
                    key: ensureKey,
                    child: ElevatedButton(
                      onPressed: () {
                        FeatureDiscovery.discoverFeatures(
                          context,
                          const <String>{
                            feature1,
                            feature2,
                            feature3,
                            feature4,
                            feature6,
                            feature5
                          },
                        );
                      },
                      child: const Text('Start Feature Discovery'),
                    ),
                  ),
                ),
              ),
              Container(
                height: 1500,
                color: Colors.blueAccent,
              ),
              StatefulBuilder(
                builder: (BuildContext context,
                        void Function(void Function()) setState) =>
                    DescribedFeatureOverlay(
                  barrierDismissible: false,
                  featureId: feature6,
                  tapTarget: const Icon(Icons.drive_eta),
                  backgroundColor: Colors.green,
                  onComplete: () async {
                    print('Tapped tap target of $feature6.');
                    return true;
                  },
                  onOpen: () async {
                    WidgetsBinding.instance!.addPostFrameCallback((_) {
                      ensureKey2!.currentState!.ensureVisible(
                          duration: const Duration(milliseconds: 600));
                    });
                    return true;
                  },
                  description: Column(children: <Widget>[
                    const Text(
                        'You can test OverflowMode.wrapBackground here.'),
                    TextButton(
                      onPressed: () => setState(() {
                        feature6ItemCount++;
                      }),
                      child: Text('Add item',
                          style: Theme.of(context)
                              .textTheme
                              .button!
                              .copyWith(color: Colors.white)),
                    ),
                    for (int n = feature6ItemCount; n > 0; n--)
                      const Text('Testing OverflowMode.wrapBackground'),
                  ]),
                  overflowMode: OverflowMode.wrapBackground,
                  child: EnsureVisible(
                    key: ensureKey2,
                    child: const Text(
                      'Custom text',
                    ),
                  ),
                ),
              ),
              Container(
                height: 300,
                color: Colors.red,
              ),
            ],
          ),
        ),
        Positioned(
          top: 200.0,
          right: 0.0,
          child: FractionalTranslation(
            translation: const Offset(-.5, -0.5),
            child: DescribedFeatureOverlay(
              featureId: feature4,
              tapTarget: const Icon(Icons.drive_eta),
              backgroundColor: Colors.green,
              onOpen: () async {
                print('Tapped tap target of $feature4.');
                return true;
              },
              title: const Text('Find the fastest route'),
              description: const Text(
                  'Get car, walking, cycling or public transit directions to this place.'),
              child: FloatingActionButton(
                backgroundColor: Colors.white,
                foregroundColor: Colors.blue,
                onPressed: () {
                  print('Floating action button tapped.');
                },
                child: const Icon(Icons.drive_eta),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

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

1 回复

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


当然,以下是如何在Flutter项目中使用feature_discovery_fork插件来实现功能引导的一个示例。这个插件允许你在应用中突出显示新功能或关键功能,引导用户了解如何使用它们。

首先,确保你的Flutter项目已经添加了feature_discovery_fork依赖。在pubspec.yaml文件中添加以下依赖项:

dependencies:
  flutter:
    sdk: flutter
  feature_discovery: ^0.14.0 # 请检查最新版本号

然后运行flutter pub get来安装依赖。

接下来,在你的Dart代码中,你可以按照以下步骤使用feature_discovery_fork插件。

示例代码

  1. 导入必要的包
import 'package:flutter/material.dart';
import 'package:feature_discovery/feature_discovery.dart';
  1. 定义引导的目标Widget

假设我们有一个按钮,我们想引导用户点击它。

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey _targetKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Feature Discovery Example'),
        ),
        body: Center(
          child: FeatureDiscoveryController(
            // 创建FeatureDiscoveryController实例
            child: Builder(
              builder: (context) {
                final FeatureDiscoveryController controller =
                    FeatureDiscoveryController.of(context);

                return Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    ElevatedButton(
                      key: _targetKey, // 为目标Widget设置GlobalKey
                      onPressed: () {
                        // 按钮点击事件
                      },
                      child: Text('Feature to Discover'),
                    ),
                    SizedBox(height: 20),
                    ElevatedButton(
                      onPressed: () {
                        // 触发功能引导
                        controller.startDiscoveryForKey(_targetKey);
                      },
                      child: Text('Show Feature Discovery'),
                    ),
                  ],
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}
  1. 运行应用

现在,当你运行应用并点击“Show Feature Discovery”按钮时,应用会引导用户注意到“Feature to Discover”按钮。

自定义引导样式

你还可以自定义引导的背景、文本、形状等。以下是一个简单的自定义示例:

FeatureDiscoveryOptions options = FeatureDiscoveryOptions(
  backgroundColor: Colors.blue.withOpacity(0.8),
  textStyle: TextStyle(color: Colors.white, fontSize: 18),
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
);

// 在FeatureDiscoveryController中使用自定义选项
FeatureDiscoveryController(
  featureOptions: options,
  // 其他代码保持不变
)

将上述FeatureDiscoveryOptions实例传递给FeatureDiscoveryControllerfeatureOptions属性即可。

注意事项

  • 确保目标Widget(在这个例子中是按钮)在引导触发时是可见的。
  • 你可以在用户完成引导后禁用引导,或者根据用户的某些行为(如首次启动应用)来触发引导。

这样,你就可以在你的Flutter应用中使用feature_discovery_fork插件来实现功能引导了。希望这个示例对你有帮助!

回到顶部