简介
很多时候,咱们须要一些特效性能,比方给图片做个滤镜什么的,如果是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
发表回复