PhotoView和PhotoViewGallery-图片查看

photo_view是什么

photo_view 是 Flutter 生态中的一个第三方库,用于在移动应用中方便地实现可缩放、可拖拽的图片查看功能,常用于图片浏览器、相册等场景。

主要功能:

  • 手势缩放(双指捏合)

  • 双击放大/缩小

  • 平移(拖动图片)

  • 旋转(可选)

  • 支持 Hero 动画(实现图片查看时的平滑过渡效果)

  • 支持多图浏览(通过 PhotoViewGallery

photo_view的PhotoView

PhotoView是什么

PhotoView 是一个 单图查看组件。它支持 双指缩放、双击放大、拖拽平移、旋转(可选) 等操作。常用于放大预览单张图片。

PhotoView(
  imageProvider: NetworkImage('https://example.com/image.jpg'),
  minScale: PhotoViewComputedScale.contained,
  maxScale: PhotoViewComputedScale.covered * 2,
  enableRotation: true,
)

常用属性

属性名 作用 备注
imageProvider 必填,指定要显示的图片资源 NetworkImageAssetImageFileImage
loadingBuilder 图片加载时的占位内容 常用于显示 CircularProgressIndicator
errorBuilder 图片加载失败时显示的内容 比如“加载失败”图标
backgroundDecoration 背景装饰 例如设置纯色背景 BoxDecoration(color: Colors.black)
minScale / maxScale 最小/最大缩放比例 控制缩放范围
initialScale 初始缩放比例 一般用 PhotoViewComputedScale.contained
basePosition 图片初始对齐方式 默认 Alignment.center
enableRotation 是否允许旋转图片 默认 false
filterQuality 渲染质量 low / medium / high / none
heroAttributes 启用 动画 用于页面跳转间的共享元素动画
tightMode 是否紧贴父组件尺寸 一般用于和 Container 配合
scaleStateChangedCallback 缩放状态变化时的回调 可监听当前是缩放中、已缩放还是复位

PhotoViewComputedScale

PhotoViewComputedScale 是一个 辅助类(枚举常量),用于设置 minScale / maxScale / initialScale,表示图片缩放的基准值。

含义
PhotoViewComputedScale.contained 等比缩放,使整张图完整地包含在视口内
(不会裁剪,可能留空白)
PhotoViewComputedScale.covered 等比缩放,使视口完全被图片覆盖(可能裁剪边缘)
PhotoViewComputedScale.covered * n 基于 covered 再放大 n 倍(常用于 maxScale
PhotoViewComputedScale.contained * n 基于 contained 再放大 n

heroAttributes

heroAttributes 的作用是 为图片添加 Hero 动画效果,用于在两个页面之间 平滑地过渡同一张图片

当你给 PhotoView 添加 heroAttributes 时,这个图片会在页面跳转时执行:

动画元素 说明
位置动画 从旧页面的位置平滑移动到新页面中心位置
缩放动画 从原来的大小平滑放大到全屏(或目标尺寸)
裁剪/圆角动画 如果两边的圆角不同,会过渡圆角半径
透明度动画 部分系统主题下,会渐入渐出

视觉上就是:“点击网格中的一张图片 → 它从原位置‘飞’到全屏 → 返回时再飞回原位置”

案例

import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'PhotoView Demo', home: const ThumbnailPage());
  }
}

// 页面 A:缩略图
class ThumbnailPage extends StatelessWidget {
  const ThumbnailPage({super.key});

  final String imageUrl =
      'https://images.unsplash.com/photo-1506744038136-46273834b3fb';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('缩略图页面')),
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => FullScreenPhotoPage(imageUrl: imageUrl),
              ),
            );
          },
          child: Hero(
            tag: imageUrl, // tag 必须和 PhotoView 中一致
            child: ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: Image.network(
                imageUrl,
                width: 150,
                height: 150,
                fit: BoxFit.cover,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

// 页面 B:全屏预览
class FullScreenPhotoPage extends StatelessWidget {
  final String imageUrl;
  const FullScreenPhotoPage({super.key, required this.imageUrl});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('全屏预览页面')),
      body: PhotoView(
        imageProvider: NetworkImage(imageUrl),

        // 背景颜色
        backgroundDecoration: const BoxDecoration(color: Colors.black),

        // 初始缩放 & 缩放范围
        initialScale: PhotoViewComputedScale.contained,
        minScale: PhotoViewComputedScale.contained * 0.8,
        maxScale: PhotoViewComputedScale.covered * 2.5,

        // 启用旋转
        enableRotation: true,

        // 加载时
        loadingBuilder: (context, event) =>
            const Center(child: CircularProgressIndicator()),

        // hero 动画
        heroAttributes: PhotoViewHeroAttributes(tag: imageUrl),
      ),
    );
  }
}

photo_view的PhotoViewGallery

是什么

