Flutter使用Overlay与ColorFiltered新手引导实现示例

目录
  • 思路
  • Flutter BlendMode
  • ColorFiltered
  • 实现
    • 获取镂空位置
    • ColorFiltered child
  • 完整代码
  • 最终效果
  • 小结

思路

开发过程中常见这样的需求,页面中有几个按钮,用户首次进入时需要对这几个按钮高亮展示并加上文字提示。常见的一种方案是找UI切图,那如何完全使用代码来实现呢?

就以Flutter原始Demo页面为例,如果我们需要对中间展示区域以及右下角按钮进行一个引导提示。

我们需要做到的效果是除了红色框内的Widget,其余部分要盖上一层半透明黑色浮层,相当于是全屏浮层,红色区域镂空。

首先是黑色浮层,这个比较容易,Flutter中的Overlay可以轻易实现,它可以浮在任意的Widget之上,包括Dialog

那么如何镂空呢?

一种思路是首先拿到对应的Widget与其宽高xy偏移量,然后在Overlay中先铺一层浮层后,把该WidgetOverlay的对应位置中再绘制一遍。也就是说该Widget存在两份,一份是原本的Widget,另一份是在Overlay之上又绘制一层,并且不会被浮层所覆盖,即为高亮。这是一种思路,但如果你需要进行引导提示的Widget自身有透明度,那么这个方案就略有问题,因为你的浮层即为半透明,那么用户就可以穿过顶层的Widget看到下面的内容,略有瑕疵。

那么另一种思路就是我们不去在Overlay之上盖上另一个克隆Widget,而是将Overlay半透明黑色涂层对应位置进行镂空即可,就不存在任何问题了。

Flutter BlendMode

既然需要镂空,我们需要了解一下Flutter中的图层混合模式概念

在画布上绘制形状或图像时,可以使用不同的算法来混合像素,每个算法都存在两个输入,即源(正在绘制的图像 src)和目标(要合成源图像的图像 dst)

我们把半透明黑色涂层 和 需要进行高亮的Widget 理解为src和dst。

接下来我们通过下面的图例可知,如果我们需要实现镂空效果,需要的混合模式为SrcOutDstOut,因为他们的混合模式为一个源展示,且该源与另一个源有非透明像素交汇部分完全剔除。

ColorFiltered

Flutter中为我们提供了ColorFiltered,这是一个官方为我们封装的一个以Color作为源的混合模式Widget。其接收两个参数,colorFilterchild,前者我们可以理解为上述的src,后者则为dst

下面以一段简单的代码说明

class TestColorFilteredPage extends StatelessWidget {
  const TestColorFilteredPage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ColorFiltered(
      colorFilter: const ColorFilter.mode(Colors.yellow, BlendMode.srcOut),
      child: Stack(
        children: [
          Positioned.fill(
              child: Container(
            color: Colors.transparent,
          )),
          Positioned(
              top: 100,
              left: 100,
              child: Container(
                color: Colors.black,
                height: 100,
                width: 100,
              ))
        ],
      ),
    );
  }
}

效果:

可以看到作为srccolorFiler除了与作为dstStack非透明像素交汇的地方被镂空了,其他地方均正常显示。

此处需要说明一下,作为dstchild,要实现蒙版的效果,必须要与src有所交汇,所以Stack中使用了透明的Positioned.fill填充,之所以要用透明色,是因为我们使用的混合模式srcOut的算法会剔除非透明像素交互部分

实现

上述部分思路已经足够支持我们写出想要的效果了,接下来我们来进行实现

获取镂空位置

首先我需要拿到对应Widgetkey,就可以拿到对应的宽高与xy偏移量

RenderObject? promptRenderObject =
    promptWidgetKey.currentContext?.findRenderObject();
