flutter系列之:做一个图像滤镜

news2024/11/22 15:20:24

文章目录

  • 简介
  • 我们的目标
  • 带滤镜的图片
  • 打造filter按钮
  • 打造可滑动按钮
  • 最后要解决的问题

简介

很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。

那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起来看看吧。

我们的目标

在继续进行之前,我们先来讨论下本章到底要做什么。最终的目标是希望能够实现一个图片的滤镜功能。

那么我们的app界面实际上可以分为两个部分。第一个部分就是带滤镜效果的图片,第二个部分就是可以切换的滤镜按钮。

接下来我们一步步来看如何实现这些功能。

带滤镜的图片

要实现这个功能其实比较简单,我们构建一个widget,因为这个widget中的图片需要根据自身选择的滤镜颜色来改变图片的状态,所以这里我们需要的是一个StatefulWidget,在state里面,存储的就是当前的_filterColor。

构建一个图片的widget的代码可以如下所示:

class ImageFilterApp extends StatefulWidget {
  const ImageFilterApp({super.key});

  @override
  State<ImageFilterApp> createState() =>
      _ImageFilterAppState();
}

class _ImageFilterAppState
    extends State<ImageFilterApp> {
  final _filters = [
    Colors.white,
    ...Colors.primaries
  ];

  final _filterColor = ValueNotifier<Color>(Colors.white);

  void _onFilterChanged(Color value) {
    _filterColor.value = value;
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.black,
      child: Stack(
        children: [
          Positioned.fill(
            child: _buildPhotoWithFilter(),
          ),
        ],
      ),
    );
  }

  Widget _buildPhotoWithFilter() {
    return ValueListenableBuilder(
      valueListenable: _filterColor,
      builder: (context, value, child) {
        final color = value;
        return Image.asset(
          'images/head.jpg',
          color: color.withOpacity(0.5),
          colorBlendMode: BlendMode.color,
          fit: BoxFit.cover,
        );
      },
    );
  }
}

在build方法中,我们返回了一个Positioned.fill填充的widget,这个widget可以把app的视图填满。

在_buildPhotoWithFilter方法中,我们返回了Image.asset,里面可以设置image的color和colorBlendMode。这两个值就是图片滤镜的关键。

就这么简单?一个图片滤镜就完成了?对的就是这么简单。图片滤镜就是Image.asset中自带的功能。

但是在实际的应用中,这个color不会是固定的,是需要根据我们的不同选择而进行变化的。为了能够接受到这个变化的值,我们使用了ValueListenableBuilder,通过传入一个可变的ValueNotifier,来实现监听color变化的结果。

  final _filterColor = ValueNotifier<Color>(Colors.white);

  void _onFilterChanged(Color value) {
    _filterColor.value = value;
  }

另外,我们提供了一个触发_filterColor的值进行变化的方法_onFilterChanged。

上面的代码运行的结果如下:

很好,现在我们已经有了一个带有颜色filter功能的界面了。 接下来我们还需要一个filter的按钮,来触发filter颜色的变化。

打造filter按钮

这里我们的filter包含了Colors.primaries中所有的颜色再加上一个自定义的白色。

每一个filter按钮其实都可以用一个widget来表示。我们希望是一个圆形的filter按钮,里面有一个图片的小的缩略图来展示filter的效果。

另外通过tap对应的filter按钮,还可以实现color切换的功能。

所以对于Filter按钮widget来说,可以接收两个参数,一个是当前的color,另外一个是tap之后的VoidCallback onFilterSelected, 所以最终我们的FilterItem是下面的样子的:

class FilterItem extends StatelessWidget {
  const FilterItem({
    super.key,
    required this.color,
    this.onFilterSelected,
  });

  final Color color;
  final VoidCallback? onFilterSelected;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onFilterSelected,
      child: AspectRatio(
        aspectRatio: 1.0,
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: ClipOval(
            child: Image.asset(
                'images/head.jpg',
              color: color.withOpacity(0.5),
              colorBlendMode: BlendMode.hardLight,
            ),
          ),
        ),
      ),
    );
  }

打造可滑动按钮

上一节我们创建好了filter按钮,接下来就是把filter按钮组装起来,形成一个可滑动的filter按钮组件。

要想滑动widget,我们可以使用Scrollable组件,通过传入一个PageController来控制PageView的展示。

