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 |
必填,指定要显示的图片资源 | 如 NetworkImage、AssetImage、FileImage 等 |
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 几乎完全一致,包括:imageProvider,heroAttributes,minScale / maxScale,initialScale,basePosition,enableRotation,filterQuality等等。
也就是说: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: '第三页'),
],
),
);
}
}