PhotoViewGallery 是一个 多图查看组件,基于 PageView,支持 左右滑动切换图片,每一页内部就是一个 PhotoView,因此支持相同的缩放、拖拽等功能。它常用于做 相册浏览

默认构造

默认构造:传 pageOptions 列表(适合少量固定图片)

PhotoViewGallery(
  pageOptions: [
    PhotoViewGalleryPageOptions(
      imageProvider: NetworkImage('https://example.com/img1.jpg'),
    ),
    PhotoViewGalleryPageOptions(
      imageProvider: NetworkImage('https://example.com/img2.jpg'),
    ),
  ],
  backgroundDecoration: const BoxDecoration(color: Colors.black),
)

.builder 构造

.builder 构造:传 itemCount + builder(懒加载的方式,适合动态或大量图片)

PhotoViewGallery.builder(
  itemCount: imageUrls.length,
  builder: (context, index) {
    return PhotoViewGalleryPageOptions(
      imageProvider: NetworkImage(imageUrls[index]),
    );
  },
  backgroundDecoration: const BoxDecoration(color: Colors.black),
)

常用属性

属性名 类型 & 说明
pageOptions List<PhotoViewGalleryPageOptions>必填,定义每一页要显示的内容,每一页基本等同于一个 PhotoView
builder PhotoViewGalleryBuilder:另一种构建方式(与 pageOptions 二选一),用 builder 动态生成页面内容
itemCount int:与 builder 一起使用,表示总共有几张图
backgroundDecoration BoxDecoration:背景装饰,例如 BoxDecoration(color: Colors.black)
scrollPhysics ScrollPhysics:控制翻页滚动的手感
pageController PageController:控制当前页、初始页等,可传入 PageController(initialPage: x)
onPageChanged ValueChanged<int>:页面切换时回调当前页索引,可用于显示页码等
loadingBuilder Widget Function(BuildContext, ImageChunkEvent?)?:图片加载时的占位内容
scaleStateChangedCallback ValueChanged<PhotoViewScaleState>?:缩放状态变化时的回调,用于监听缩放状态
wantKeepAlive bool:是否缓存已浏览页面,默认为 false

pageOptions

pageOptions 列表里的每个元素是一个 PhotoViewGalleryPageOptions,它支持的属性和 PhotoView 几乎完全一致,包括:imageProviderheroAttributesminScale / maxScaleinitialScalebasePositionenableRotationfilterQuality等等。

也就是说:PhotoViewGallery = 多个 PhotoView + 横向分页浏览器

scrollPhysics

  • 类型:ScrollPhysics
  • 作用:控制滚动或翻页的行为和手感
  • 简单来说,它决定了用户在滑动图片时 手指拖动的响应方式、阻尼、惯性、回弹效果 等。
ScrollPhysics 类型 作用说明
BouncingScrollPhysics 拖动到边界时会有回弹效果(iOS 风格)
ClampingScrollPhysics 到边界直接停止,不会回弹(Android 风格)
NeverScrollableScrollPhysics 禁止用户滑动,页面只能通过代码切换
AlwaysScrollableScrollPhysics 即使内容不足一页,也允许滑动
自定义组合 可以使用 BouncingScrollPhysics(parent: ClampingScrollPhysics()) 等组合

案例:PhotoViewGallery的基本使用

import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PhotoViewGallery + PageController',
      home: const ThumbnailPage(),
    );
  }
}

/// 缩略图页面
class ThumbnailPage extends StatelessWidget {
  const ThumbnailPage({super.key});

  // 图片列表
  final List<String> imageUrls = const [
    'https://images.unsplash.com/photo-1506744038136-46273834b3fb',
    'https://images.unsplash.com/photo-1507525428034-b723cf961d3e',
    'https://images.unsplash.com/photo-1491553895911-0055eca6402d',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('缩略图页面')),
      body: GridView.builder(
        padding: const EdgeInsets.all(8),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3, // 每行 3 张缩略图
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
        ),
        itemCount: imageUrls.length,
        itemBuilder: (context, index) {
          final url = imageUrls[index];
          return GestureDetector(
            onTap: () {
              // 点击后跳转到大图浏览页,并把点击的索引传过去
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => GalleryPage(
                    images: imageUrls,
                    initialIndex: index, // 这里传入初始页
                  ),
                ),
              );
            },
            child: Hero(
              tag: url, // Hero 动画的 tag 要和大图页对应
              child: Image.network(url, fit: BoxFit.cover),
            ),
          );
        },
      ),
    );
  }
}

/// 大图浏览页面
class GalleryPage extends StatefulWidget {
  final List<String> images;
  final int initialIndex;

  const GalleryPage({super.key, required this.images, this.initialIndex = 0});

  @override
  State<GalleryPage> createState() => _GalleryPageState();
}

class _GalleryPageState extends State<GalleryPage> {
  late final PageController _pageController;
  int _currentIndex = 0;