double widgetHeight = promptRenderObject?.paintBounds.height ?? 0;
double widgetWidth = promptRenderObject?.paintBounds.width ?? 0;
double widgetTop = 0;
double widgetLeft = 0;
if (promptRenderObject is RenderBox) {
  Offset offset = promptRenderObject.localToGlobal(Offset.zero);
  widgetTop = offset.dy;
  widgetLeft = offset.dx;
}

ColorFiltered child

lastOverlay = OverlayEntry(builder: (ctx) {
  return GestureDetector(
    onTap: () {
      // 点击后移除当前展示的overlay
      _removeCurrentOverlay();
      // 准备展示下一个overlay
      _prepareToPromptSingleWidget();
    },
    child: Stack(
      children: [
        Positioned.fill(
            child: ColorFiltered(
          colorFilter: ColorFilter.mode(
              Colors.black.withOpacity(0.7), BlendMode.srcOut),
          child: Stack(
            children: [
              // 透明色填充背景,作为蒙版
              Positioned.fill(
                  child: Container(
                color: Colors.transparent,
              )),
              // 镂空区域
              Positioned(
                  left: l,
                  top: t,
                  child: Container(
                    width: w,
                    height: h,
                    decoration: decoration ??
                        const BoxDecoration(color: Colors.black),
                  )),
            ],
          ),
        )),
        // 文字提示,需要放在ColorFiltered的外层
        Positioned(
            left: l - 40,
            top: t - 40,
            child: Material(
              color: Colors.transparent,
              child: Text(
                tips,
                style: const TextStyle(fontSize: 14, color: Colors.white),
              ),
            ))
      ],
    ),
  );
});
Overlay.of(context)?.insert(lastOverlay!);

其中的文字偏移量,可以自己通过代码来设置,展示在中心,或者判断位置跟随Widget展示均可,此处不再赘述。

最后我们把Overlay添加到屏幕上展示即可。

完整代码

这里我将逻辑封装在静态工具类中,鉴于单个页面可能会有不止一个引导Widget,所以对于这个静态工具类,我们需要传入需要进行高亮引导的Widget和提示语的集合。

