Flutter提示框插件flutter_callouts的使用
Flutter提示框插件flutter_callouts的使用
目录
动机
我们希望创建一个单一的包,能够在多种场景下与用户进行交流。
- 弹出指向目标部件的提示框
- 弹出没有目标的提示框,并可以在屏幕上定位
- 弹出不同位置的Toast(使用对齐属性)
特性
Callout API 使你能够通过显示 callout 小部件来指出目标部件。
callout 的出现方式和与用户的交互方式高度可配置。
简单的API
- 目标部件必须有一个
GlobalKey
。 - callout 必须提供一个字符串 cId。
- callout 需要你提供内容部件。
- 与你的UI解耦
- 一个 callout 或 toast 是在 Flutter Overlay 中显示的,因此不会干扰你的UI。
极度可配置
callout 的每一个展示方面都可以配置,包括样式、指针样式、拖动性、可调整大小性、动画、屏幕上的持续时间等…
callout 和指针样式
- 可以配置 callout 的颜色、形状、装饰、边框。
- 可以配置 callout 如何指向目标,例如带有箭头的线或气泡形状,以及它应该与目标分离的距离。
动画
- 出现方式可以动画化,指向目标的指针也可以动画化。
用户可拖动
- callout 可以选择是否可以拖动。你可以指定 callout 的一部分作为拖动句柄。
用户可调整大小
- callout 可以选择被四个角和四条边的调整大小部件包围,即用户可以通过拖动角或边来调整 callout 的大小。
可点击的屏障
- callout 可以有一个可选的可点击屏障(当不透明度大于0时)。(点击 callout 外部关闭它,否则可以配置关闭按钮)
关闭按钮
- 关闭按钮是可选的。其回调和外观都是可配置的。
- callout 还可以通过 API 隐藏/显示。
“知道了”按钮
- callout 可以配置为显示一个“知道了”按钮。
- 点击会记录在浏览器或应用程序的本地存储中(使用 callout 的 id)。
滚动感知
- 如果将您的 ScrollController 传递给 API,则即使发生滚动或窗口重新调整大小,callout 仍可以继续指向其目标。
解耦、非侵入式API
- 任何部件都可以是一个目标:只需给它一个 GlobalKey。
- 不需要在您的 UI 中插入包装部件。
- 每个 callout 在自己的 Overlay 中显示。
有用的回调
- 提供了每个可能的回调,以便您的应用可以对 callout 的活动作出反应:
ValueNotifier<int>? movedOrResizedNotifier; // 每次 callout overlay 移动或调整大小时都会增加
Function? onGotitPressedF;
VoidCallback? CalloutBarrier.onTapped;
VoidCallback? onCloseButtonPressF;
ValueChanged<Offset>? onDragF;
VoidCallback? onDragStartedF;
ValueChanged<Offset>? onDragEndedF;
ValueChanged<Size>? onResizeF;
VoidCallback? onDismissedF;
VoidCallback? onHiddenF;
VoidCallback? onAcceptedF;
快速开始
- 安装或更新
flutter_callouts
:
flutter pub add flutter_callouts
使用
在简单的示例演示中,一个 callout 和 toast 在 initState()
方法中创建。
import 'package:flutter/material.dart';
import 'package:flutter_callouts/flutter_callouts.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return const MaterialApp(
title: 'flutter_callouts demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
[@override](/user/override)
State<MyHomePage> createState() => _MyHomePageState();
}
/// 因为 callouts 是动画化的,所以添加混入
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
int _counter = 0;
late GlobalKey fabGK;
ScrollController controller = ScrollController();
/// CalloutConfig 对象是配置 callout 及其指针的地方
/// 所有参数都显示出来了,许多参数在这个示例 callout 中被注释掉了
CalloutConfig basicCalloutConfig(ScrollController controller) =>
CalloutConfig(
cId: 'basic',
// -- 初始位置和动画 ---------------------------------
initialTargetAlignment: Alignment.topLeft,
initialCalloutAlignment: Alignment.bottomRight,
// initialCalloutPos:
finalSeparation: 100,
// fromDelta: 0.0,
// toDelta : 0.0,
// initialAnimatedPositionDurationMs:
// -- 可选屏障(当不透明度 > 0 时) ----------------------
// barrier: CalloutBarrier(
// opacity: .5,
// onTappedF: () {
// Callout.dismiss("basic");
// },
// ),
// -- callout 外观 ----------------------------------------
// suppliedCalloutW: 280, // 如果未提供,则测量 callout 内容部件
// suppliedCalloutH: 200, // 如果未提供,则测量 callout 内容部件
// borderRadius: 12,
borderThickness: 3,
fillColor: Colors.yellow[700],
// elevation: 10,
// frameTarget: true,
// -- 可选关闭按钮和“知道了”按钮 -------------------
// showGotitButton: true,
// showCloseButton: true,
// closeButtonColor:
// closeButtonPos:
// gotitAxis:
// -- 指针 -------------------------------------------------
// arrowColor: Colors.green,
// arrowType: ArrowType.THIN,
animate: true,
// lineLabel: Text('line label'),
// fromDelta: -20,
// toDelta: -20,
// lengthDeltaPc: ,
// contentTranslateX: ,
// contentTranslateY:
// targetTranslateX:
// targetTranslateY:
// scaleTarget:
// -- 调整大小 -------------------------------------------------
// resizeableH: true,
// resizeableV: true,
// -- 拖动 -------------------------------------------------
// draggable: false,
// draggableColor: Colors.green,
// dragHandleHeight: ,
vScrollController: controller,
vsync: this,
);
void _incrementCounter() {
setState(() {
_counter++;
});
}
[@override](/user/override)
void initState() {
super.initState();
/// 目标的 key
fabGK = GlobalKey();
fca.afterNextBuildDo(() {
Callout.showOverlay(
calloutConfig: basicCalloutConfig(controller),
calloutContentF: (context) => const Padding(
padding: EdgeInsets.all(8.0),
child: Text('Tap this floating action button to increment the counter.'),
),
targetGkF: () => fabGK,
);
fca.afterMsDelayDo(
800,
() => _showToast(Alignment.topCenter),
);
});
}
void _showToast(Alignment gravity,
{int showForMs = 0, VoidCallback? onDismissedF}) =>
Callout.showToast(
removeAfterMs: showForMs,
calloutConfig: CalloutConfig(
cId: 'initstate-toast',
gravity: gravity,
initialCalloutW: 500,
initialCalloutH: 90,
fillColor: Colors.black26,
showCloseButton: true,
borderThickness: 5,
borderRadius: 16,
borderColor: Colors.yellow,
elevation: 10,
vScrollController: controller,
vsync: this,
onDismissedF: () => onDismissedF?.call(),
),
calloutContentF: (_) => Center(
child: Text(
'gravity: ${gravity.toString()}',
textScaler: const TextScaler.linear(2),
style: const TextStyle(color: Colors.white),
),
),
);
[@override](/user/override)
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (SizeChangedLayoutNotification notification) {
// Callout.dismissAll(exceptFeatures: []);
FlutterCallouts.instance.afterMsDelayDo(300, () {
Callout.refreshAll();
});
return true;
},
child: SizeChangedLayoutNotifier(
child: Scaffold(
body: Center(
child: SingleChildScrollView(
controller: controller,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
height: screenSize.height - 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the + button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
Container(
width: double.infinity,
height: 100,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
key: fabGK,
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
),
),
Container(
height: 1000,
width: double.infinity,
color: Colors.blue[50],
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text('Scroll to see that the yellow callout is Scroll-aware.\n'
'Resize the window to see the pointer refreshing.'),
),
),
],
),
),
),
),
),
);
}
}
示例代码
import 'package:flutter/material.dart';
import 'package:flutter_callouts/flutter_callouts.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await fca.initLocalStorage();
runApp(const FlutterCalloutsSimpleDemo());
}
class FlutterCalloutsSimpleDemo extends StatelessWidget {
const FlutterCalloutsSimpleDemo({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return const MaterialApp(
title: 'flutter_callouts demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
[@override](/user/override)
State<MyHomePage> createState() => _MyHomePageState();
}
/// 因为 callouts 是动画化的,所以添加混入
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late int _counter;
late GlobalKey fabGK;
NamedScrollController namedSC = NamedScrollController('main', Axis.vertical);
/// CalloutConfig 对象是配置 callout 及其指针的地方
/// 所有参数都显示出来了,许多参数在这个示例 callout 中被注释掉了
CalloutConfig
basicCalloutConfig(ScrollController controller) =>
CalloutConfig(
cId: 'basic',
// -- 初始位置和动画 ---------------------------------
initialTargetAlignment: Alignment.topLeft,
initialCalloutAlignment: Alignment.bottomRight,
// initialCalloutPos:
finalSeparation: 100,
// fromDelta: 0.0,
// toDelta : 0.0,
// initialAnimatedPositionDurationMs:
// -- 可选屏障(当不透明度 > 0 时) ----------------------
// barrier: CalloutBarrier(
// opacity: .5,
// onTappedF: () {
// Callout.dismiss("basic");
// },
// ),
// -- callout 外观 ----------------------------------------
// suppliedCalloutW: 280, // 如果未提供,则测量 callout 内容部件
// suppliedCalloutH: 200, // 如果未提供,则测量 callout 内容部件
// borderRadius: 12,
borderThickness: 3,
fillColor: Colors.yellow[700],
// elevation: 10,
// frameTarget: true,
// -- 可选关闭按钮和“知道了”按钮 -------------------
// showGotitButton: true,
// showCloseButton: true,
// closeButtonColor:
// closeButtonPos:
// gotitAxis:
// -- 指针 -------------------------------------------------
// arrowColor: Colors.green,
arrowType: ArrowType.POINTY,
animate: true,
// lineLabel: Text('line label'),
// fromDelta: -20,
// toDelta: -20,
// lengthDeltaPc: ,
// contentTranslateX: ,
// contentTranslateY:
// targetTranslateX:
// targetTranslateY:
// scaleTarget:
// -- 调整大小 -------------------------------------------------
// resizeableH: true,
// resizeableV: true,
// -- 拖动 -------------------------------------------------
// draggable: false,
// draggableColor: Colors.green,
// dragHandleHeight: ,
scrollControllerName: namedSC.name,
followScroll: false,
);
void _incrementCounter() {
setState(() {
_counter++;
fca.spwc?.setInt("counter", _counter);
});
}
[@override](/user/override)
void initState() {
super.initState();
_counter = fca.spwc?.getInt("counter") ?? 0;
/// 目标的 key
fabGK = GlobalKey();
// controller.listenToOffset();
fca.afterNextBuildDo(() {
fca.showOverlay(
calloutConfig: basicCalloutConfig(namedSC),
calloutContent: const Padding(
padding: EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Tap this floating action button to increment the counter.'),
],
),
),
targetGkF: () => fabGK,
);
fca.afterMsDelayDo(
800,
() => _showToast(Alignment.topCenter),
);
});
}
[@override](/user/override)
void dispose() {
namedSC.dispose();
super.dispose();
}
[@override](/user/override)
void didChangeDependencies() {
fca.initWithContext(context);
super.didChangeDependencies();
}
void _showToast(Alignment gravity,
{int showForMs = 0, VoidCallback? onDismissedF}) =>
fca.showToast(
removeAfterMs: showForMs,
calloutConfig: CalloutConfig(
cId: 'main-toast',
gravity: gravity,
initialCalloutW: 500,
initialCalloutH: 90,
fillColor: Colors.black26,
showCloseButton: true,
borderThickness: 5,
borderRadius: 16,
borderColor: Colors.yellow,
elevation: 10,
scrollControllerName: namedSC.name,
onDismissedF: () => onDismissedF?.call(),
// allowCalloutToScroll: false,
),
calloutContent: Center(
child: Text(
'gravity: ${gravity.toString()}',
textScaler: const TextScaler.linear(2),
style: const TextStyle(color: Colors.white),
),
),
);
[@override](/user/override)
Widget build(BuildContext context) =>
NotificationListener<SizeChangedLayoutNotification>(
onNotification: (SizeChangedLayoutNotification notification) {
// Callout.dismissAll(exceptFeatures: []);
fca.afterMsDelayDo(300, () {
fca.refreshAll();
});
return true;
},
child: SizeChangedLayoutNotifier(
child: Scaffold(
body: Center(
child: SingleChildScrollView(
controller: namedSC,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
height: MediaQuery.of(context).size.height - 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the + button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
SizedBox(
width: double.infinity,
height: 100,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
key: fabGK,
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
),
),
Container(
height: 1000,
width: double.infinity,
color: Colors.blue[50],
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Scroll to see that the yellow callout is Scroll-aware.\n'
'Resize the window to see the pointer refreshing.'),
),
),
],
),
),
),
),
),
);
}
更多关于Flutter提示框插件flutter_callouts的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter提示框插件flutter_callouts的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,下面是一个关于如何在Flutter项目中使用flutter_callouts
插件来显示提示框的示例代码。flutter_callouts
插件允许你在应用中的特定位置显示丰富的提示框,非常适合用于引导用户或显示信息。
首先,你需要在pubspec.yaml
文件中添加flutter_callouts
依赖:
dependencies:
flutter:
sdk: flutter
flutter_callouts: ^x.y.z # 请替换为最新版本号
然后运行flutter pub get
来安装依赖。
接下来是一个简单的示例,展示如何使用flutter_callouts
插件:
import 'package:flutter/material.dart';
import 'package:flutter_callouts/flutter_callouts.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Callouts Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
CalloutController? _calloutController;
@override
void initState() {
super.initState();
// 初始化CalloutController
_calloutController = CalloutController();
// 显示提示框(在3秒后)
Future.delayed(Duration(seconds: 3), () {
_showCallout();
});
}
@override
void dispose() {
// 释放资源
_calloutController?.dispose();
super.dispose();
}
void _showCallout() {
_calloutController?.show(
context,
target: _getTargetWidgetKey(context),
content: Container(
padding: EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 2), // changes position of shadow
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'这是一个提示框',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8.0),
Text('你可以在这里显示更多信息。'),
],
),
),
arrow: CalloutArrow(
color: Colors.white,
borderColor: Colors.grey.shade300,
borderWidth: 2.0,
),
arrowPosition: CalloutArrowPosition.top,
margin: EdgeInsets.only(top: 8.0),
constraints: BoxConstraints(maxWidth: 200),
backgroundColor: Colors.transparent,
onDismiss: () {
print('提示框已关闭');
},
);
}
GlobalKey _getTargetWidgetKey(BuildContext context) {
// 这里返回一个目标小部件的GlobalKey,提示框将指向这个小部件
final targetKey = GlobalKey();
WidgetsBinding.instance!.addPostFrameCallback((_) {
// 在下一帧显示提示框(确保目标小部件已渲染)
_showCallout(); // 注意:这里仅为演示目的,实际应在initState中调用一次_showCallout
});
return targetKey; // 注意:在实际使用中,你应该将这个key分配给你希望指向的小部件
}
@override
Widget build(BuildContext context) {
// 注意:为了简化示例,这里并没有实际使用_getTargetWidgetKey返回的key。
// 在实际使用中,你应该将_getTargetWidgetKey(context)分配给某个小部件的key属性。
return Scaffold(
appBar: AppBar(
title: Text('Flutter Callouts Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {},
child: Text('点击我(目标小部件应在这里设置key)'),
),
],
),
),
);
}
}
注意:
- 上述代码中的
_getTargetWidgetKey
方法仅用于演示目的,并没有实际将返回的GlobalKey
分配给任何小部件。在实际使用中,你需要将GlobalKey
分配给你希望提示框指向的具体小部件。 Future.delayed(Duration(seconds: 3), () { _showCallout(); });
用于在3秒后显示提示框,你可以根据需要调整或移除这个延迟。_showCallout
方法中的target
参数接收一个GlobalKey
,该GlobalKey
应绑定到你希望提示框指向的小部件上。
这个示例展示了如何使用flutter_callouts
插件来显示一个简单的提示框,你可以根据需要进一步自定义提示框的内容和样式。