  @override
  void initState() {
    super.initState();

    // 初始化 PageController,并指定初始页为点击的那张图
    _pageController = PageController(initialPage: widget.initialIndex);
    _currentIndex = widget.initialIndex;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 顶部显示当前页码
      appBar: AppBar(
        title: Text('第 ${_currentIndex + 1} / ${widget.images.length} 张'),
      ),
      body: PhotoViewGallery(
        pageOptions: widget.images.map((url) {
          return PhotoViewGalleryPageOptions(
            imageProvider: NetworkImage(url),

            // 缩放配置
            minScale: PhotoViewComputedScale.contained * 0.8,
            maxScale: PhotoViewComputedScale.covered * 2.5,

            // hero 动画
            heroAttributes: PhotoViewHeroAttributes(tag: url),
          );
        }).toList(),

        // 指定 PageController,用于控制当前页
        pageController: _pageController,

        // 页面切换时更新当前索引
        onPageChanged: (index) {
          setState(() => _currentIndex = index);
        },

        // 背景颜色
        backgroundDecoration: const BoxDecoration(color: Colors.black),

        // 图片加载中的占位
        loadingBuilder: (context, event) =>
            const Center(child: CircularProgressIndicator()),
      ),
    );
  }
}

pageController

pageController 接受一个 PageController 对象,用来控制和监听当前显示的图片页,它和 PhotoViewGallery 内部的分页系统关联。

换句话说:PhotoViewGallery 是基于 PageView 实现的,而 pageController 就是 PageView 的控制器。

pageController 就像遥控器,能让你控制当前页的位置、初始页、跳转动画,并且和外部状态联动。

常用方法与属性

方法 / 属性名 类型 / 说明
jumpToPage(int page) 立即无动画跳转到指定页。
animateToPage(int page, {Duration duration, Curve curve}) 使用动画方式跳转到指定页,可以指定动画时长和曲线。
nextPage({Duration duration, Curve curve}) 动画滑动到下一页。
previousPage({Duration duration, Curve curve}) 动画滑动到上一页。
page double?,获取当前精确页码(可能是小数,表示滑动中的位置)。
initialPage int,构造时设置的初始页码。
jumpTo(double offset) 跳到指定的滚动偏移位置(不常用,一般用 jumpToPage)。
animateTo(double offset, {Duration duration, Curve curve}) 动画滚动到指定偏移位置(不常用)。
viewportFraction double,构造时指定页面占视口的比例(默认 1.0)。
dispose() 释放控制器资源,通常在 State.dispose() 中调用。

案例: pageController的使用

import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart'; // 引入 photo_view 库

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: GalleryPage());
  }
}

class GalleryPage extends StatefulWidget {
  const GalleryPage({super.key});

  @override
  State<GalleryPage> createState() => _GalleryPageState();
}

class _GalleryPageState extends State<GalleryPage> {
  // 初始化 PageController,初始页面为第 0 页
  final PageController _pageController = PageController(initialPage: 0);

  // 当前页索引
  int _currentIndex = 0;

  // 模拟的网络图片列表
  final List<String> imageUrls = [
    'https://picsum.photos/id/1015/800/600',
    'https://picsum.photos/id/1016/800/600',
    'https://picsum.photos/id/1018/800/600',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('当前第 $_currentIndex 页'),
        actions: [
          // 点击按钮跳转到下一页
          IconButton(
            icon: const Icon(Icons.arrow_forward),
            onPressed: () {
              if (_currentIndex < imageUrls.length - 1) {
                _pageController.nextPage(
                  // 使用 Curves.easeInOut 进行平滑过渡
                  // duration: 设置动画持续时间
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeInOut,
                );
              }
            },
          ),
        ],
      ),
      body: PhotoViewGallery.builder(
        // 💡 绑定 PageController 控制翻页
        pageController: _pageController,

        // 图片数量
        itemCount: imageUrls.length,

        // 构建每一页
        builder: (context, index) {
          return PhotoViewGalleryPageOptions(
            imageProvider: NetworkImage(imageUrls[index]),
            // 上面没有用的tag,所以这里的tag没有用处。
            heroAttributes: PhotoViewHeroAttributes(tag: 'photo_$index'),
          );
        },

        // 当页面切换时回调
        onPageChanged: (index) {
          setState(() {
            _currentIndex = index;
          });
        },

        // 缩放相关
        backgroundDecoration: const BoxDecoration(color: Colors.black),
        scrollPhysics: const BouncingScrollPhysics(),
      ),

      // 底部添加跳转按钮
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          // 💡 点击底部导航时使用 PageController 跳转到指定页
          _pageController.animateToPage(
            index,
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeInOut,
          );
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.photo), label: '第一页'),
          BottomNavigationBarItem(icon: Icon(Icons.photo), label: '第二页'),
          BottomNavigationBarItem(icon: Icon(Icons.photo), label: '第三页'),
        ],
      ),
    );
  }
}

×

喜欢就点赞,疼爱就打赏