Scrollable出了controller之外,还有一个非常重要的属性就是viewportBuilder。在viewportBuilder中可以传入viewportOffset。

当Scrollable滑动的时候,viewportOffset中的pixels是会动态变化的。我们可以根据viewportOffset中的pixels的变化来重绘filter按钮。

如果要根据viewportOffset的变化来重新定位child组件的位置的话,最好的方式就是将其包裹在Flow组件中。

因为Flow提供了一个FlowDelegate,我们可以在FlowDelegate中根据viewportOffset的不同来重绘filter widget。这个FlowDelegate的实现如下:

class CarouselFlowDelegate extends FlowDelegate {
  CarouselFlowDelegate({
    required this.viewportOffset,
    required this.filtersPerScreen,
  }) : super(repaint: viewportOffset);

  final ViewportOffset viewportOffset;
  final int filtersPerScreen;

  @override
  void paintChildren(FlowPaintingContext context) {
    print(viewportOffset.pixels);

    final count = context.childCount;

    //绘制宽度
    final size = context.size.width;

    // 一个单独item的宽度
    final itemExtent = size / filtersPerScreen;

    // active item的index
    final active = viewportOffset.pixels / itemExtent;
    print('active$active');

    // 要绘制的最小的index,在active item的左边最多绘制3个items
    final min = math.max(0, active.floor() - 3).toInt();

    //要绘制的最大index,在active item的右边最多绘制3个items
    final max = math.min(count - 1, active.ceil() + 3).toInt();

    // 重新绘制要展示的item
    for (var index = min; index <= max; index++) {
      final itemXFromCenter = itemExtent * index - viewportOffset.pixels;
      final percentFromCenter = 1.0 - (itemXFromCenter / (size / 2)).abs();
      final itemScale = 0.5 + (percentFromCenter * 0.5);
      final opacity = 0.25 + (percentFromCenter * 0.75);

      final itemTransform = Matrix4.identity()
        ..translate((size - itemExtent) / 2)
        ..translate(itemXFromCenter)
        ..translate(itemExtent / 2, itemExtent / 2)
        ..multiply(Matrix4.diagonal3Values(itemScale, itemScale, 1.0))
        ..translate(-itemExtent / 2, -itemExtent / 2);

      context.paintChild(
        index,
        transform: itemTransform,
        opacity: opacity,
      );
    }
  }

  @override
  bool shouldRepaint(covariant CarouselFlowDelegate oldDelegate) {
    //viewportOffset被替换的情况下触发
    return oldDelegate.viewportOffset != viewportOffset;
  }
}

在paintChildren的最后,我们通过调用context.paintChild来重绘child。

可以看到这里传入了三个参数,第一个参数是child的index,这个index指的是创建Flow时候传入的children数组中的index:

      Flow(
        delegate: CarouselFlowDelegate(
          viewportOffset: viewportOffset,
          filtersPerScreen: _filtersPerScreen,
        ),
        children: [
          for (int i = 0; i < filterCount; i++)
            FilterItem(
              onFilterSelected: () => _onFilterTapped(i),
              color: itemColor(i),
            ),
        ],
      )

最后,我们把创建Flow的方法_buildCarousel放到Scrollable中去,并将viewportOffset作为Flow的构造函数参数传入,从而实现Flow根据Scrollable的滑动而发送相应的变化:

