Flutter功能引导插件feature_discovery_fork的使用
Flutter功能引导插件feature_discovery_fork的使用
1. 简介
feature_discovery_fork
是一个遵循 Material Design 指南的 Flutter 插件,用于在应用程序中实现功能引导。通过这个插件,你可以为任何 UI 元素(即任何 Widget
)添加上下文信息,帮助用户更好地理解应用程序的功能。
2. 安装
要使用此插件,请按照以下步骤进行安装:
-
在
pubspec.yaml
文件中添加依赖:dependencies: feature_discovery_fork: ^latest_version
-
运行
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',
},
);
如果你希望在页面加载后立即展示功能引导,可以在 StatefulWidget
的 initState
方法中使用 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
更多关于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
插件。
示例代码
- 导入必要的包
import 'package:flutter/material.dart';
import 'package:feature_discovery/feature_discovery.dart';
- 定义引导的目标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'),
),
],
);
},
),
),
),
),
);
}
}
- 运行应用
现在,当你运行应用并点击“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
实例传递给FeatureDiscoveryController
的featureOptions
属性即可。
注意事项
- 确保目标Widget(在这个例子中是按钮)在引导触发时是可见的。
- 你可以在用户完成引导后禁用引导,或者根据用户的某些行为(如首次启动应用)来触发引导。
这样,你就可以在你的Flutter应用中使用feature_discovery_fork
插件来实现功能引导了。希望这个示例对你有帮助!