Flutter中如何检测缩放时InteractiveViewer内图片是否平移至边缘
实现方案
要实现图片放大时禁用翻页,但在放大状态下平移到最左/最右侧时重新启用对应方向翻页的功能,需要结合跟踪图片的缩放比例和平移偏移,并使用自定义滚动物理规则来控制PageView的滚动行为。以下是具体实现步骤和完整代码:
核心逻辑
- 跟踪图片状态:实时获取图片的缩放比例、平移偏移、缩放后的宽度及视口宽度。
- 判断边缘状态:根据图片的缩放和平移计算是否处于左边缘或右边缘。
- 自定义滚动物理:根据边缘状态允许或禁止PageView在对应方向的滚动,让InteractiveViewer优先处理图片平移,仅在到达边缘时触发翻页。
- 页面切换重置:切换页面时重置当前图片的缩放和平移状态。
完整代码
import 'dart:io'; import 'package:flutter/material.dart'; class PhotoViewer extends StatefulWidget { final List<String> photoPaths; final int startIndex; const PhotoViewer({ super.key, required this.photoPaths, this.startIndex = 0, }); @override State<PhotoViewer> createState() => _PhotoViewerState(); } class _PhotoViewerState extends State<PhotoViewer> { late PageController _pageController; int _selectedPageIndex = 0; late ValueNotifier<int> _currentPageNotifier; // 跟踪当前图片的状态 double _currentScale = 1.0; double _currentTranslationX = 0.0; double _currentScaledWidth = 0.0; double _currentViewportWidth = 0.0; @override void initState() { super.initState(); _selectedPageIndex = widget.startIndex; _pageController = PageController(initialPage: _selectedPageIndex); _currentPageNotifier = ValueNotifier(_selectedPageIndex); } @override void dispose() { _pageController.dispose(); _currentPageNotifier.dispose(); super.dispose(); } void _pageChanged(int index) { if (_selectedPageIndex != index) { setState(() { _selectedPageIndex = index; // 重置当前图片状态 _currentScale = 1.0; _currentTranslationX = 0.0; _currentScaledWidth = 0.0; }); _currentPageNotifier.value = index; } FocusScope.of(context).unfocus(); } void _onInteractionUpdate({ required double scale, required double translationX, required double scaledWidth, required double viewportWidth, }) { setState(() { _currentScale = scale; _currentTranslationX = translationX; _currentScaledWidth = scaledWidth; _currentViewportWidth = viewportWidth; }); } // 判断是否允许向左翻页 bool get _allowLeftScroll { if (_currentScale <= 1.0) return true; // 图片处于左边缘(无法继续向左平移) return _currentTranslationX >= 0; } // 判断是否允许向右翻页 bool get _allowRightScroll { if (_currentScale <= 1.0) return true; // 计算最大向右平移偏移(图片处于右边缘) final maxNegativeTranslation = -(_currentScaledWidth - _currentViewportWidth); return _currentTranslationX <= maxNegativeTranslation; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Trip Photos"), backgroundColor: Colors.black, foregroundColor: Colors.white, ), body: Container( color: Colors.black, child: LayoutBuilder( builder: (context, constraints) { _currentViewportWidth = constraints.maxWidth; return PageView( physics: ConditionalPageScrollPhysics( allowLeftScroll: _allowLeftScroll, allowRightScroll: _allowRightScroll, ), controller: _pageController, onPageChanged: _pageChanged, children: widget.photoPaths.asMap().entries.map((entry) { return _InteractiveImage( entry.value, index: entry.key, onInteractionUpdate: _onInteractionUpdate, ); }).toList(), ); }, ), ), ); } } class _InteractiveImage extends StatefulWidget { final String imgPath; final int index; final Function({ required double scale, required double translationX, required double scaledWidth, required double viewportWidth, }) onInteractionUpdate; const _InteractiveImage( this.imgPath, { required this.index, required this.onInteractionUpdate, }); @override State<_InteractiveImage> createState() => _InteractiveImageState(); } class _InteractiveImageState extends State<_InteractiveImage> { late TransformationController _transformationController; Size? _imageSize; @override void initState() { super.initState(); _transformationController = TransformationController(); // 加载图片原始尺寸 FileImage(File(widget.imgPath)).resolve(const ImageConfiguration()).addListener( ImageStreamListener((ImageInfo info, bool _) { setState(() { _imageSize = Size( info.image.width.toDouble(), info.image.height.toDouble(), ); }); }), ); // 监听页面切换,重置当前图片的变换状态 final parentState = context.findAncestorStateOfType<_PhotoViewerState>(); parentState?._currentPageNotifier.addListener(_resetTransformation); } @override void dispose() { final parentState = context.findAncestorStateOfType<_PhotoViewerState>(); parentState?._currentPageNotifier.removeListener(_resetTransformation); _transformationController.dispose(); super.dispose(); } void _resetTransformation() { final parentState = context.findAncestorStateOfType<_PhotoViewerState>(); // 非当前页面的图片重置为初始状态 if (parentState?._selectedPageIndex != widget.index) { _transformationController.value = Matrix4.identity(); } } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final viewportWidth = constraints.maxWidth; return InteractiveViewer( transformationController: _transformationController, onInteractionUpdate: (details) { if (_imageSize == null) return; // 获取当前缩放比例 final scale = _transformationController.value.getMaxScaleOnAxis(); // 获取当前X轴平移偏移 final translationX = _transformationController.value.column3.x; // 计算缩放后的图片宽度 final scaledWidth = _imageSize!.width * scale; // 通知父组件更新状态 widget.onInteractionUpdate( scale: scale, translationX: translationX, scaledWidth: scaledWidth, viewportWidth: viewportWidth, ); }, child: Image.file(File(widget.imgPath)), ); }, ); } } // 自定义滚动物理规则,控制PageView的滚动方向 class ConditionalPageScrollPhysics extends PageScrollPhysics { final bool allowLeftScroll; final bool allowRightScroll; const ConditionalPageScrollPhysics({ required this.allowLeftScroll, required this.allowRightScroll, super.parent, }); @override ConditionalPageScrollPhysics applyTo(ScrollPhysics? ancestor) { return ConditionalPageScrollPhysics( allowLeftScroll: allowLeftScroll, allowRightScroll: allowRightScroll, parent: buildParent(ancestor), ); } @override bool shouldAcceptUserOffset(ScrollMetrics position) { final direction = position.userScrollDirection; // 向左滑动(切换到上一页) if (direction == ScrollDirection.forward) { return allowLeftScroll; } // 向右滑动(切换到下一页) else if (direction == ScrollDirection.reverse) { return allowRightScroll; } return super.shouldAcceptUserOffset(position); } }
关键说明
- 自定义滚动物理类:
ConditionalPageScrollPhysics继承自PageScrollPhysics,通过shouldAcceptUserOffset方法根据当前状态决定是否接受用户的滚动手势。 - 图片状态跟踪:通过
InteractiveViewer的onInteractionUpdate回调实时获取图片的缩放和平移信息,并传递给父组件。 - 边缘判断逻辑:根据图片缩放后的宽度、视口宽度和平移偏移,计算图片是否处于左/右边缘。
- 页面切换重置:使用
ValueNotifier通知子组件在页面切换时重置图片的变换状态,确保新页面的图片处于初始状态。
内容的提问来源于stack exchange,提问作者ShrimpCrackers
相关产品推荐
相关产品推荐