Widget build(BuildContext context) {
    return Scrollable(
      controller: _controller,
      axisDirection: AxisDirection.right,
      physics: const PageScrollPhysics(),
      viewportBuilder: (context, viewportOffset) {
        return LayoutBuilder(
          builder: (context, constraints) {
            final itemSize = constraints.maxWidth * _viewportFractionPerItem;
            viewportOffset
              ..applyViewportDimension(constraints.maxWidth)
              ..applyContentDimensions(0.0, itemSize * (filterCount - 1));

            return Stack(
              alignment: Alignment.bottomCenter,
              children: [
                _buildCarousel(
                  viewportOffset: viewportOffset,
                  itemSize: itemSize,
                ),
              ],
            );
          },
        );
      },
    );

最后要解决的问题

到目前为止,一切看起来都很好。但是如果你仔细研究的话可能会产生一个疑问。那就是Scrollable的controller是PageController,我们是通过PageController中的page来切换对应的filter颜色的:

  void _onPageChanged() {
    print('page${_controller.page}');
    final page = (_controller.page ?? 0).round();
    if (page != _page) {
      _page = page;
      widget.onFilterChanged(widget.filters[page]);
    }
  }

那么这个page是如何变化的呢?什么时候从0变成1呢?

我们先来看下PageController的构造函数:

    _controller = PageController(
      initialPage: _page,
      viewportFraction: _viewportFractionPerItem,
    );

除了初始化的initialPage之外,还有一个viewportFraction。这个值就是指一个view可以被分成多少个page。

以我的iphone14为例,它的constraints.maxWidth=390.0, 如果被分成5份的话,一份的值是78.0。 也就是说当Scrollable滑动78,的时候,page就从0变成1了。这和我们在Flow中重绘child时候,取的index是一致的。

最后,效果图如下:

本文的例子:https://github.com/ddean2009/learn-flutter.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/652844.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

复合垂直纳米线中硅锗的选择性湿法蚀刻

引言 目前&#xff0c;对高效能和高性能微电子组件的持续需求是晶体管小型化和芯片密度增加的关键驱动因素。这些纳米级晶体管的性能取决于其架构和材料特性。由于更好的静电性能&#xff0c;全栅场效应晶体管(GAAFET)有望取代当前的FinFET架构。 垂直GAAFET很难集成到当前的…

vue 树形结构,后端返回平级数据 一个页面向另一个页面传递数据

代码 Index.vue页面 methods: {/** 查询会议列表 */getList() {// this.loading true;listMeeting(this.queryParams).then(response > {this.meetingList response.rowsthis.total response.totalthis.loading false})//后端请求树形数据listNoPage().then(response &…

Vue- mixin(混入)

mixin(混入) mixin&#xff1a;翻译过来叫混入&#xff0c;也有人叫混合&#xff0c;官方叫混入 作用&#xff1a;两个组件共享一个配置(配置必须是一样的)&#xff0c;本质就是复用配置 通过案例理解学习如何使用以及作用 1 编写案例 提出需求&#xff0c;新增一个组件&a…

css平面转换

目录 平面转换transform(2D转换) CSS3 过渡&#xff08;重点&#xff09; 平面转换-平移 平移实现居中效果 平移转换-旋转 平面多重转换 旋转-改变转换原点 平移转换-缩放 平移转换-倾斜 平面转换transform(2D转换) 作用&#xff1a;给元素添加动态效果&#xff0c;一…

阿里云ECS弹性公网IP临时升级带宽

背景 618到来&#xff0c;公司服务器近一周内会出现IO暴增&#xff0c;需要临时升级带宽以满足需求。但由于使用的是EIP&#xff0c;无法直接在实例控制台进行升配。 方案 在弹性公网IP进行扩容 阿里云登录 - 欢迎登录阿里云&#xff0c;安全稳定的云计算服务平台

redis面试题详解

缓存 缓存穿透 缓存穿透&#xff1a;查询一个不存在的数据&#xff0c;MySQL查询不到数据也不会直接写入缓存&#xff0c;就会导致每次请求都查数据库。 解决方案一&#xff1a;缓存空数据&#xff0c;查询返回的数据为空&#xff0c;仍把这个空结果进行缓存 解决方案二&am…

WSL2安装Ubuntu及一些问题

文章目录 安装wsl2设置wsl版本安装Linux发行版问题问题1问题2 迁移导出注销原系统导入 windows和linux互传文件解决raw.githubusercontent.com无法访问的问题 安装wsl2 安装条件 内部版本 19041 及以上 (win10 2004以上或者win11) 查看方法&#xff1a;按 Windows健 R -->…

mathtype7.4数学计算工具下载安装教程

很多朋友使用word写文档或者论文&#xff0c;但是又要插入很多数学计算公式的情况&#xff0c;使用word本身的公式和符号并不是很方便用&#xff0c;今天分享给大家一个实用的数学工具mathtype。 MathType软件可以选择下载Windows版本MathType 7或Mac版本MathType 7。MathType…

OpenGL超级第11章学习笔记:高级数据管理

前言 本篇在讲什么 OpenGL蓝宝书第十一章学习笔记之高级数据管理 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C语法有简单认知 对OpenGL有简单认知 最好是有OpenGL超级宝典蓝宝书 依赖Visual Studio编辑器 本篇的特色 具有全流程的图文教学 重实践&#x…

export 和 export default 的区别

一、铺垫 ES6模块化主要包含以下3种用法&#xff1a; ① 默认导出和默认导入---export default ② 按需导出和按需导入---export ③ 直接导入并执行模块中的代码 二、默认导出 语法&#xff1a;export default 需要共享的成员 创建一个共享的数据 order.js let name…

云服务器上的数据库莫名被删除?

云服务器上的数据库莫名被删除&#xff1f; 前言排查问题及解决后续 前言 如题&#xff0c;之前&#xff0c;自己买了一个云服务器&#xff0c;用来练手&#xff0c;然后搭了一个自己的网站部署了上去。本来也就是练练手&#xff0c;没想到前几天去用的时候发现死活登录不上去…

SSTI——java里的ssti

1.Velocity 2.FreeMarker 因为从来没接触过java语言 所以对这些也是基本上一窍不通 这里只简单的提及 不做具体介绍 会找一下题来做 但是没有找到有关java ssti的题目 confusion1 看一下描述 打开题目 没发现什么东西 但是 login register页面显示访问不成功 查看源代码找到…

【转载】陆奇最新演讲全文实录:大模型带来的新范式(附下载文档)

《新范式 新时代 新机会》为主题的分享活动&#xff0c;陆奇博士以“新范式”为核心&#xff0c;分享了他对当前技术变革的观点&#xff0c;基于演讲整理了文字稿&#xff0c;也同步分享完整的PPT和视频&#xff0c;欢迎大家一起交流。 下载PDF文件&#xff1a; 《ChatGPT研究…

yolov8 人体姿态关键点检测

参考&#xff1a; https://github.com/ultralytics/ultralytics https://github.com/TommyZihao/Train_Custom_Dataset/tree/main/%E5%85%B3%E9%94%AE%E7%82%B9%E6%A3%80%E6%B5%8B ##安装 pip install ultralytics -i https://pypi.douban.com/simple1、命令行运行 pip安装好…

图像金字塔

​ 图像金字塔是由一幅图像的多个不同分辨率的子图构成的图像集合。是通过一个图像不断的降低采样率产生的&#xff0c;最小的图像可能仅仅有一个像素点。下图是一个图像金子塔的示例。从图中可以看到&#xff0c;图像金字塔是一系列以金字塔形状排列的、自底向上分辨率逐渐降低…

静电监控系统选购策略:有效提高系统可靠性与性价比

随着工业自动化和信息化程度的提高&#xff0c;静电在工业生产中的影响越来越显著。静电的存在会对电子设备、传输介质等产生不良影响&#xff0c;甚至会导致产品损坏和火灾事故的发生。因此&#xff0c;静电监控系统的选购对于工业生产的稳定具有重要意义。 一、功能需求 静…

Java实训日志06

文章目录 八、项目开发实现步骤&#xff08;八&#xff09;创建服务接口1、创建学校服务接口2、创建状态服务接口3、创建学生服务接口4、创建用户服务接口 &#xff08;九&#xff09;创建服务接口实现类1、创建学校服务接口实现类2、创建状态服务接口实现类3、创建学生服务接口…

【CMake 入门与进阶(10)】 CMakeLists.txt 语法规则_循环语句(附使用代码)

cmake 中除了 if 条件判断之外&#xff0c;还支持循环语句&#xff0c;包括 foreach()循环、while()循环。 foreach 循环 1、foreach 基本用法 foreach 循环的基本用法如下所示&#xff1a; foreach(loop_var arg1 arg2 ...)command1(args ...)command2(args ...)... endfore…

chatgpt赋能python:Python编写计算器的方法

Python编写计算器的方法 介绍 计算器是一款常见的实用工具&#xff0c;它能够进行基本的算数运算&#xff0c;如加减乘除等。在Python中&#xff0c;我们可以利用各种库和函数来实现计算器的功能&#xff0c;从而方便地进行数值计算。 怎么编写计算器 在Python中&#xff0…

WINS服务的作用和效率

WINS服务的作用和效率 目录 WINS服务的作用和效率 一、WINS服务 二、配置 2.1、服务端 2.2、客户端 2.3、配置方法 三、效果 3.1、计算机-网络&#xff08;过去的“网上邻居”&#xff09; 3.2、局域网打印机共享 3.3、基于NetBios名称的数据库客户端连接 一、WINS服…