You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何检测缩放时InteractiveViewer内图片是否平移至边缘

实现方案

要实现图片放大时禁用翻页,但在放大状态下平移到最左/最右侧时重新启用对应方向翻页的功能,需要结合跟踪图片的缩放比例和平移偏移,并使用自定义滚动物理规则来控制PageView的滚动行为。以下是具体实现步骤和完整代码:

核心逻辑

  1. 跟踪图片状态:实时获取图片的缩放比例、平移偏移、缩放后的宽度及视口宽度。
  2. 判断边缘状态:根据图片的缩放和平移计算是否处于左边缘或右边缘。
  3. 自定义滚动物理:根据边缘状态允许或禁止PageView在对应方向的滚动,让InteractiveViewer优先处理图片平移,仅在到达边缘时触发翻页。
  4. 页面切换重置:切换页面时重置当前图片的缩放和平移状态。

完整代码

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);
  }
}

关键说明

  1. 自定义滚动物理类:ConditionalPageScrollPhysics 继承自PageScrollPhysics,通过shouldAcceptUserOffset方法根据当前状态决定是否接受用户的滚动手势。
  2. 图片状态跟踪:通过InteractiveViewer的onInteractionUpdate回调实时获取图片的缩放和平移信息,并传递给父组件。
  3. 边缘判断逻辑:根据图片缩放后的宽度、视口宽度和平移偏移,计算图片是否处于左/右边缘。
  4. 页面切换重置:使用ValueNotifier通知子组件在页面切换时重置图片的变换状态,确保新页面的图片处于初始状态。

内容的提问来源于stack exchange,提问作者ShrimpCrackers

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 08:52:00