赞
踩
flutter开发实战-hero实现图片预览功能extend_image
在开发中,经常遇到需要图片预览,当feed中点击一个图片,开启预览,多个图片可以左右切换swiper,双击图片及手势进行缩放功能。
这个主要实现使用extend_image插件。在点击图片时候使用hero动画进行展示。
Hero简单使用,可以查看https://brucegwo.blog.csdn.net/article/details/134005601
hero实现图片预览功能效果图
在展示多张图片,使用GridView来展示。
GridView可以构建一个二维网格列表,其默认构造函数定义如下:
GridView({ Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, required this.gridDelegate, //下面解释 bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, double? cacheExtent, List<Widget> children = const <Widget>[], ... })
SliverGridDelegate是一个抽象类,定义了GridView Layout相关接口,子类需要通过实现它们来实现具体的布局算法。
实现展示图片GridView
GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemBuilder: (BuildContext context, int index) {
...
完整代码如下
class GridSimplePhotoViewDemo extends StatefulWidget { @override _GridSimplePhotoViewDemoState createState() => _GridSimplePhotoViewDemoState(); } class _GridSimplePhotoViewDemoState extends State<GridSimplePhotoViewDemo> { List<String> images = <String>[ 'https://photo.tuchong.com/14649482/f/601672690.jpg', 'https://photo.tuchong.com/17325605/f/641585173.jpg', 'https://photo.tuchong.com/3541468/f/256561232.jpg', 'https://photo.tuchong.com/16709139/f/278778447.jpg', 'This is an video', 'https://photo.tuchong.com/5040418/f/43305517.jpg', 'https://photo.tuchong.com/3019649/f/302699092.jpg' ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('SimplePhotoView'), ), body: Padding( padding: const EdgeInsets.all(10.0), child: GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 300, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemBuilder: (BuildContext context, int index) { final String url = images[index]; return GestureDetector( child: AspectRatio( aspectRatio: 1.0, child: Hero( tag: url, child: url == 'This is an video' ? Container( alignment: Alignment.center, child: const Text('This is an video'), ) : ExtendedImage.network( url, fit: BoxFit.cover, ), ), ), onTap: () { Navigator.of(context).push(TransparentPageRoute(pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return PicSwiper( index: index, pics: images, ); })); }, ); }, itemCount: images.length, ), ), ); } }
当点击跳转到新的页面的时候,可以使用TransparentPageRoute,该类继承与PageRouteBuilder,实现FadeTransition在点击图片展示预览图片的时候,通过渐隐渐显的方式跳转到下一个路由。
Widget _defaultTransitionsBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
),
child: child,
);
}
完整代码如下
import 'package:flutter/material.dart'; /// Transparent Page Route class TransparentPageRoute<T> extends PageRouteBuilder<T> { TransparentPageRoute({ RouteSettings? settings, required RoutePageBuilder pageBuilder, RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder, Duration transitionDuration = const Duration(milliseconds: 250), bool barrierDismissible = false, Color? barrierColor, String? barrierLabel, bool maintainState = true, }) : super( settings: settings, opaque: false, pageBuilder: pageBuilder, transitionsBuilder: transitionsBuilder, transitionDuration: transitionDuration, barrierDismissible: barrierDismissible, barrierColor: barrierColor, barrierLabel: barrierLabel, maintainState: maintainState, ); } Widget _defaultTransitionsBuilder( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return FadeTransition( opacity: CurvedAnimation( parent: animation, curve: Curves.easeOut, ), child: child, ); }
在pubspec.yaml引入extend_image
# extended_image
extended_image: ^7.0.2
当点击图片的时候,传入多张图片,定位到当前的index,多个图片可以左右切换Swiper。这里使用到了ExtendedImageGesturePageView。ExtendedImageGesturePageView与PageView类似,它是为显示缩放/平移图像而设计的。
如果您已经缓存了手势,请记住在正确的时间调用clearGestureDetailsCache()方法。(例如,页面视图页面被丢弃)
ExtendedImageGesturePageView属性
使用示例
ExtendedImageGesturePageView.builder( itemBuilder: (BuildContext context, int index) { var item = widget.pics[index].picUrl; Widget image = ExtendedImage.network( item, fit: BoxFit.contain, mode: ExtendedImageMode.gesture, gestureConfig: GestureConfig( inPageView: true, initialScale: 1.0, //you can cache gesture state even though page view page change. //remember call clearGestureDetailsCache() method at the right time.(for example,this page dispose) cacheGesture: false ), ); image = Container( child: image, padding: EdgeInsets.all(5.0), ); if (index == currentIndex) { return Hero( tag: item + index.toString(), child: image, ); } else { return image; } }, itemCount: widget.pics.length, onPageChanged: (int index) { currentIndex = index; rebuild.add(index); }, controller: PageController( initialPage: currentIndex, ), scrollDirection: Axis.horizontal, )
当点击图片,实现hero_widget实现hero动画来实现图片预览。
使用Flutter的Hero widget创建hero动画。 将hero从一个路由飞到另一个路由。 将hero 的形状从圆形转换为矩形,同时将其从一个路由飞到另一个路由的过程中进行动画处理。
这里使用的hero_widget完整代码如下
import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; /// make hero better when slide out class HeroWidget extends StatefulWidget { const HeroWidget({ required this.child, required this.tag, required this.slidePagekey, this.slideType = SlideType.onlyImage, }); final Widget child; final SlideType slideType; final Object tag; final GlobalKey<ExtendedImageSlidePageState> slidePagekey; @override _HeroWidgetState createState() => _HeroWidgetState(); } class _HeroWidgetState extends State<HeroWidget> { RectTween? _rectTween; @override Widget build(BuildContext context) { return Hero( tag: widget.tag, createRectTween: (Rect? begin, Rect? end) { _rectTween = RectTween(begin: begin, end: end); return _rectTween!; }, // make hero better when slide out flightShuttleBuilder: (BuildContext flightContext, Animation<double> animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext) { // make hero more smoothly final Hero hero = (flightDirection == HeroFlightDirection.pop ? fromHeroContext.widget : toHeroContext.widget) as Hero; if (_rectTween == null) { return hero; } if (flightDirection == HeroFlightDirection.pop) { final bool fixTransform = widget.slideType == SlideType.onlyImage && (widget.slidePagekey.currentState!.offset != Offset.zero || widget.slidePagekey.currentState!.scale != 1.0); final Widget toHeroWidget = (toHeroContext.widget as Hero).child; return AnimatedBuilder( animation: animation, builder: (BuildContext buildContext, Widget? child) { Widget animatedBuilderChild = hero.child; // make hero more smoothly animatedBuilderChild = Stack( clipBehavior: Clip.antiAlias, alignment: Alignment.center, children: <Widget>[ Opacity( opacity: 1 - animation.value, child: UnconstrainedBox( child: SizedBox( width: _rectTween!.begin!.width, height: _rectTween!.begin!.height, child: toHeroWidget, ), ), ), Opacity( opacity: animation.value, child: animatedBuilderChild, ) ], ); // fix transform when slide out if (fixTransform) { final Tween<Offset> offsetTween = Tween<Offset>( begin: Offset.zero, end: widget.slidePagekey.currentState!.offset); final Tween<double> scaleTween = Tween<double>( begin: 1.0, end: widget.slidePagekey.currentState!.scale); animatedBuilderChild = Transform.translate( offset: offsetTween.evaluate(animation), child: Transform.scale( scale: scaleTween.evaluate(animation), child: animatedBuilderChild, ), ); } return animatedBuilderChild; }, ); } return hero.child; }, child: widget.child, ); } }
在swiper左右切换功能,使用ExtendedImageGesturePageView来实现切换功能,双击图片及手势进行缩放功能。
完整代码如下
typedef DoubleClickAnimationListener = void Function(); class PicSwiper extends StatefulWidget { const PicSwiper({ super.key, this.index, this.pics, }); final int? index; final List<String>? pics; @override _PicSwiperState createState() => _PicSwiperState(); } class _PicSwiperState extends State<PicSwiper> with TickerProviderStateMixin { final StreamController<int> rebuildIndex = StreamController<int>.broadcast(); final StreamController<bool> rebuildSwiper = StreamController<bool>.broadcast(); final StreamController<double> rebuildDetail = StreamController<double>.broadcast(); late AnimationController _doubleClickAnimationController; late AnimationController _slideEndAnimationController; late Animation<double> _slideEndAnimation; Animation<double>? _doubleClickAnimation; late DoubleClickAnimationListener _doubleClickAnimationListener; List<double> doubleTapScales = <double>[1.0, 2.0]; GlobalKey<ExtendedImageSlidePageState> slidePagekey = GlobalKey<ExtendedImageSlidePageState>(); int? _currentIndex = 0; bool _showSwiper = true; double _imageDetailY = 0; Rect? imageDRect; @override Widget build(BuildContext context) { final Size size = MediaQuery.of(context).size; double statusBarHeight = MediaQuery.of(context).padding.top; imageDRect = Offset.zero & size; Widget result = Material( color: Colors.transparent, shadowColor: Colors.transparent, child: Stack( fit: StackFit.expand, children: <Widget>[ ExtendedImageGesturePageView.builder( controller: ExtendedPageController( initialPage: widget.index!, pageSpacing: 50, shouldIgnorePointerWhenScrolling: false, ), scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), canScrollPage: (GestureDetails? gestureDetails) { return _imageDetailY >= 0; }, itemBuilder: (BuildContext context, int index) { final String item = widget.pics![index]; Widget image = ExtendedImage.network( item, fit: BoxFit.contain, enableSlideOutPage: true, mode: ExtendedImageMode.gesture, imageCacheName: 'CropImage', //layoutInsets: EdgeInsets.all(20), initGestureConfigHandler: (ExtendedImageState state) { double? initialScale = 1.0; if (state.extendedImageInfo != null) { initialScale = initScale( size: size, initialScale: initialScale, imageSize: Size( state.extendedImageInfo!.image.width.toDouble(), state.extendedImageInfo!.image.height.toDouble())); } return GestureConfig( inPageView: true, initialScale: initialScale!, maxScale: max(initialScale, 5.0), animationMaxScale: max(initialScale, 5.0), initialAlignment: InitialAlignment.center, //you can cache gesture state even though page view page change. //remember call clearGestureDetailsCache() method at the right time.(for example,this page dispose) cacheGesture: false, ); }, onDoubleTap: (ExtendedImageGestureState state) { ///you can use define pointerDownPosition as you can, ///default value is double tap pointer down postion. final Offset? pointerDownPosition = state.pointerDownPosition; final double? begin = state.gestureDetails!.totalScale; double end; //remove old _doubleClickAnimation ?.removeListener(_doubleClickAnimationListener); //stop pre _doubleClickAnimationController.stop(); //reset to use _doubleClickAnimationController.reset(); if (begin == doubleTapScales[0]) { end = doubleTapScales[1]; } else { end = doubleTapScales[0]; } _doubleClickAnimationListener = () { //print(_animation.value); state.handleDoubleTap( scale: _doubleClickAnimation!.value, doubleTapPosition: pointerDownPosition); }; _doubleClickAnimation = _doubleClickAnimationController .drive(Tween<double>(begin: begin, end: end)); _doubleClickAnimation! .addListener(_doubleClickAnimationListener); _doubleClickAnimationController.forward(); }, loadStateChanged: (ExtendedImageState state) { if (state.extendedImageLoadState == LoadState.completed) { return StreamBuilder<double>( builder: (BuildContext context, AsyncSnapshot<double> data) { return ExtendedImageGesture( state, imageBuilder: (Widget image) { return Stack( children: <Widget>[ Positioned.fill( child: image, ), ], ); }, ); }, initialData: _imageDetailY, stream: rebuildDetail.stream, ); } return null; }, ); image = HeroWidget( tag: item, slideType: SlideType.onlyImage, slidePagekey: slidePagekey, child: image, ); image = GestureDetector( child: image, onTap: () { slidePagekey.currentState!.popPage(); Navigator.pop(context); }, ); return image; }, itemCount: widget.pics!.length, onPageChanged: (int index) { _currentIndex = index; rebuildIndex.add(index); if (_imageDetailY != 0) { _imageDetailY = 0; rebuildDetail.sink.add(_imageDetailY); } _showSwiper = true; rebuildSwiper.add(_showSwiper); }, ), StreamBuilder<bool>( builder: (BuildContext c, AsyncSnapshot<bool> d) { if (d.data == null || !d.data!) { return Container(); } return Positioned( top: statusBarHeight, left: 0.0, right: 0.0, child: MySwiperPlugin(widget.pics, _currentIndex, rebuildIndex), ); }, initialData: true, stream: rebuildSwiper.stream, ) ], ), ); result = ExtendedImageSlidePage( key: slidePagekey, child: result, slideAxis: SlideAxis.vertical, slideType: SlideType.onlyImage, slideScaleHandler: ( Offset offset, { ExtendedImageSlidePageState? state, }) { return null; }, slideOffsetHandler: ( Offset offset, { ExtendedImageSlidePageState? state, }) { return null; }, slideEndHandler: ( Offset offset, { ExtendedImageSlidePageState? state, ScaleEndDetails? details, }) { return null; }, onSlidingPage: (ExtendedImageSlidePageState state) { ///you can change other widgets' state on page as you want ///base on offset/isSliding etc //var offset= state.offset; final bool showSwiper = !state.isSliding; if (showSwiper != _showSwiper) { // do not setState directly here, the image state will change, // you should only notify the widgets which are needed to change // setState(() { // _showSwiper = showSwiper; // }); _showSwiper = showSwiper; rebuildSwiper.add(_showSwiper); } }, ); return result; } @override void dispose() { rebuildIndex.close(); rebuildSwiper.close(); rebuildDetail.close(); _doubleClickAnimationController.dispose(); _slideEndAnimationController.dispose(); clearGestureDetailsCache(); //cancelToken?.cancel(); super.dispose(); } @override void initState() { super.initState(); _currentIndex = widget.index; _doubleClickAnimationController = AnimationController( duration: const Duration(milliseconds: 150), vsync: this); _slideEndAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 150), ); _slideEndAnimationController.addListener(() { _imageDetailY = _slideEndAnimation.value; if (_imageDetailY == 0) { _showSwiper = true; rebuildSwiper.add(_showSwiper); } rebuildDetail.sink.add(_imageDetailY); }); } } class MySwiperPlugin extends StatelessWidget { const MySwiperPlugin(this.pics, this.index, this.reBuild); final List<String>? pics; final int? index; final StreamController<int> reBuild; @override Widget build(BuildContext context) { return StreamBuilder<int>( builder: (BuildContext context, AsyncSnapshot<int> data) { return DefaultTextStyle( style: const TextStyle(color: Colors.blue), child: Container( height: 50.0, width: double.infinity, // color: Colors.grey.withOpacity(0.2), child: Row( children: <Widget>[ Container( width: 10.0, ), Text( '${data.data! + 1}', ), Text( ' / ${pics!.length}', ), const SizedBox( width: 10.0, ), const SizedBox( width: 10.0, ), if (!kIsWeb) GestureDetector( child: Container( padding: const EdgeInsets.only(right: 10.0), alignment: Alignment.center, child: const Text( 'Save', style: TextStyle(fontSize: 16.0, color: Colors.blue), ), ), onTap: () { // saveNetworkImageToPhoto(pics![index!].picUrl) // .then((bool done) { // showToast(done ? 'save succeed' : 'save failed', // position: const ToastPosition( // align: Alignment.topCenter)); // }); }, ), ], ), ), ); }, initialData: index, stream: reBuild.stream, ); } } class ImageDetailInfo { ImageDetailInfo({ required this.imageDRect, required this.pageSize, required this.imageInfo, }); final GlobalKey<State<StatefulWidget>> key = GlobalKey<State>(); final Rect imageDRect; final Size pageSize; final ImageInfo imageInfo; double? _maxImageDetailY; double get imageBottom => imageDRect.bottom - 20; double get maxImageDetailY { try { // return _maxImageDetailY ??= max( key.currentContext!.size!.height - (pageSize.height - imageBottom), 0.1); } catch (e) { //currentContext is not ready return 100.0; } } }
使用过程中的util
import 'package:extended_image/extended_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; /// /// create by zmtzawqlp on 2020/1/31 /// double? initScale({ required Size imageSize, required Size size, double? initialScale, }) { final double n1 = imageSize.height / imageSize.width; final double n2 = size.height / size.width; if (n1 > n2) { final FittedSizes fittedSizes = applyBoxFit(BoxFit.contain, imageSize, size); //final Size sourceSize = fittedSizes.source; final Size destinationSize = fittedSizes.destination; return size.width / destinationSize.width; } else if (n1 / n2 < 1 / 4) { final FittedSizes fittedSizes = applyBoxFit(BoxFit.contain, imageSize, size); //final Size sourceSize = fittedSizes.source; final Size destinationSize = fittedSizes.destination; return size.height / destinationSize.height; } return initialScale; }
效果视频
flutter开发实战-hero实现图片预览功能extend_image。描述可能不太准确,请见谅。
学习记录,每天不停进步。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。