flutter系列之:做一个图像滤镜
简介
很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是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系列之:做一个图像滤镜的更多相关文章
- flutter系列之:创建一个内嵌的navigation
目录 简介 搭建主Navigator 构建子路由 总结 简介 我们在flutter中可以使用Navigator.push或者Navigator.pushNamed方法来向Navigator中添加不同的 ...
- python爬虫系列:做一个简单的动态代理池
自动 1.设置动态的user agent import urllib.request as ure import urllib.parse as upa import random from bs4 ...
- 做一个会PS切图的前端开发
系列链接 做一个会使用PS的前端开发 做一个会PS切图的前端开发 切图方法分类 PhotoShop从CS版本演变到现在的CC版本,切图功能发生了比较大的变化,我们可以把PhotoShop CS版本时的 ...
- 【 D3.js 入门系列 --- 3 】 做一个简单的图表!
前面说了几节,都是对文字进行处理,这一节中将用 D3.js 做一个简单的柱形图. 做柱形图有很多种方法,比如用 HTML 的 div 标签,或用 svg . 推荐用 SVG 来做各种图形.SVG 意为 ...
- SLAM+语音机器人DIY系列:(三)感知与大脑——6.做一个能走路和对话的机器人
摘要 在我的想象中机器人首先应该能自由的走来走去,然后应该能流利的与主人对话.朝着这个理想,我准备设计一个能自由行走,并且可以与人语音对话的机器人.实现的关键是让机器人能通过传感器感知周围环境,并通过 ...
- 致敬学长!J20航模遥控器开源项目计划【开局篇】 | 先做一个开机界面 | MATLAB图像二值化 | Img2Lcd图片取模 | OLED显示图片
我们的开源宗旨:自由 协调 开放 合作 共享 拥抱开源,丰富国内开源生态,开展多人运动,欢迎加入我们哈~ 和一群志同道合的人,做自己所热爱的事! 项目开源地址:https://github.com/C ...
- 【 D3.js 入门系列 --- 5.1 】 做一个带坐标轴和标签的图表
前面几节讲解了图标.坐标轴.比例等等,这一节整合这些内容做一个实用的图表.结果图如下: 代码如下所示: <html> <head> <meta charset=" ...
- ADO.NET学习系列(三)----做一个登录案例
总体思路.根据用户输入的用户名和密码,来判断,和数据库里面存的是不是一样,如果一样就表明登录成功,否则就登录失败. 方案一: 1.select* from 表名 where username=&quo ...
- 【 D3.js 入门系列 — 3 】 做一个简单的图表!
图1. 柱形图 1. 柱形图 前几章的例子,都是对文字进行处理.本章中将用 D3 做一个简单的柱形图.制作柱形图有很多种方法,比如用 HTML 的 <div> 标签,或在 SVG 上绘制 ...
- 图像滤镜艺术---ZPhotoEngine超级算法库
原文:图像滤镜艺术---ZPhotoEngine超级算法库 一直以来,都有个想法,想要做一个属于自己的图像算法库,这个想法,在经过了几个月的努力之后,终于诞生了,这就是ZPhotoEngine算法库. ...
随机推荐
- 升级pip报错ERROR: Could not install packages due to an OSError: [WinError 5]
今天在安装python第三方库时,提示pip需要升级,没有多想直接升级,结果报错: 1 ERROR: Could not install packages due to an OSError: [Wi ...
- Element.scrollIntoView() 方法让当前的元素滚动到浏览器窗口的可视区域内
Element.scrollIntoView() 方法让当前的元素滚动到浏览器窗口的可视区域内 语法 element.scrollIntoView(); // 等同于element.scrollInt ...
- Python集合常用操作
一.集合的介绍 1.定义:集合是无序的,集合中的元素是唯一的,集合一般用于元组或者列表中的元素去重. 2.特性:集合的目的是将不同的值存放在一起,不同的集合间用来做关系运算,无须纠结于集合中的 ...
- Java 生成海报
最近项目有个功能是生成海报 一个背景图片,一个二维码图片 将两个图片合并成一个图片. 写了一个工具类,需要的朋友自取. 1 @Component 2 public class PictureUtil ...
- SQL优化(二)-- 慢查询
1.慢查询日志 MYSQL的慢查询日志是Mysql提供的一种日志记录方式,它主要是用来记录mysql执行语句过程中,响应时间超过阀值的语句,这个阀值可以通过long_query_time去指定,比如说 ...
- 动态规划DP入门问题----最大连续子序列,最长不下降子序列(可以不连续),最长公共子序列
一.最大连续子序列 1.题目叙述 对于一个数字序列A1A2A3...An,求出连续子序列的最大和,如对于序列-2,11,-4,13,-5,-2,其中的最大序列和是11+(-4)+13=20 2.动态规 ...
- mysql 命令批量修改一个字段/帝国cms sql命令修改一个字段
UPDATE phome_enewstagsdata SET classid=5 where classid=1 UPDATE phome_ecms_news SET classid=8 where ...
- 实例讲解Playwright(一)
实例讲解Playwright(一) 网址 说明 https://playwright.dev/ 官网首页 https://playwright.dev/python/docs/intro Python ...
- EasyExcel配置步骤
1.介绍 EasyExcel是一个基于Java的简单.省内存的读写Excel的开源项目 参考 https://blog.csdn.net/u013044713/article/details/1202 ...
- [[SCOI2008]城堡] 解题报告
[SCOI2008]城堡 最大值最小,显然二分答案,但考虑二分后如何 check. \(n\) 个点 \(n\) 条边,显然这是一个基环树森林.对于基环树,常用的套路是拆环为链,枚举删去哪条边.但这题 ...