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 children = const [],

...

})

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 {

List images = [

'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 animation,

Animation secondaryAnimation) {

return PicSwiper(

index: index,

pics: images,

);

}));

},

);

},

itemCount: images.length,

),

),

);

}

}

二、跳转到Swiper的TransparentPageRoute

当点击跳转到新的页面的时候,可以使用TransparentPageRoute,该类继承与PageRouteBuilder,实现FadeTransition在点击图片展示预览图片的时候,通过渐隐渐显的方式跳转到下一个路由。

Widget _defaultTransitionsBuilder(

BuildContext context,

Animation animation,

Animation secondaryAnimation,

Widget child,

) {

return FadeTransition(

opacity: CurvedAnimation(

parent: animation,

curve: Curves.easeOut,

),

child: child,

);

}

完整代码如下

import 'package:flutter/material.dart';

/// Transparent Page Route

class TransparentPageRoute extends PageRouteBuilder {

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 animation,

Animation secondaryAnimation,

Widget child,

) {

return FadeTransition(

opacity: CurvedAnimation(

parent: animation,

curve: Curves.easeOut,

),

child: child,

);

}

三、使用extend_image

在pubspec.yaml引入extend_image

# extended_image

extended_image: ^7.0.2

当点击图片的时候,传入多张图片,定位到当前的index,多个图片可以左右切换Swiper。这里使用到了ExtendedImageGesturePageView。ExtendedImageGesturePageView与PageView类似,它是为显示缩放/平移图像而设计的。 如果您已经缓存了手势,请记住在正确的时间调用clearGestureDetailsCache()方法。(例如,页面视图页面被丢弃)

ExtendedImageGesturePageView属性

cacheGesture 保存手势状态(例如在ExtendedImageGesturePageView中,向后滚动时手势状态不会改变),记住clearGestureDetailsCacheinPageView 是否在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 slidePagekey;

@override

_HeroWidgetState createState() => _HeroWidgetState();

}

class _HeroWidgetState extends State {

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 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: [

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 offsetTween = Tween(

begin: Offset.zero,

end: widget.slidePagekey.currentState!.offset);

final Tween scaleTween = Tween(

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? pics;

@override

_PicSwiperState createState() => _PicSwiperState();

}

class _PicSwiperState extends State with TickerProviderStateMixin {

final StreamController rebuildIndex = StreamController.broadcast();

final StreamController rebuildSwiper =

StreamController.broadcast();

final StreamController rebuildDetail =

StreamController.broadcast();

late AnimationController _doubleClickAnimationController;

late AnimationController _slideEndAnimationController;

late Animation _slideEndAnimation;

Animation? _doubleClickAnimation;

late DoubleClickAnimationListener _doubleClickAnimationListener;

List doubleTapScales = [1.0, 2.0];

GlobalKey slidePagekey =

GlobalKey();

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: [

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(begin: begin, end: end));

_doubleClickAnimation!

.addListener(_doubleClickAnimationListener);

_doubleClickAnimationController.forward();

},

loadStateChanged: (ExtendedImageState state) {

if (state.extendedImageLoadState == LoadState.completed) {

return StreamBuilder(

builder:

(BuildContext context, AsyncSnapshot data) {

return ExtendedImageGesture(

state,

imageBuilder: (Widget image) {

return Stack(

children: [

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(

builder: (BuildContext c, AsyncSnapshot 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? pics;

final int? index;

final StreamController reBuild;

@override

Widget build(BuildContext context) {

return StreamBuilder(

builder: (BuildContext context, AsyncSnapshot 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: [

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> key = GlobalKey();

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。描述可能不太准确,请见谅。

学习记录,每天不停进步。

参考阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: