网易首页 > 网易号 > 正文 申请入驻

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

0
分享至

简介

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

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

我们的目标

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

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

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

带滤镜的图片

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

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

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

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

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

final _filterColor = ValueNotifier(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(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

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
越是大佬的商战博弈,越是像小孩子捣蛋一样!网友:大人都办不上来

越是大佬的商战博弈,越是像小孩子捣蛋一样!网友:大人都办不上来

夜深爱杂谈
2026-01-27 18:34:06
LME伦铜涨幅扩大至10% 突破每吨14400美元

LME伦铜涨幅扩大至10% 突破每吨14400美元

财联社
2026-01-29 22:11:12
农村6条硬规矩出炉!宅基地补偿款全管,农民别再被村干部拿捏

农村6条硬规矩出炉!宅基地补偿款全管,农民别再被村干部拿捏

社会野生观察员
2026-01-28 17:50:28
“金融女神”李蓓称上个月已清空黄金,未来10至20年不值得投资,她给出两点分析,但有人表示不赞同

“金融女神”李蓓称上个月已清空黄金,未来10至20年不值得投资,她给出两点分析,但有人表示不赞同

台州交通广播
2026-01-29 22:00:32
给过去十年最好的10部“历史剧”排名:《太平年》第7,第1无争议

给过去十年最好的10部“历史剧”排名:《太平年》第7,第1无争议

TVB的四小花
2026-01-29 15:11:30
穆里尼奥:我赛后向阿韦洛亚道歉了

穆里尼奥:我赛后向阿韦洛亚道歉了

体坛周报
2026-01-29 10:18:11
从“至道学宫”的反智狂欢到“牢A”的流量爆火,手法一模一样

从“至道学宫”的反智狂欢到“牢A”的流量爆火,手法一模一样

壹家言
2026-01-28 15:14:03
又曝3大瓜!婚前一夜情、张杰官宣退出、女明星知三当三,太离谱

又曝3大瓜!婚前一夜情、张杰官宣退出、女明星知三当三,太离谱

最美的巧合
2026-01-29 01:49:07
“小刘亦菲”王楚然崛起,成为新一代仙系花朵!

“小刘亦菲”王楚然崛起,成为新一代仙系花朵!

时尚搭配Anne
2026-01-03 10:00:09
陪读妈妈半夜录视频引发争议,清凉装扮引发热议。

陪读妈妈半夜录视频引发争议,清凉装扮引发热议。

特约前排观众
2026-01-29 00:15:03
致命48小时!战争,还是要来了!

致命48小时!战争,还是要来了!

大嘴说天下
2026-01-28 21:03:17
特朗普:将对伊朗发动大规模打击

特朗普:将对伊朗发动大规模打击

亚太观澜
2026-01-29 20:40:03
自取灭亡的立陶宛总统瑙塞达,这次中国没有迁就他!

自取灭亡的立陶宛总统瑙塞达,这次中国没有迁就他!

百态人间
2026-01-29 15:43:09
曼纳:我们很难签斯特林,他要价不菲并且已经长时间没有比赛

曼纳:我们很难签斯特林,他要价不菲并且已经长时间没有比赛

懂球帝
2026-01-29 04:22:08
真的佩服我们公司的主管!

真的佩服我们公司的主管!

太急张三疯
2026-01-16 06:28:07
日媒:中国同辈球员中,没人能与张本美和相提并论

日媒:中国同辈球员中,没人能与张本美和相提并论

湖报体育
2026-01-29 22:13:51
苹果正式宣布推出全新套餐服务,性价比超高!

苹果正式宣布推出全新套餐服务,性价比超高!

XCiOS俱乐部
2026-01-29 13:53:48
全世界都在屏息等待,一场大战即将到来…

全世界都在屏息等待,一场大战即将到来…

牛弹琴
2026-01-29 07:34:42
他奉命清剿红军,见阵地炊烟令全军做饭,一饭换后半生平安

他奉命清剿红军,见阵地炊烟令全军做饭,一饭换后半生平安

磊子讲史
2026-01-28 11:49:14
辞去央视工作,嫁大18岁李铁为妻,如今52岁张泉灵已走上另一条路

辞去央视工作,嫁大18岁李铁为妻,如今52岁张泉灵已走上另一条路

白面书誏
2026-01-07 17:17:06
2026-01-29 22:55:00
flydean程序那些事
flydean程序那些事
最通俗的解读,最深刻的干货!
356文章数 438关注度
往期回顾 全部

科技要闻

周亚辉的AI新赌局:国内太卷 出海另起炉灶

头条要闻

"轮椅女孩"小学6年第一次靠自己走进教室 获全班欢呼

头条要闻

"轮椅女孩"小学6年第一次靠自己走进教室 获全班欢呼

体育要闻

詹姆斯哭了!骑士视频致敬41岁超巨

娱乐要闻

曝金晨涉嫌交通肇事逃逸 本人尚未回应

财经要闻

崔东树:中国汽车未来年销或达5000万辆

汽车要闻

车长超5米还带后轮转向 比亚迪海豹08/海狮08将亮相

态度原创

数码
手机
教育
公开课
军事航空

数码要闻

REDMI Pad 2 Pro:千元档的最新性价比之选

手机要闻

苹果为澳大利亚Telstra用户推送更新,修复iOS 16.7.13网络故障

教育要闻

教育家精神万里行|彭向:扎根乡村讲台 以爱为炬育新苗

公开课

李玫瑾:为什么性格比能力更重要?

军事要闻

中方被指支持俄生产武器 外交部回应

无障碍浏览 进入关怀版