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 {
_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,
),
),
);
}
}
当点击跳转到新的页面的时候,可以使用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;
_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 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;
_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 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。描述可能不太准确,请见谅。
学习记录,每天不停进步。