【问题标题】:How to make opaque tutorial screen in flutter?如何在颤动中制作不透明的教程屏幕?
【发布时间】:2019-11-11 07:41:17
【问题描述】:

我想制作一开始就向用户显示的教程屏幕。如下所示:

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

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

【问题讨论】:

  • 我认为最简单的方法是,您必须使用堆栈创建这种类型的图像并显示在屏幕顶部,并将其包装到不透明度小部件中,以便在点击后将其隐藏。就是这样。
  • @iPatel 但是图像如何适应各种手机屏幕尺寸?

标签: android user-interface flutter dart user-experience


【解决方案1】:

正如 RoyalGriffin 提到的,您可以使用 highlighter_coachmark 库,而且我也知道您遇到的错误,错误是因为您使用的是从 2 个不同包导入的 RangeSlider 类。您可以在您的应用中尝试这个示例并检查它是否正常工作吗?

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

    dependencies:
      flutter:
        sdk: flutter
    
      highlighter_coachmark: ^0.0.3
    
  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
    );
  }
}

输出:


【讨论】:

  • 嗨@CopsOnRoad 我想在主屏幕的开头运行Highlight Coachmark,但出现错误:[ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: NoSuchMethodError: The method 'findRenderObject' was called on null.
  • 在我调用showMark()之前如何确保所有UI组件都已渲染
  • @anunixercoder 抱歉耽搁了,我现在才有机会使用我的电脑,所以我相信一旦所有的小部件都呈现在屏幕上,拨打showMark()是安全的,你可以检查是否使用SchedulerBindingInstance.addPostFrameCallback 方法呈现小部件。您可以在开头加上bool _rendered = false,然后在回调中设置_rendered = true
  • 包 highlighter_coachmark 已经一年多没有收到更新,并且没有 null 安全性。我建议使用包tutorial_coachmark。 @CopsOnRoad 也许您可以将此添加到您的答案中?
  • @josxha 谢谢,我会在一天左右更新我的答案。
【解决方案2】:

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

【讨论】:

【解决方案3】:

用 Stack 小部件包装您当前的顶部小部件,让 Stack 的第一个子小部件成为您当前的小部件。 在这个小部件下面添加一个黑色的容器,用 Opacity 包裹,如下所示:

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(...) 小部件来加载您的图像(它们将以正确的分辨率加载),并将这些小部件放置在另一个容器上,该容器也将是堆栈的子容器,并且将被放置在子列表中的黑色容器下方(上例中的 Opacity 小部件)。

【讨论】:

  • 嗨,有没有使用你的方法的完整例子?因为我不明白如何将图像放在不透明层上以及如何使特定元素突出显示...谢谢
  • 透明覆盖不是问题,我猜。但是他如何切出底层 UI 元素所在的空间呢?
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    屏幕截图(使用 null-safety):


    由于在撰写本文时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'),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    感谢@josxha 的建议。

    【讨论】:

      【解决方案6】:

      如果你不想依赖外部库,你可以自己做。其实没那么难。 使用堆栈小部件,您可以将半透明叠加层放在所有内容之上。现在,如何在强调底层 UI 元素的叠加层中“挖洞”?

      这是一篇涵盖确切主题的文章:https://www.flutterclutter.dev/flutter/tutorials/how-to-cut-a-hole-in-an-overlay/2020/510/

      我会总结一下你的可能性:

      使用 ClipPath

      通过使用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()
      );
      

      使用颜色过滤

      此解决方案无需油漆即可使用。它通过使用特定的 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),
                  ),
                ),
              ),
            ),
          ],
        ),
      );
      

      【讨论】:

        猜你喜欢
        • 2021-01-09
        • 2022-10-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-11-07
        • 1970-01-01
        • 1970-01-01
        • 2020-12-12
        相关资源
        最近更新 更多