class PromptItem {
  GlobalKey promptWidgetKey;
  String promptTips;
  PromptItem(this.promptWidgetKey, this.promptTips);
}
class PromptBuilder {
  static List<PromptItem> toPromptWidgetKeys = [];
  static OverlayEntry? lastOverlay;
  static promptToWidgets(List<PromptItem> widgetKeys) {
    toPromptWidgetKeys = widgetKeys;
    _prepareToPromptSingleWidget();
  }
  static _prepareToPromptSingleWidget() async {
    if (toPromptWidgetKeys.isEmpty) {
      return;
    }
    PromptItem promptItem = toPromptWidgetKeys.removeAt(0);
    RenderObject? promptRenderObject =
        promptItem.promptWidgetKey.currentContext?.findRenderObject();
    double widgetHeight = promptRenderObject?.paintBounds.height ?? 0;
    double widgetWidth = promptRenderObject?.paintBounds.width ?? 0;
    double widgetTop = 0;
    double widgetLeft = 0;
    if (promptRenderObject is RenderBox) {
      Offset offset = promptRenderObject.localToGlobal(Offset.zero);
      widgetTop = offset.dy;
      widgetLeft = offset.dx;
    }
    if (widgetHeight != 0 &&
        widgetWidth != 0 &&
        widgetTop != 0 &&
        widgetLeft != 0) {
      _buildNextPromptOverlay(
          promptItem.promptWidgetKey.currentContext!,
          widgetWidth,
          widgetHeight,
          widgetLeft,
          widgetTop,
          null,
          promptItem.promptTips);
    }
  }
  static _buildNextPromptOverlay(BuildContext context, double w, double h,
      double l, double t, Decoration? decoration, String tips) {
    _removeCurrentOverlay();
    lastOverlay = OverlayEntry(builder: (ctx) {
      return GestureDetector(
        onTap: () {
          // 点击后移除当前展示的overlay
          _removeCurrentOverlay();
          // 准备展示下一个overlay
          _prepareToPromptSingleWidget();
        },
        child: Stack(
          children: [
            Positioned.fill(
                child: ColorFiltered(
              colorFilter: ColorFilter.mode(
                  Colors.black.withOpacity(0.7), BlendMode.srcOut),
              child: Stack(
                children: [
                  // 透明色填充背景,作为蒙版
                  Positioned.fill(
                      child: Container(
                    color: Colors.transparent,
                  )),
                  // 镂空区域
                  Positioned(
                      left: l,
                      top: t,
                      child: Container(
                        width: w,
                        height: h,
                        decoration: decoration ??
                            const BoxDecoration(color: Colors.black),
                      )),
                ],
              ),
            )),
            // 文字提示,需要放在ColorFiltered的外层
            Positioned(
                left: l - 40,
                top: t - 40,
                child: Material(
                  color: Colors.transparent,
                  child: Text(
                    tips,
                    style: const TextStyle(fontSize: 14, color: Colors.white),
                  ),
                ))
          ],
        ),
      );
    });
    Overlay.of(context)?.insert(lastOverlay!);
  }
  static _removeCurrentOverlay() {
    if (lastOverlay != null) {
      lastOverlay!.remove();
      lastOverlay = null;
    }
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  int _counter = 0;
  GlobalKey centerWidgetKey = GlobalKey();
  GlobalKey bottomWidgetKey = GlobalKey();
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  @override
  void initState() {
    super.initState();
    // 页面展示时进行prompt绘制,在此添加observer监听等待渲染完成后挂载prompt
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      List<PromptItem> prompts = [];
      prompts.add(PromptItem(centerWidgetKey, "这是中心Widget"));
      prompts.add(PromptItem(bottomWidgetKey, "这是底部Button"));
      PromptBuilder.promptToWidgets(prompts);
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          // 需要高亮展示的widget,需要声明其GlobalKey
          key: centerWidgetKey,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // 需要高亮展示的widget,需要声明其GlobalKey
        key: bottomWidgetKey,
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

最终效果

小结

本文仅总结代码实现思路,对于具体细节并未处理,可以在PromptItemPromptBuilder进行更多的属性声明以更加灵活的展示prompt,比如圆角等参数。有任何问题欢迎大家随时讨论。

最后附上github地址:github.com/slowguy/flu…

以上就是Flutter使用Overlay与ColorFiltered新手引导实现示例的详细内容,更多关于Flutter使用Overlay ColorFiltered的资料请关注我们其它相关文章!

(0)

相关推荐

  • Flutter EventBus事件总线的应用详解

    目录 前言 EventBus的简介 EventBus的实际应用 总结 前言 flutter项目中,有许多可以实现跨组件通讯的方案,其中包括InheritedWidget,Notification,EventBus等.本文主要探讨的是EventBus事件总线实现跨组件通讯的方法. EventBus的简介 EventBus的核心是基于Streams.它允许侦听器订阅事件并允许发布者触发事件,使得不同组件的数据不需要一层层传递,可以直接通过EventBus实现跨组件通讯. EventBus最主要是通过

  • ios开发Flutter构建todo list应用

    目录 正文 基础 Flutter 应用脚手架 创建 TodoItem 展示 Dialog 去添加列表项 列表项添加状态 正文 今天,我们将使用 Flutter 构建一个动态的 todo list 的应用. 开发完成的效果如下: 我们直接进入正题. 基础 Flutter 应用脚手架 # create new project flutter create flutter_todo_app # navigate to project cd flutter_todo_app # run flutter

  • Flutter GetPageRoute实现嵌套导航学习

    目录 1. 嵌套导航-GetPageRoute 2. 自定义拓展 3. 使用bottomNavigationBar 4.小结 1. 嵌套导航-GetPageRoute 本文主要介绍在Getx下快速实现一个嵌套导航 嵌套导航顾名思义,我们导航页面中嵌套一个独立的路由,效果如下 点击跳转 代码如下,也是比较简单 return Scaffold( appBar: AppBar(title: const Text('嵌套导航'),), body: Navigator( key: Get.nestedKe

  • Flutter 异步编程之单线程下异步模型图文示例详解

    目录 一. 本专栏图示概念规范 1. 任务概念规范 2. 任务的状态 3. 时刻与时间线 4.同步与异步 二.理解单线程中的异步任务 1. 任务的分配 2.异步任务特点 3. 异步任务完成与回调 三. Dart 语言中的异步 1.编程语言中与异步模型的对应关系 2.Dart 编程中的异步任务 3.当前任务分析 四.异步模型的延伸 1. 单线程异步模型的局限性 2. 多线程与异步的关系 3. Dart 中如何解决单线程异步模型的局限性 一. 本专栏图示概念规范 本专栏是对 异步编程 的系统探索,会

  • Flutter入门学习Dart语言变量及基本使用概念

    目录 正文 变量 变量的声明赋值 变量的划分 默认值 变量的类型推断修饰符 Late变量 类型判断is和类型转换as 一些重要概念 空安全和可空类型? 表达式和语句 注释 DartPad 正文 Dart是Google发布的开源编程语言,是一种面向对象的语言.其主要应用是Flutter框架开发(Android.IOS),此外,也可以用在服务器.脚本.Web开发中.随着Flutter3.0开始支持全平台开发,Dart也可以实现桌面应用. 关于Dart的介绍不再细说.下面开始Dart的使用介绍 首先记

  • Flutter使用push pop方法及路由进行导航详解

    目录 正文 准备工作 第一种导航方式 第二种导航方式 正文 在 Web/Mobile 应用程序中,导航是一个很重要的特性,因为它允许你从一个页面跳转到另一个页面. 在 flutter 应用程序中,我们可以使用 push(), pop() 方法实现导航,或者编写我们自己的路由. 准备工作 我们假设 FirstScreen 和 SecondScreen 是两个不同的类,分别在各自的 FirstScreen.dart 和 SecondScreen.dart 文件内. FirstScreen.dart

  • Flutter使用Overlay与ColorFiltered新手引导实现示例

    目录 思路 Flutter BlendMode ColorFiltered 实现 获取镂空位置 ColorFiltered child 完整代码 最终效果 小结 思路 开发过程中常见这样的需求,页面中有几个按钮,用户首次进入时需要对这几个按钮高亮展示并加上文字提示.常见的一种方案是找UI切图,那如何完全使用代码来实现呢? 就以Flutter原始Demo页面为例,如果我们需要对中间展示区域以及右下角按钮进行一个引导提示. 我们需要做到的效果是除了红色框内的Widget,其余部分要盖上一层半透明黑色

  • Flutter 系统是如何实现ExpansionPanelList的示例代码

    在了解ExpansionPanelList实现前,先来了解下MergeableMaterial,它展示多个MergeableMaterialItem组件,当子组件发生变化时,以动画的方式打开或者关闭子组件,MergeableMaterial的父控件需要在主轴方向是一个没有限制的控件,比如SingleChildScrollView.Row.Column等. 基本用法如下: SingleChildScrollView( child: MergeableMaterial( children: [ Ma

  • Flutter 实现酷炫的3D效果示例代码

    此文讲解3个酷炫的3D动画效果. 下面是要实现的效果: Flutter 中3D效果是通过 Transform 组件实现的,没有变换效果的实现: class TransformDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('3D 变换Demo'), ), body: Container( alignm

  • Flutter仿钉钉考勤日历的示例代码

    本文主要介绍了Flutter仿钉钉考勤日历的示例代码,分享给大家,具体如下: 效果 原型 开发 1. 使用 // 考勤日历 DatePickerDialog( initialDate: DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime(2030), onDateChanged: onDateChanged, // 0:无状态,1:正常考勤 2:异常考情,迟到,早退, // 若不满一个月,日历会自动用0补满一个月 check

  • Flutter实现仿微信分享功能的示例代码

    目录 1.首先去pub官网 2 在微信开放平台注册开发者账号以及创建你的应用程序 3 在分享页面 3.1 初始化 3.2 检测微信是否安装 3.3 分享微信消息 总结 本文设计到的知识点有 主要问题 Flutter 用来快速开发 Android iOS平台应用,在Flutter 中,通过 fluwx或者fluwx_no_pay 插件来实现微信分享功能 主要还是看自己的需求,本示例我将按照没有支付的实现.至于为什么,主要是ios打包提审比较麻烦. 那么接下来就看一下如何实现吧, 1.首先去pub官

  • Flutter实现文本滚动高亮效果的示例讲解

    目录 前言 功能实现 前言 最近有个需求是人工语音播放时文本能随语音朗读时像歌词滚动的效果. 原本第一考虑的时能随时间字体渐变成更改后的颜色, 有比较流畅的走马灯效果. 但最终实践了几次后发现要能够逐字逐行渐变有一些麻烦, 不好实现. 所以转而变为将字体直接将字体高亮, 一段文本区分成两个部分, 一个部分是高亮文本, 也就是已朗读的部分, 一个部分是剩下未朗读的非高亮文本. 通过时时渲染页面就能达成滚动高亮的效果. 功能实现 因为在Text中会存在两段文本, 所以就不能单只用Text组件, 而改

  • Flutter实现自定义搜索框AppBar的示例代码

    目录 介绍 效果图 实现步骤 完整源码 总结 介绍 开发中,页面头部为搜索样式的设计非常常见,为了可以像系统AppBar那样使用,这篇文章记录下在Flutter中自定义一个通用的搜索框AppBar记录. 功能点: 搜索框.返回键.清除搜索内容功能.键盘处理. 效果图 实现步骤 首先我们先来看下AppBar的源码,实现了PreferredSizeWidget类,我们可以知道这个类主要是控制AppBar的高度的,Scaffold脚手架里的AppBar的参数类型就是PreferredSizeWidge

  • Flutter实现笑嘻嘻的动态表情的示例代码

    目录 前言 AnimatedContainer 介绍 组件结构 细节实现 总结 前言 身在孤岛有很多无奈,比如说程序员属于比较偏门的职业.尤其是早些年,在行业里跳过几次槽后,可能你就已经认识整个圈子的人了.然后,再跳槽很可能就再次“偶遇”前同事了,用大潘的口头语来说就是:“好尴尬呀”.因此, 问起职业,往往只能是回答是搞计算机的.结果可能更尴尬,问的人可能笑嘻嘻地瞅着你,像看怪物一样看着你,接着突然冒出一句灵魂拷问:“我家电脑坏了,你能修不?”不过也不奇怪,那个时候在岛上重装一个 Windows

  • Flutter实现牛顿摆动画效果的示例代码

    目录 前言 实现步骤 1.绘制静态效果 2.加入动画 两个关键点 完整源码 总结 前言 牛顿摆大家应该都不陌生,也叫碰碰球.永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用. - 知识点:绘制.动画曲线.多动画状态更新 效果图: 实现步骤 1.绘制静态效果 首先我们需要把线和小圆球绘制出来,对于看过我之前文章的小伙伴来说这个就很简单了,效果图: 关键代码: // 小圆球半径 double radius = 6; /// 小球圆心和直线终

  • 基于Flutter实现转场动效的示例代码

    目录 前言 CupertinoFullscreenDialogTransition CupertinoPageTransition DecoratedBoxTransition FadeTransition PositionedTransition RotationTransition ScaleTransition SizeTransition SlideTransition 前言 动画经常会用于场景切换,比如滑动,缩放,尺寸变化,为应对这样的场景转换需要,Flutter 提供了 Transi

随机推荐