如何使不透明的教程屏幕变得扑朔迷离?

anu*_*der 18 user-interface android user-experience dart flutter

我想制作一开始显示给用户的教程屏幕。就像下面这样:

在此处输入图片说明

我的具体问题是,如何使某些特定元素正常显示而其他不透明?

还有the arrow文字,如何根据移动设备的屏幕尺寸(移动响应能力)使其完美指向

Cop*_*oad 16

正如 RoyalGriffin 提到的,您可以使用highlighter_coachmark库,我也知道您遇到的错误,错误是因为您使用的RangeSlider是从 2 个不同包导入的类。你能在你的应用程序中试试这个例子并检查它是否有效吗?

  1. 添加highlighter_coachmark到您的pubspec.yaml文件

    dependencies:
      flutter:
        sdk: flutter
    
      highlighter_coachmark: ^0.0.3
    
    Run Code Online (Sandbox Code Playgroud)
  2. 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
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

输出:

在此处输入图片说明


  • 包highlighter_coachmark已经一年多没有收到更新并且没有空安全性。我建议使用包 [tutorial_coachmark](https://pub.dev/packages/tutorial_coach_mark)。@CopsOnRoad 也许你可以将其添加到你的答案中? (3认同)

Roy*_*fin 11

您可以使用库来帮助您实现所需的内容。它允许您标记要突出显示的视图以及突出显示它们的方式。


Cop*_*oad 11

屏幕截图(使用空安全):

在此输入图像描述


由于highlighter_coachmark在撰写本文时不支持空安全,因此请使用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'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

感谢@josxha的建议。


Moz*_*esM 9

用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),
      ),
    )
  ],
);
Run Code Online (Sandbox Code Playgroud)

然后,您需要以1x,2x,3x分辨率创建描述和箭头的图像资产,并将它们放在资产文件夹中的适当结构中,如下所述:https : //flutter.dev/docs/development/ui/资产和图像#声明分辨率感知图像资产

然后,您可以使用Image.asset(...)小部件加载图像(它们将以正确的分辨率加载),并将这些小部件放置在另一个容器中,该容器也将是堆栈的子容器,并将被放置在子级列表中黑色容器的下方(上面示例中的Opacity小部件)。


Pet*_*vin 6

应该提到的是,面向 Material 的feature_discovery包不是一种不透明的方法,而是使用动画并集成到应用程序对象层次结构本身中,因此需要较少的自定义高亮编程。交钥匙解决方案还支持多步骤亮点。


Sch*_*ken 5

如果你不想依赖外部库,你可以自己做。其实没那么难。使用堆栈小部件,您可以将半透明覆盖层放在所有内容之上。现在,如何在覆盖层中“切洞”以强调底层 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;
}
Run Code Online (Sandbox Code Playgroud)

像这样将其插入您的应用程序中:

ClipPath(
  clipper: InvertedClipper(),
    child: Container(
      color: Colors.black54,
    ),
);
Run Code Online (Sandbox Code Playgroud)

使用自定义画家

您可以直接绘制一个与屏幕一样大并且已经切出孔的形状,而不是在覆盖层中切孔:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

像这样插入:

CustomPaint(
  size: MediaQuery.of(context).size,
  painter: HolePainter()
);
Run Code Online (Sandbox Code Playgroud)

使用颜色过滤

该解决方案无需油漆即可工作。它通过使用特定的混合模式在插入小部件树中的子项的地方切出洞:

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),
            ),
          ),
        ),
      ),
    ],
  ),
);
Run Code Online (Sandbox Code Playgroud)