手機(jī)版 | 網(wǎng)站導(dǎo)航
觀察家網(wǎng) > 消費(fèi) >

flutter系列之:做一個(gè)圖像濾鏡 全球快資訊

博客園 | 2023-06-16 14:17:55
目錄簡介我們的目標(biāo)帶濾鏡的圖片打造filter按鈕打造可滑動(dòng)按鈕最后要解決的問題簡介

很多時(shí)候,我們需要一些特效功能,比如給圖片做個(gè)濾鏡什么的,如果是h5頁面,那么我們可以很容易的通過css濾鏡來實(shí)現(xiàn)這個(gè)功能。


(資料圖)

那么如果在flutter中,如果要實(shí)現(xiàn)這樣的濾鏡功能應(yīng)該怎么處理呢?一起來看看吧。

我們的目標(biāo)

在繼續(xù)進(jìn)行之前,我們先來討論下本章到底要做什么。最終的目標(biāo)是希望能夠?qū)崿F(xiàn)一個(gè)圖片的濾鏡功能。

那么我們的app界面實(shí)際上可以分為兩個(gè)部分。第一個(gè)部分就是帶濾鏡效果的圖片,第二個(gè)部分就是可以切換的濾鏡按鈕。

接下來我們一步步來看如何實(shí)現(xiàn)這些功能。

帶濾鏡的圖片

要實(shí)現(xiàn)這個(gè)功能其實(shí)比較簡單,我們構(gòu)建一個(gè)widget,因?yàn)檫@個(gè)widget中的圖片需要根據(jù)自身選擇的濾鏡顏色來改變圖片的狀態(tài),所以這里我們需要的是一個(gè)StatefulWidget,在state里面,存儲(chǔ)的就是當(dāng)前的_filterColor。

構(gòu)建一個(gè)圖片的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方法中,我們返回了一個(gè)Positioned.fill填充的widget,這個(gè)widget可以把a(bǔ)pp的視圖填滿。

在_buildPhotoWithFilter方法中,我們返回了Image.asset,里面可以設(shè)置image的color和colorBlendMode。這兩個(gè)值就是圖片濾鏡的關(guān)鍵。

就這么簡單?一個(gè)圖片濾鏡就完成了?對的就是這么簡單。圖片濾鏡就是Image.asset中自帶的功能。

但是在實(shí)際的應(yīng)用中,這個(gè)color不會(huì)是固定的,是需要根據(jù)我們的不同選擇而進(jìn)行變化的。為了能夠接受到這個(gè)變化的值,我們使用了ValueListenableBuilder,通過傳入一個(gè)可變的ValueNotifier,來實(shí)現(xiàn)監(jiān)聽color變化的結(jié)果。

final _filterColor = ValueNotifier(Colors.white);  void _onFilterChanged(Color value) {    _filterColor.value = value;  }

另外,我們提供了一個(gè)觸發(fā)_filterColor的值進(jìn)行變化的方法_onFilterChanged。

上面的代碼運(yùn)行的結(jié)果如下:

很好,現(xiàn)在我們已經(jīng)有了一個(gè)帶有顏色filter功能的界面了。 接下來我們還需要一個(gè)filter的按鈕,來觸發(fā)filter顏色的變化。

打造filter按鈕

這里我們的filter包含了Colors.primaries中所有的顏色再加上一個(gè)自定義的白色。

每一個(gè)filter按鈕其實(shí)都可以用一個(gè)widget來表示。我們希望是一個(gè)圓形的filter按鈕,里面有一個(gè)圖片的小的縮略圖來展示filter的效果。

另外通過tap對應(yīng)的filter按鈕,還可以實(shí)現(xiàn)color切換的功能。

所以對于Filter按鈕widget來說,可以接收兩個(gè)參數(shù),一個(gè)是當(dāng)前的color,另外一個(gè)是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,            ),          ),        ),      ),    );  }
打造可滑動(dòng)按鈕

上一節(jié)我們創(chuàng)建好了filter按鈕,接下來就是把filter按鈕組裝起來,形成一個(gè)可滑動(dòng)的filter按鈕組件。

要想滑動(dòng)widget,我們可以使用Scrollable組件,通過傳入一個(gè)PageController來控制PageView的展示。

Scrollable出了controller之外,還有一個(gè)非常重要的屬性就是viewportBuilder。在viewportBuilder中可以傳入viewportOffset。

當(dāng)Scrollable滑動(dòng)的時(shí)候,viewportOffset中的pixels是會(huì)動(dòng)態(tài)變化的。我們可以根據(jù)viewportOffset中的pixels的變化來重繪filter按鈕。

如果要根據(jù)viewportOffset的變化來重新定位child組件的位置的話,最好的方式就是將其包裹在Flow組件中。

因?yàn)镕low提供了一個(gè)FlowDelegate,我們可以在FlowDelegate中根據(jù)viewportOffset的不同來重繪filter widget。這個(gè)FlowDelegate的實(shí)現(xiàn)如下:

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;    // 一個(gè)單獨(dú)item的寬度    final itemExtent = size / filtersPerScreen;    // active item的index    final active = viewportOffset.pixels / itemExtent;    print("active$active");    // 要繪制的最小的index,在active item的左邊最多繪制3個(gè)items    final min = math.max(0, active.floor() - 3).toInt();    //要繪制的最大index,在active item的右邊最多繪制3個(gè)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被替換的情況下觸發(fā)    return oldDelegate.viewportOffset != viewportOffset;  }}

在paintChildren的最后,我們通過調(diào)用context.paintChild來重繪child。

可以看到這里傳入了三個(gè)參數(shù),第一個(gè)參數(shù)是child的index,這個(gè)index指的是創(chuàng)建Flow時(shí)候傳入的children數(shù)組中的index:

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

最后,我們把創(chuàng)建Flow的方法_buildCarousel放到Scrollable中去,并將viewportOffset作為Flow的構(gòu)造函數(shù)參數(shù)傳入,從而實(shí)現(xiàn)Flow根據(jù)Scrollable的滑動(dòng)而發(fā)送相應(yīng)的變化:

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,                ),              ],            );          },        );      },    );
最后要解決的問題

到目前為止,一切看起來都很好。但是如果你仔細(xì)研究的話可能會(huì)產(chǎn)生一個(gè)疑問。那就是Scrollable的controller是PageController,我們是通過PageController中的page來切換對應(yīng)的filter顏色的:

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

那么這個(gè)page是如何變化的呢?什么時(shí)候從0變成1呢?

我們先來看下PageController的構(gòu)造函數(shù):

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

除了初始化的initialPage之外,還有一個(gè)viewportFraction。這個(gè)值就是指一個(gè)view可以被分成多少個(gè)page。

以我的iphone14為例,它的constraints.maxWidth=390.0, 如果被分成5份的話,一份的值是78.0。 也就是說當(dāng)Scrollable滑動(dòng)78,的時(shí)候,page就從0變成1了。這和我們在Flow中重繪child時(shí)候,取的index是一致的。

最后,效果圖如下:

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

標(biāo)簽:

  • 標(biāo)簽:中國觀察家網(wǎng),商業(yè)門戶網(wǎng)站,新聞,專題,財(cái)經(jīng),新媒體,焦點(diǎn),排行,教育,熱點(diǎn),行業(yè),消費(fèi),互聯(lián)網(wǎng),科技,國際,文化,時(shí)事,社會(huì),國內(nèi),健康,產(chǎn)業(yè)資訊,房產(chǎn),體育。

相關(guān)推薦