亚洲人成网网址在线看_亚洲国产美女视频_激情综合色五月丁香六月亚洲_亚洲精品在线网站

手機版 | 網站導航
觀察家網 > 消費 >

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

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

很多時候,我們需要一些特效功能,比如給圖片做個濾鏡什么的,如果是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

標簽:

  • 標簽:中國觀察家網,商業門戶網站,新聞,專題,財經,新媒體,焦點,排行,教育,熱點,行業,消費,互聯網,科技,國際,文化,時事,社會,國內,健康,產業資訊,房產,體育。

相關推薦

亚洲人成网网址在线看_亚洲国产美女视频_激情综合色五月丁香六月亚洲_亚洲精品在线网站
<ul id="ouw02"></ul>
  • 中文字幕在线视频一区| 蜜臀av性久久久久av蜜臀妖精| 久久国产麻豆精品| 2021国产精品久久精品| 亚洲国产乱码最新视频 | 国产精品国产自产拍高清av王其| 肉肉av福利一精品导航| 97精品视频在线观看自产线路二| 亚洲激情在线激情| 精品精品欲导航| 日韩精品1区2区3区| 久久综合久久综合久久| 日本高清不卡在线观看| 亚洲国产精品黑人久久久| 卡一卡二国产精品| 国产精品盗摄一区二区三区| 91麻豆精品国产91久久久资源速度| 亚洲四区在线观看| 粗大黑人巨茎大战欧美成人| 亚洲一区二区三区爽爽爽爽爽| 欧美精品一区二| 日本欧美肥老太交大片| 国产欧美一区二区三区在线看蜜臀 | 椎名由奈av一区二区三区| 91精品国产综合久久婷婷香蕉| 亚洲精品视频一区二区| 成人av集中营| 在线看不卡av| 亚洲天堂av老司机| 成人精品免费视频| 亚洲二区视频在线| 欧美激情在线一区二区| 国产成人综合精品三级| 亚洲二区在线视频| 国产精品初高中害羞小美女文| 国产成人aaaa| 色94色欧美sute亚洲13| 亚洲欧美日韩国产手机在线| 99久久亚洲一区二区三区青草| 91久久线看在观草草青青| 亚洲欧美视频在线观看视频| 99久精品国产| 欧美精三区欧美精三区| 午夜精品久久久久久久99樱桃 | 91福利视频网站| 亚洲综合在线视频| 国产欧美精品区一区二区三区 | 久色婷婷小香蕉久久| 亚洲美女偷拍久久| 中文字幕精品在线不卡| 成人的网站免费观看| 欧美日韩在线播放三区四区| 亚洲国产你懂的| 中文字幕亚洲成人| 久久精品综合网| 成人三级伦理片| 日本黄色一区二区| 亚洲国产欧美日韩另类综合| 国产精品萝li| 国产午夜精品一区二区三区四区 | 精品一区二区三区在线观看国产| 亚洲综合成人网| 亚洲特级片在线| 亚洲国产精品国自产拍av| 久久无码av三级| 成人综合日日夜夜| 欧美久久久久久久久中文字幕| 免费观看91视频大全| 亚洲大片精品永久免费| 亚洲综合免费观看高清完整版| 国产精品久久久久影院色老大| 国产日韩精品一区| 久久综合久久鬼色中文字| 精品精品国产高清a毛片牛牛| 国产福利一区二区三区视频在线 | 亚洲成人精品一区二区| 亚洲欧美国产高清| 亚洲伦在线观看| 中文字幕一区二区三区蜜月| 国产精品嫩草影院av蜜臀| 久久色.com| 久久久国产精品麻豆| 91免费版在线看| 26uuu精品一区二区三区四区在线| 成人小视频免费在线观看| 7777精品久久久大香线蕉| 国产呦萝稀缺另类资源| 欧美日韩情趣电影| 国产一区欧美日韩| 欧美精选在线播放| 国产高清成人在线| 日韩一二在线观看| 成人少妇影院yyyy| 精品国产乱码久久久久久浪潮| gogo大胆日本视频一区| 精品福利一二区| 26uuu亚洲综合色| 国产欧美日本一区视频| 日本一区二区三区国色天香 | 午夜私人影院久久久久| 亚洲不卡在线观看| 人人狠狠综合久久亚洲| 欧美午夜精品久久久久久孕妇 | 亚洲三级免费电影| 一区二区三区精品| 亚洲v精品v日韩v欧美v专区| 日韩av中文在线观看| 欧美午夜在线观看| 国产成人啪午夜精品网站男同| 精品区一区二区| 国产午夜亚洲精品理论片色戒| 国产精品二区一区二区aⅴ污介绍| 亚洲欧美在线观看| 亚洲亚洲人成综合网络| 色吧成人激情小说| 国产曰批免费观看久久久| 日韩午夜在线影院| 久久综合色综合88| 中文字幕一区二区三区精华液| 亚洲九九爱视频| 人禽交欧美网站| 91精品国产色综合久久ai换脸 | 欧美激情一区二区三区在线| 亚洲日本青草视频在线怡红院| 亚洲激情图片一区| 青青草成人在线观看| 欧美日本视频在线| 99re8在线精品视频免费播放| 国产精品的网站| 亚洲国产精品久久人人爱蜜臀| 久久精品国产秦先生| 日韩欧美的一区二区| 国产网站一区二区三区| 亚洲在线免费播放| 欧美日韩在线综合| 91色综合久久久久婷婷| 亚洲欧美日韩国产中文在线| 色综合天天在线| 粉嫩嫩av羞羞动漫久久久| 国产精品视频看| 亚洲一二三区在线观看| 国产一区二区0| 欧美国产乱子伦| 亚洲国产精品一区二区www| 国产精品18久久久久久久久久久久| 欧美精品一区二区三区高清aⅴ| 国产精品久久毛片| 蜜臂av日日欢夜夜爽一区| 精品国产免费久久| 亚洲精品成人精品456| 激情深爱一区二区| 日本一区二区在线不卡| 亚洲福利一区二区| 成人免费视频视频在线观看免费| 中文字幕人成不卡一区| 91久久精品一区二区三区| 91视视频在线观看入口直接观看www| 亚洲精品伦理在线| 欧美美女直播网站| 国产精品三级视频| 久久国产夜色精品鲁鲁99| 久久久久久久综合| 亚洲不卡在线观看| 99精品视频中文字幕| 亚洲成人你懂的| 精品国产一区二区三区久久久蜜月| 亚洲欧美日韩中文播放| 国产成人综合在线| 亚洲黄色av一区| 欧美一二三四在线| 亚洲免费视频成人| 丁香六月综合激情| 亚洲一级电影视频| 精品久久久久99| 午夜国产不卡在线观看视频| aaa国产一区| 午夜精品久久久久久久蜜桃app| 欧美成人女星排名| 亚洲国产你懂的| 久久先锋影音av| 美国一区二区三区在线播放| 国产精品午夜电影| 欧美日韩精品一二三区| 综合久久综合久久| 成人中文字幕合集| 五月综合激情婷婷六月色窝| 国产无一区二区| 精品视频一区三区九区| 亚洲视频你懂的| 不卡视频免费播放| 日韩精品欧美成人高清一区二区| 国产性做久久久久久| 在线看国产一区| 亚洲日本在线看| www.日韩大片| 美女免费视频一区| 亚洲精品日产精品乱码不卡| 精品精品国产高清一毛片一天堂| 色一情一乱一乱一91av| 国产精品久久久久毛片软件|