如何在颤动中制作不透明的教程屏幕?
How to make opaque tutorial screen in flutter?
我想制作一开始就显示给用户的教程屏幕。如下所示:
我的具体问题,如何使某些元素正常显示而其他元素不透明?
还有 the arrow
和文本,如何根据移动设备屏幕尺寸(移动响应能力)使它们 完美点 ?
您可以使用 this 库来帮助您实现您所需要的。它允许您标记要突出显示的视图以及如何突出显示它们。
用 Stack 小部件包装您当前的顶级小部件,让 Stack 的第一个子部件成为您当前的小部件。
在这个小部件下面添加一个黑色的容器,用不透明度包裹,如下所示:
return Stack(
children: <Widget>[
Scaffold( //first child of the stack - the current widget you have
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text("Foo"),
Text("Bar"),
],
),
)),
Opacity( //seconds child - Opaque layer
opacity: 0.7,
child: Container(
decoration: BoxDecoration(color: Colors.black),
),
)
],
);
然后您需要以 1x、2x、3x 分辨率创建描述和箭头的图像资产,并将它们按照此处所述的适当结构放置在您的资产文件夹中:https://flutter.dev/docs/development/ui/assets-and-images#declaring-resolution-aware-image-assets
然后您可以使用 Image.asset(...) 小部件加载您的图像(它们将以正确的分辨率加载),并将这些小部件放在另一个容器中,该容器也是堆栈,并将放置在子列表中的黑色容器下方(上例中的不透明度小部件)。
正如 RoyalGriffin 提到的,您可以使用 highlighter_coachmark 库,我也知道您遇到的错误,该错误是因为您使用的是 RangeSlider
class从 2 个不同的包中导入。你能在你的应用程序中尝试这个例子并检查它是否有效吗?
将 highlighter_coachmark
添加到您的 pubspec.yaml
文件
dependencies:
flutter:
sdk: flutter
highlighter_coachmark: ^0.0.3
运行 flutter packages get
示例:
import 'package:highlighter_coachmark/highlighter_coachmark.dart';
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
GlobalKey _fabKey = GlobalObjectKey("fab"); // used by FAB
GlobalKey _buttonKey = GlobalObjectKey("button"); // used by RaisedButton
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
key: _fabKey, // setting key
onPressed: null,
child: Icon(Icons.add),
),
body: Center(
child: RaisedButton(
key: _buttonKey, // setting key
onPressed: showFAB,
child: Text("RaisedButton"),
),
),
);
}
// we trigger this method on RaisedButton click
void showFAB() {
CoachMark coachMarkFAB = CoachMark();
RenderBox target = _fabKey.currentContext.findRenderObject();
// you can change the shape of the mark
Rect markRect = target.localToGlobal(Offset.zero) & target.size;
markRect = Rect.fromCircle(center: markRect.center, radius: markRect.longestSide * 0.6);
coachMarkFAB.show(
targetContext: _fabKey.currentContext,
markRect: markRect,
children: [
Center(
child: Text(
"This is called\nFloatingActionButton",
style: const TextStyle(
fontSize: 24.0,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
)
],
duration: null, // we don't want to dismiss this mark automatically so we are passing null
// when this mark is closed, after 1s we show mark on RaisedButton
onClose: () => Timer(Duration(seconds: 1), () => showButton()),
);
}
// this is triggered once first mark is dismissed
void showButton() {
CoachMark coachMarkTile = CoachMark();
RenderBox target = _buttonKey.currentContext.findRenderObject();
Rect markRect = target.localToGlobal(Offset.zero) & target.size;
markRect = markRect.inflate(5.0);
coachMarkTile.show(
targetContext: _fabKey.currentContext,
markRect: markRect,
markShape: BoxShape.rectangle,
children: [
Positioned(
top: markRect.bottom + 15.0,
right: 5.0,
child: Text(
"And this is a RaisedButton",
style: const TextStyle(
fontSize: 24.0,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
)
],
duration: Duration(seconds: 5), // this effect will only last for 5s
);
}
}
输出:
应该提到的是,面向 Material 的 feature_discovery 包不是采用不透明的方法,而是使用动画并集成到应用程序对象层次结构本身中,因此需要较少的自定义突出显示编程。一站式解决方案还支持多步高亮。
如果你不想依赖外部库,你可以自己做。其实并不难。
使用堆栈小部件,您可以将半透明覆盖层放在所有内容之上。现在,如何在强调底层 UI 元素的叠加层中“挖洞”?
这是一篇涵盖确切主题的文章:https://www.flutterclutter.dev/flutter/tutorials/how-to-cut-a-hole-in-an-overlay/2020/510/
我总结一下你的可能性:
使用剪辑路径
通过使用 CustomClipper
,给定一个小部件,您可以定义要绘制的内容和不绘制的内容。然后,您可以围绕相关的底层 UI 元素绘制一个矩形或椭圆形:
class InvertedClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
return Path.combine(
PathOperation.difference,
Path()..addRect(
Rect.fromLTWH(0, 0, size.width, size.height)
),
Path()
..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
..close(),
);
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}
像这样将其插入到您的应用中:
ClipPath(
clipper: InvertedClipper(),
child: Container(
color: Colors.black54,
),
);
使用 CustomPainter
您可以直接绘制一个与屏幕一样大并且已经切出孔的形状,而不是在叠加层上切孔:
class HolePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black54;
canvas.drawPath(
Path.combine(
PathOperation.difference,
Path()..addRect(
Rect.fromLTWH(0, 0, size.width, size.height)
),
Path()
..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
..close(),
),
paint
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
这样插入:
CustomPaint(
size: MediaQuery.of(context).size,
painter: HolePainter()
);
使用 ColorFiltered
此解决方案无需油漆即可使用。它通过使用特定的 blendMode 在小部件树中插入子项的地方切洞:
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black54,
BlendMode.srcOut
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Align(
alignment: Alignment.bottomRight,
child: Container(
margin: const EdgeInsets.only(right: 4, bottom: 4),
height: 80,
width: 80,
decoration: BoxDecoration(
// Color does not matter but must not be transparent
color: Colors.black,
borderRadius: BorderRadius.circular(40),
),
),
),
),
],
),
);
屏幕截图(使用空安全):
因为 highlighter_coachmark
doesn't support null-safety as of this writing, use tutorial_coach_mark
支持空值安全。
完整代码:
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final List<TargetFocus> targets;
final GlobalKey _key1 = GlobalKey();
final GlobalKey _key2 = GlobalKey();
final GlobalKey _key3 = GlobalKey();
@override
void initState() {
super.initState();
targets = [
TargetFocus(
identify: 'Target 1',
keyTarget: _key1,
contents: [
TargetContent(
align: ContentAlign.bottom,
child: _buildColumn(title: 'First Button', subtitle: 'Hey!!! I am the first button.'),
),
],
),
TargetFocus(
identify: 'Target 2',
keyTarget: _key2,
contents: [
TargetContent(
align: ContentAlign.top,
child: _buildColumn(title: 'Second Button', subtitle: 'I am the second.'),
),
],
),
TargetFocus(
identify: 'Target 3',
keyTarget: _key3,
contents: [
TargetContent(
align: ContentAlign.left,
child: _buildColumn(title: 'Third Button', subtitle: '... and I am third.'),
)
],
),
];
}
Column _buildColumn({required String title, required String subtitle}) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(subtitle),
)
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: Stack(
children: [
Align(
alignment: Alignment.topLeft,
child: ElevatedButton(
key: _key1,
onPressed: () {},
child: Text('Button 1'),
),
),
Align(
alignment: Alignment.center,
child: ElevatedButton(
key: _key2,
onPressed: () {
TutorialCoachMark(
context,
targets: targets,
colorShadow: Colors.cyanAccent,
).show();
},
child: Text('Button 2'),
),
),
Align(
alignment: Alignment.bottomRight,
child: ElevatedButton(
key: _key3,
onPressed: () {},
child: Text('Button 3'),
),
),
],
),
),
);
}
}
感谢@josxha的建议。
我想制作一开始就显示给用户的教程屏幕。如下所示:
我的具体问题,如何使某些元素正常显示而其他元素不透明?
还有 the arrow
和文本,如何根据移动设备屏幕尺寸(移动响应能力)使它们 完美点 ?
您可以使用 this 库来帮助您实现您所需要的。它允许您标记要突出显示的视图以及如何突出显示它们。
用 Stack 小部件包装您当前的顶级小部件,让 Stack 的第一个子部件成为您当前的小部件。 在这个小部件下面添加一个黑色的容器,用不透明度包裹,如下所示:
return Stack(
children: <Widget>[
Scaffold( //first child of the stack - the current widget you have
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text("Foo"),
Text("Bar"),
],
),
)),
Opacity( //seconds child - Opaque layer
opacity: 0.7,
child: Container(
decoration: BoxDecoration(color: Colors.black),
),
)
],
);
然后您需要以 1x、2x、3x 分辨率创建描述和箭头的图像资产,并将它们按照此处所述的适当结构放置在您的资产文件夹中:https://flutter.dev/docs/development/ui/assets-and-images#declaring-resolution-aware-image-assets
然后您可以使用 Image.asset(...) 小部件加载您的图像(它们将以正确的分辨率加载),并将这些小部件放在另一个容器中,该容器也是堆栈,并将放置在子列表中的黑色容器下方(上例中的不透明度小部件)。
正如 RoyalGriffin 提到的,您可以使用 highlighter_coachmark 库,我也知道您遇到的错误,该错误是因为您使用的是 RangeSlider
class从 2 个不同的包中导入。你能在你的应用程序中尝试这个例子并检查它是否有效吗?
将
highlighter_coachmark
添加到您的pubspec.yaml
文件dependencies: flutter: sdk: flutter highlighter_coachmark: ^0.0.3
运行
flutter packages get
示例:
import 'package:highlighter_coachmark/highlighter_coachmark.dart';
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
GlobalKey _fabKey = GlobalObjectKey("fab"); // used by FAB
GlobalKey _buttonKey = GlobalObjectKey("button"); // used by RaisedButton
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
key: _fabKey, // setting key
onPressed: null,
child: Icon(Icons.add),
),
body: Center(
child: RaisedButton(
key: _buttonKey, // setting key
onPressed: showFAB,
child: Text("RaisedButton"),
),
),
);
}
// we trigger this method on RaisedButton click
void showFAB() {
CoachMark coachMarkFAB = CoachMark();
RenderBox target = _fabKey.currentContext.findRenderObject();
// you can change the shape of the mark
Rect markRect = target.localToGlobal(Offset.zero) & target.size;
markRect = Rect.fromCircle(center: markRect.center, radius: markRect.longestSide * 0.6);
coachMarkFAB.show(
targetContext: _fabKey.currentContext,
markRect: markRect,
children: [
Center(
child: Text(
"This is called\nFloatingActionButton",
style: const TextStyle(
fontSize: 24.0,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
)
],
duration: null, // we don't want to dismiss this mark automatically so we are passing null
// when this mark is closed, after 1s we show mark on RaisedButton
onClose: () => Timer(Duration(seconds: 1), () => showButton()),
);
}
// this is triggered once first mark is dismissed
void showButton() {
CoachMark coachMarkTile = CoachMark();
RenderBox target = _buttonKey.currentContext.findRenderObject();
Rect markRect = target.localToGlobal(Offset.zero) & target.size;
markRect = markRect.inflate(5.0);
coachMarkTile.show(
targetContext: _fabKey.currentContext,
markRect: markRect,
markShape: BoxShape.rectangle,
children: [
Positioned(
top: markRect.bottom + 15.0,
right: 5.0,
child: Text(
"And this is a RaisedButton",
style: const TextStyle(
fontSize: 24.0,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
)
],
duration: Duration(seconds: 5), // this effect will only last for 5s
);
}
}
输出:
应该提到的是,面向 Material 的 feature_discovery 包不是采用不透明的方法,而是使用动画并集成到应用程序对象层次结构本身中,因此需要较少的自定义突出显示编程。一站式解决方案还支持多步高亮。
如果你不想依赖外部库,你可以自己做。其实并不难。 使用堆栈小部件,您可以将半透明覆盖层放在所有内容之上。现在,如何在强调底层 UI 元素的叠加层中“挖洞”?
这是一篇涵盖确切主题的文章:https://www.flutterclutter.dev/flutter/tutorials/how-to-cut-a-hole-in-an-overlay/2020/510/
我总结一下你的可能性:
使用剪辑路径
通过使用 CustomClipper
,给定一个小部件,您可以定义要绘制的内容和不绘制的内容。然后,您可以围绕相关的底层 UI 元素绘制一个矩形或椭圆形:
class InvertedClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
return Path.combine(
PathOperation.difference,
Path()..addRect(
Rect.fromLTWH(0, 0, size.width, size.height)
),
Path()
..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
..close(),
);
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}
像这样将其插入到您的应用中:
ClipPath(
clipper: InvertedClipper(),
child: Container(
color: Colors.black54,
),
);
使用 CustomPainter
您可以直接绘制一个与屏幕一样大并且已经切出孔的形状,而不是在叠加层上切孔:
class HolePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black54;
canvas.drawPath(
Path.combine(
PathOperation.difference,
Path()..addRect(
Rect.fromLTWH(0, 0, size.width, size.height)
),
Path()
..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
..close(),
),
paint
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
这样插入:
CustomPaint(
size: MediaQuery.of(context).size,
painter: HolePainter()
);
使用 ColorFiltered
此解决方案无需油漆即可使用。它通过使用特定的 blendMode 在小部件树中插入子项的地方切洞:
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black54,
BlendMode.srcOut
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Align(
alignment: Alignment.bottomRight,
child: Container(
margin: const EdgeInsets.only(right: 4, bottom: 4),
height: 80,
width: 80,
decoration: BoxDecoration(
// Color does not matter but must not be transparent
color: Colors.black,
borderRadius: BorderRadius.circular(40),
),
),
),
),
],
),
);
屏幕截图(使用空安全):
因为 highlighter_coachmark
doesn't support null-safety as of this writing, use tutorial_coach_mark
支持空值安全。
完整代码:
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final List<TargetFocus> targets;
final GlobalKey _key1 = GlobalKey();
final GlobalKey _key2 = GlobalKey();
final GlobalKey _key3 = GlobalKey();
@override
void initState() {
super.initState();
targets = [
TargetFocus(
identify: 'Target 1',
keyTarget: _key1,
contents: [
TargetContent(
align: ContentAlign.bottom,
child: _buildColumn(title: 'First Button', subtitle: 'Hey!!! I am the first button.'),
),
],
),
TargetFocus(
identify: 'Target 2',
keyTarget: _key2,
contents: [
TargetContent(
align: ContentAlign.top,
child: _buildColumn(title: 'Second Button', subtitle: 'I am the second.'),
),
],
),
TargetFocus(
identify: 'Target 3',
keyTarget: _key3,
contents: [
TargetContent(
align: ContentAlign.left,
child: _buildColumn(title: 'Third Button', subtitle: '... and I am third.'),
)
],
),
];
}
Column _buildColumn({required String title, required String subtitle}) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(subtitle),
)
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: Stack(
children: [
Align(
alignment: Alignment.topLeft,
child: ElevatedButton(
key: _key1,
onPressed: () {},
child: Text('Button 1'),
),
),
Align(
alignment: Alignment.center,
child: ElevatedButton(
key: _key2,
onPressed: () {
TutorialCoachMark(
context,
targets: targets,
colorShadow: Colors.cyanAccent,
).show();
},
child: Text('Button 2'),
),
),
Align(
alignment: Alignment.bottomRight,
child: ElevatedButton(
key: _key3,
onPressed: () {},
child: Text('Button 3'),
),
),
],
),
),
);
}
}
感谢@josxha的建议。