flutter开发实战-hero实现图片预览功能extend_image
在开发中,经常遇到需要图片预览,当feed中点击一个图片,开启预览,多个图片可以左右切换swiper,双击图片及手势进行缩放功能。
这个主要实现使用extend_image插件。在点击图片时候使用hero动画进行展示。
Hero简单使用,可以查看https://brucegwo.blog.csdn.net/article/details/134005601
hero实现图片预览功能效果图
一、图片GridView
在展示多张图片,使用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 { _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'];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,),),);}
}
二、跳转到Swiper的TransparentPageRoute
当点击跳转到新的页面的时候,可以使用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,);
}
三、使用extend_image
在pubspec.yaml引入extend_image
# extended_imageextended_image: ^7.0.2
当点击图片的时候,传入多张图片,定位到当前的index,多个图片可以左右切换Swiper。这里使用到了ExtendedImageGesturePageView。ExtendedImageGesturePageView与PageView类似,它是为显示缩放/平移图像而设计的。
如果您已经缓存了手势,请记住在正确的时间调用clearGestureDetailsCache()方法。(例如,页面视图页面被丢弃)
ExtendedImageGesturePageView属性
- cacheGesture 保存手势状态(例如在ExtendedImageGesturePageView中,向后滚动时手势状态不会改变),记住clearGestureDetailsCache
- inPageView 是否在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_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; _HeroWidgetState createState() => _HeroWidgetState();
}class _HeroWidgetState extends State<HeroWidget> {RectTween? _rectTween;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 outflightShuttleBuilder: (BuildContext flightContext,Animation<double> animation,HeroFlightDirection flightDirection,BuildContext fromHeroContext,BuildContext toHeroContext) {// make hero more smoothlyfinal 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 smoothlyanimatedBuilderChild = 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 outif (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,);}
}
五、使用Pic_Swiper
在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; _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;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;}void dispose() {rebuildIndex.close();rebuildSwiper.close();rebuildDetail.close();_doubleClickAnimationController.dispose();_slideEndAnimationController.dispose();clearGestureDetailsCache();//cancelToken?.cancel();super.dispose();}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;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 readyreturn 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。描述可能不太准确,请见谅。
学习记录,每天不停进步。