简介

很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是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

flutter系列之:做一个图像滤镜的更多相关文章

  1. flutter系列之:创建一个内嵌的navigation

    目录 简介 搭建主Navigator 构建子路由 总结 简介 我们在flutter中可以使用Navigator.push或者Navigator.pushNamed方法来向Navigator中添加不同的 ...

  2. python爬虫系列:做一个简单的动态代理池

    自动 1.设置动态的user agent import urllib.request as ure import urllib.parse as upa import random from bs4 ...

  3. 做一个会PS切图的前端开发

    系列链接 做一个会使用PS的前端开发 做一个会PS切图的前端开发 切图方法分类 PhotoShop从CS版本演变到现在的CC版本,切图功能发生了比较大的变化,我们可以把PhotoShop CS版本时的 ...

  4. 【 D3.js 入门系列 --- 3 】 做一个简单的图表!

    前面说了几节,都是对文字进行处理,这一节中将用 D3.js 做一个简单的柱形图. 做柱形图有很多种方法,比如用 HTML 的 div 标签,或用 svg . 推荐用 SVG 来做各种图形.SVG 意为 ...

  5. SLAM+语音机器人DIY系列:(三)感知与大脑——6.做一个能走路和对话的机器人

    摘要 在我的想象中机器人首先应该能自由的走来走去,然后应该能流利的与主人对话.朝着这个理想,我准备设计一个能自由行走,并且可以与人语音对话的机器人.实现的关键是让机器人能通过传感器感知周围环境,并通过 ...

  6. 致敬学长!J20航模遥控器开源项目计划【开局篇】 | 先做一个开机界面 | MATLAB图像二值化 | Img2Lcd图片取模 | OLED显示图片

    我们的开源宗旨:自由 协调 开放 合作 共享 拥抱开源,丰富国内开源生态,开展多人运动,欢迎加入我们哈~ 和一群志同道合的人,做自己所热爱的事! 项目开源地址:https://github.com/C ...

  7. 【 D3.js 入门系列 --- 5.1 】 做一个带坐标轴和标签的图表

    前面几节讲解了图标.坐标轴.比例等等,这一节整合这些内容做一个实用的图表.结果图如下: 代码如下所示: <html> <head> <meta charset=" ...

  8. ADO.NET学习系列(三)----做一个登录案例

    总体思路.根据用户输入的用户名和密码,来判断,和数据库里面存的是不是一样,如果一样就表明登录成功,否则就登录失败. 方案一: 1.select* from 表名 where username=&quo ...

  9. 【 D3.js 入门系列 — 3 】 做一个简单的图表!

    图1. 柱形图 1. 柱形图 前几章的例子,都是对文字进行处理.本章中将用 D3 做一个简单的柱形图.制作柱形图有很多种方法,比如用 HTML 的 <div> 标签,或在 SVG 上绘制 ...

  10. 图像滤镜艺术---ZPhotoEngine超级算法库

    原文:图像滤镜艺术---ZPhotoEngine超级算法库 一直以来,都有个想法,想要做一个属于自己的图像算法库,这个想法,在经过了几个月的努力之后,终于诞生了,这就是ZPhotoEngine算法库. ...

随机推荐

  1. 第四部分:Spdlog日志库的核心组件分析-logger

    Spdlog是一个快速且可扩展的C++日志库,它支持多线程和异步日志记录.在本文中,我们将分析Spdlog日志库的核心代码,探究其实现原理和代码结构. Spdlog的基本架构 上一篇文章介绍了spdl ...

  2. Sokit(TCP/UDP调试工具)

    下载:http://www.winwin7.com/soft/56522.html#xiazai Sokit中文版是一款免费开源的TCP / UDP 测试(调试)工具,它主要可以用于接收和发送TCP/ ...

  3. 微软博客上几篇 Semantic-kernel (SK)文章

    自从最近微软开源Semantic-kernel  来帮助开发人员在其应用程序中使用AI大型语言模型(LLM)以来,Microsoft一直在忙于改进它,发布了有关如何使用它的新指南并发布了5篇文章介绍他 ...

  4. Wiki.js配置LDAP认证

    安装好wikijs 之后, 可以进行进一步的详细配置. 这里介绍 LDAP 认证的配置. 在 管理 -> 身份验证 -> 添加策略 -> 选择 LDAP/AD , 如下: 接下来进行 ...

  5. [软件测试] sonar 常见问题及修复思路【待完善】

    1 sonar 概述 sonar 是什么? Sonar 是一个用于代码质量管理的开放平台.通过插件机制,Sonar 可以集成不同的测试工具,代码分析工具,以及持续集成工具. 与持续集成工具(例如 Hu ...

  6. [Linux]常用命令之【which/whereis/whatis】

    1 which shows the full path of (shell) commands. 显示(系统)命令所在目录 [root@test ~]# which ls alias ls='ls - ...

  7. Redis key命名规范

    Redis key命名规范 一.实现目标 简洁,高效,可维护 二.键值设计规约 1 Redis key 命名风格 [推荐]Redis key 命名需具有可读性以及可管理性,不该使用含义不清的 key ...

  8. Python程序笔记20230302

    Alice.Bob 和他们的朋友们 问题主体 密码学家 Rivest.Shamir.Adleman 于1977年4月撰写了一篇论文<数字签名与公钥密码学>(On Digital Signa ...

  9. 无法使用Resource注解

    问题描述: 学习Spring框架的时候,发现无法使用@Resource注解,只能使用@Autowired注解. 问题原因: JDK11删除了javax.annotation包,需要导入,否则无法使用@ ...

  10. pyinstaller打包python程序

    pyinstaller打包python程序 1.pyinstaller安装 安装命令: #升级pip版本 >>> pip install -U pip #安装pyinstaller ...