Flutter for OpenHarmony三方库适配实战:pdf_render PDF渲染与查看
PDF文档查看是移动应用的常见需求,用于电子书阅读、文档预览、报表查看等场景。在 Flutter for OpenHarmony 应用开发中,pdf_render是一个功能强大的 PDF 渲染插件,提供了完整的跨平台 PDF 查看能力。@overrideappBar: AppBar(title: const Text('水平滚动 PDF')),assetPath,},),),
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
本文基于flutter3.27.5开发
一、pdf_render 库概述
PDF文档查看是移动应用的常见需求,用于电子书阅读、文档预览、报表查看等场景。在 Flutter for OpenHarmony 应用开发中,pdf_render 是一个功能强大的 PDF 渲染插件,提供了完整的跨平台 PDF 查看能力。
pdf_render 库特点
pdf_render 库基于 Flutter 平台接口实现,提供了以下核心特性:
多源加载:支持从文件、Asset、二进制数据、网络等多种来源加载 PDF 文档。
高性能渲染:使用 Texture 机制渲染 PDF 页面,避免数据拷贝,提供流畅的滚动和缩放体验。
完整控制:提供 PdfViewerController 控制器,支持页面跳转、缩放、滚动等操作。
丰富组件:提供 PdfViewer、PdfPageView 等开箱即用的 Widget 组件。
低级 API:提供 PdfDocument、PdfPage 等低级 API,支持自定义渲染逻辑。
页面信息:支持获取页面尺寸、页数、PDF 版本等文档信息。
功能支持对比
| 功能 | Android | iOS | OpenHarmony |
|---|---|---|---|
| 文件加载 | ✅ | ✅ | ✅ |
| Asset加载 | ✅ | ✅ | ✅ |
| 二进制加载 | ✅ | ✅ | ✅ |
| 页面渲染 | ✅ | ✅ | ✅ |
| Texture渲染 | ✅ | ✅ | ✅ |
| 页面跳转 | ✅ | ✅ | ✅ |
| 缩放控制 | ✅ | ✅ | ✅ |
| 页面信息 | ✅ | ✅ | ✅ |
使用场景:电子书阅读器、文档查看器、报表预览、合同签署、说明书展示等。
二、安装与配置
2.1 添加依赖
在项目的 pubspec.yaml 文件中添加 pdf_render_ohos 依赖:
dependencies:
pdf_render_ohos:
git:
url: https://atomgit.com/OpenHarmony-SIG/fluttertpc_flutter_pdf_render.git
path: ohos
然后执行以下命令获取依赖:
flutter pub get
2.2 Asset 配置
如果需要从 Asset 加载 PDF 文件,需要在 pubspec.yaml 中配置:
flutter:
assets:
- assets/hello.pdf
将 PDF 文件放置在项目的 assets 目录下。
三、核心 API 详解
3.1 PdfDocument 类
PdfDocument 是 PDF 文档的核心类,提供文档加载和信息获取功能。
abstract class PdfDocument {
final String sourceName; // 文件路径/asset名/memory
final int pageCount; // 页数
final int verMajor; // PDF 主版本
final int verMinor; // PDF 次版本
final bool isEncrypted; // 是否加密
final bool allowsCopying; // 是否允许复制
final bool allowsPrinting; // 是否允许打印
static Future<PdfDocument> openFile(String filePath);
static Future<PdfDocument> openAsset(String name);
static Future<PdfDocument> openData(Uint8List data);
Future<PdfPage> getPage(int pageNumber);
Future<void> dispose();
}
3.2 openFile 方法
openFile 方法用于从文件路径加载 PDF 文档。
static Future<PdfDocument> openFile(String filePath)
参数说明:
filePath 参数是 PDF 文件的绝对路径。
使用示例:
final doc = await PdfDocument.openFile('/data/storage/el2/base/files/sample.pdf');
print('页数: ${doc.pageCount}');
3.3 openAsset 方法
openAsset 方法用于从 Asset 加载 PDF 文档。
static Future<PdfDocument> openAsset(String name)
参数说明:
name 参数是 Asset 名称,需要在 pubspec.yaml 中配置。
使用示例:
final doc = await PdfDocument.openAsset('assets/hello.pdf');
print('页数: ${doc.pageCount}');
3.4 openData 方法
openData 方法用于从二进制数据加载 PDF 文档。
static Future<PdfDocument> openData(Uint8List data)
参数说明:
data 参数是 PDF 文件的二进制数据。
使用示例:
final bytes = await File('sample.pdf').readAsBytes();
final doc = await PdfDocument.openData(bytes);
print('页数: ${doc.pageCount}');
3.5 getPage 方法
getPage 方法用于获取指定页面的 PdfPage 对象。
Future<PdfPage> getPage(int pageNumber)
参数说明:
pageNumber 参数是页码,从 1 开始(第一页是 1,不是 0)。
使用示例:
final page = await doc.getPage(1);
print('页面尺寸: ${page.width} x ${page.height}');
3.6 dispose 方法
dispose 方法用于释放 PDF 文档资源。
Future<void> dispose()
使用示例:
final doc = await PdfDocument.openAsset('assets/hello.pdf');
try {
// 使用文档...
} finally {
doc.dispose();
}
3.7 PdfPage 类
PdfPage 是 PDF 页面的核心类,提供页面渲染功能。
abstract class PdfPage {
final PdfDocument document; // 所属文档
final int pageNumber; // 页码(从1开始)
final double width; // 页面宽度(点)
final double height; // 页面高度(点)
Future<PdfPageImage> render({
int x = 0,
int y = 0,
int? width,
int? height,
double? fullWidth,
double? fullHeight,
bool backgroundFill = true,
bool allowAntialiasingIOS = false,
});
}
3.8 render 方法
render 方法用于渲染 PDF 页面为图像。
Future<PdfPageImage> render({
int x = 0,
int y = 0,
int? width,
int? height,
double? fullWidth,
double? fullHeight,
bool backgroundFill = true,
bool allowAntialiasingIOS = false,
})
参数说明:
x、y 参数指定渲染区域的起始坐标。
width、height 参数指定渲染区域的像素大小。
fullWidth、fullHeight 参数指定虚拟完整页面大小,用于缩放计算。
backgroundFill 参数指定是否填充白色背景。
使用示例:
final page = await doc.getPage(1);
final image = await page.render(
width: (page.width * 2).toInt(),
height: (page.height * 2).toInt(),
);
print('图像尺寸: ${image.width}x${image.height}');
print('像素数据: ${image.pixels.lengthInBytes} bytes');
3.9 PdfPageImage 类
PdfPageImage 是渲染后的图像对象。
abstract class PdfPageImage {
final int pageNumber; // 页码
final int x; // 渲染区域左上角X
final int y; // 渲染区域左上角Y
final int width; // 渲染宽度(像素)
final int height; // 渲染高度(像素)
final double fullWidth; // 完整页面宽度
final double fullHeight; // 完整页面高度
final double pageWidth; // 页面宽度(点)
final double pageHeight; // 页面高度(点)
Uint8List get pixels; // RGBA 像素数据
Future<ui.Image> createImageIfNotAvailable();
void dispose();
}
3.10 PdfPageImageTexture 类
PdfPageImageTexture 提供 Texture 渲染功能,性能更高。
abstract class PdfPageImageTexture {
final PdfDocument pdfDocument;
final int pageNumber;
final int texId;
static Future<PdfPageImageTexture> create({
required FutureOr<PdfDocument> pdfDocument,
required int pageNumber,
});
Future<void> dispose();
Future<bool> extractSubrect({
int x = 0,
int y = 0,
required int width,
required int height,
double? fullWidth,
double? fullHeight,
bool backgroundFill = true,
bool allowAntialiasingIOS = true,
});
}
四、Widget 组件详解
4.1 PdfViewer 组件
PdfViewer 是完整的 PDF 查看器组件,支持滚动、缩放、页面跳转等功能。
class PdfViewer extends StatefulWidget {
static Widget openAsset(String assetName, {
PdfViewerController? viewerController,
void Function(dynamic)? onError,
PdfViewerParams? params,
});
static Widget openFile(String filePath, {
PdfViewerController? viewerController,
void Function(dynamic)? onError,
PdfViewerParams? params,
});
static Widget openFutureFile(Future<String> Function() filePathFactory, {
PdfViewerController? viewerController,
void Function(dynamic)? onError,
PdfViewerParams? params,
});
}
4.2 PdfViewerController 类
PdfViewerController 是 PDF 查看器的控制器,提供各种控制方法。
class PdfViewerController extends TransformationController {
bool get isReady; // 是否就绪
int get pageCount; // 总页数
int get currentPageNumber; // 当前页码
double get zoomRatio; // 缩放比例
Size get viewSize; // 视图大小
Size get fullSize; // 文档完整大小
PdfViewerController? get ready; // 安全访问
Future<void> goToPage({required int pageNumber});
Future<void> goTo({Matrix4? destination, Duration duration});
void setZoomRatio({required double zoomRatio, Offset? center});
Rect? getPageRect(int pageNumber);
Matrix4? calculatePageFitMatrix({required int pageNumber, double? padding});
}
4.3 goToPage 方法
goToPage 方法用于跳转到指定页面。
Future<void> goToPage({required int pageNumber})
参数说明:
pageNumber 参数是目标页码,从 1 开始。
使用示例:
controller.ready?.goToPage(pageNumber: 1); // 跳转到第一页
controller.ready?.goToPage(pageNumber: controller.pageCount); // 跳转到最后一页
4.4 setZoomRatio 方法
setZoomRatio 方法用于设置缩放比例。
void setZoomRatio({required double zoomRatio, Offset? center})
参数说明:
zoomRatio 参数是目标缩放比例。
center 参数是缩放中心点(可选)。
使用示例:
controller.ready?.setZoomRatio(
zoomRatio: controller.zoomRatio * 1.5,
center: tapPosition,
);
4.5 PdfViewerParams 类
PdfViewerParams 是 PDF 查看器的参数配置类。
class PdfViewerParams {
final EdgeInsets padding; // 页面间距
final double minScale; // 最小缩放比例
final double maxScale; // 最大缩放比例
final Axis scrollDirection; // 滚动方向
final PdfPageBuilder? pageBuilder; // 页面构建器
final LayoutPagesFunc? layoutPages; // 页面布局函数
}
4.6 PdfDocumentLoader 组件
PdfDocumentLoader 用于加载 PDF 文档并管理 PdfDocument 实例。
class PdfDocumentLoader extends StatefulWidget {
factory PdfDocumentLoader.openFile(String filePath, {
PdfDocumentBuilder? documentBuilder,
int? pageNumber,
PdfPageBuilder? pageBuilder,
Function(dynamic)? onError,
});
factory PdfDocumentLoader.openAsset(String assetName, {
PdfDocumentBuilder? documentBuilder,
int? pageNumber,
PdfPageBuilder? pageBuilder,
Function(dynamic)? onError,
});
factory PdfDocumentLoader.openData(Uint8List data, {
PdfDocumentBuilder? documentBuilder,
int? pageNumber,
PdfPageBuilder? pageBuilder,
Function(dynamic)? onError,
});
}
4.7 PdfPageView 组件
PdfPageView 用于渲染单个 PDF 页面。
class PdfPageView extends StatefulWidget {
final PdfDocument? pdfDocument;
final int? pageNumber;
final PdfPageBuilder? pageBuilder;
}
五、OpenHarmony 平台实现原理
5.1 原生 API 映射
pdf_render 在 OpenHarmony 平台上使用 @kit.PDFKit 和 @kit.ImageKit 模块实现:
| Flutter API | OpenHarmony API |
|---|---|
| openFile | pdfService.PdfDocument.loadDocument |
| openAsset | resourceManager.getRawFileContentSync + loadDocument |
| openData | fs.writeSync + loadDocument |
| getPage | PdfDocument.getPage |
| render | PdfPage.getPagePixelMap |
| allocTex | TextureRegistry.getTextureId |
| updateTex | PlatformViewController.render |
5.2 文档加载实现
OpenHarmony 使用 pdfService.PdfDocument 加载 PDF 文档:
openFileDoc(filePath: string): pdfService.PdfDocument {
let pdfDocument = new pdfService.PdfDocument();
pdfDocument.loadDocument(filePath, "");
return pdfDocument;
}
5.3 Asset 加载实现
Asset 文件需要先读取为二进制数据,再写入临时文件:
openAssetDoc(pdfAssetName: string): pdfService.PdfDocument {
let context = getContext() as common.UIAbilityContext;
let newAsset = "flutter_assets/" + pdfAssetName;
let content = context.resourceManager.getRawFileContentSync(newAsset);
return this.openDataDoc(content);
}
5.4 二进制数据加载实现
二进制数据需要写入临时文件后加载:
openDataDoc(data: Uint8Array): pdfService.PdfDocument {
let context = getContext() as common.UIAbilityContext;
let dir = context.filesDir;
let filePath = dir + "/input.pdf";
let pdfDocument = new pdfService.PdfDocument();
let fdSand = fs.openSync(filePath,
fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
fs.writeSync(fdSand.fd, data.buffer);
fs.closeSync(fdSand.fd);
pdfDocument.loadDocument(filePath, "");
fs.unlink(filePath); // 删除临时文件
return pdfDocument;
}
5.5 页面渲染实现
OpenHarmony 使用 PdfPage.getPagePixelMap 获取页面像素图:
let pdfPage: pdfService.PdfPage = document.getPage(pageNumber - 1);
let pixelMap: image.PixelMap = pdfPage.getPagePixelMap();
5.6 Texture 渲染实现
Texture 渲染使用 PlatformView 机制:
updateTex(call: MethodCall): number {
let textureId: number = call.argument("texId");
let surfaceTexture = this.textures.get(textureId);
let pixelMap: image.PixelMap = pdfPage.getPagePixelMap();
let platformView = this.platformViews.get(textureId);
platformView!.setPixelMap(pixelMap);
this.platformViewController?.render(
surfaceTexture?.getSurfaceId(),
platformView!,
width,
height,
left,
top
);
return 0;
}
5.7 PlatformView 实现
FlutterPdfRenderView 继承 PlatformView,用于显示 PDF 页面:
export class FlutterPdfRenderView extends PlatformView {
private pixelMap: image.PixelMap | undefined = undefined;
private width: number = 0;
private height: number = 0;
getView(): WrappedBuilder<[Params]> {
return new WrappedBuilder(PdfBuilder);
}
getPixelMap(): image.PixelMap | undefined {
return this.pixelMap;
}
setPixelMap(pixelMap: image.PixelMap | undefined): void {
this.pixelMap = pixelMap;
}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
}
5.8 UI 组件实现
PdfView 使用 ArkUI 的 Image 组件显示 PixelMap:
@Entry
@Component
struct PDFPreview {
@Prop params: Params;
private pixelMap: image.PixelMap | undefined = undefined;
build() {
Column() {
Image(this.pixelMap)
}
}
}
@Builder
export function PdfBuilder(params: Params) {
PDFPreview({
params: params,
pixelMap: (params.platformView as FlutterPdfRenderView).getPixelMap()
});
}
六、MethodChannel 通信协议
6.1 方法列表
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
file |
filePath | docId, pageCount, … | 打开文件 |
asset |
assetName | docId, pageCount, … | 打开 Asset |
data |
Uint8Array | docId, pageCount, … | 打开二进制数据 |
close |
docId | 0 | 关闭文档 |
info |
docId | docId, pageCount, … | 获取文档信息 |
page |
docId, pageNumber | width, height | 获取页面信息 |
allocTex |
- | textureId | 分配 Texture |
releaseTex |
texId | 0 | 释放 Texture |
updateTex |
texId, docId, pageNumber, width, height | 0 | 更新 Texture |
6.2 文档信息返回格式
{
"docId": 1,
"pageCount": 10,
"verMajor": 1,
"verMinor": 7,
"isEncrypted": false,
"allowsCopying": false,
"allowsPrinting": false,
}
6.3 页面信息返回格式
{
"docId": 1,
"pageNumber": 1,
"width": 595.0,
"height": 842.0,
}
七、实战案例
7.1 基础 PDF 查看器
import 'package:flutter/material.dart';
import 'package:pdf_render_ohos/pdf_render.dart';
import 'package:pdf_render_ohos/pdf_render_widgets.dart';
class BasicPdfViewer extends StatelessWidget {
final String assetPath;
const BasicPdfViewer({super.key, required this.assetPath});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PDF 查看器')),
body: PdfViewer.openAsset(
assetPath,
onError: (err) => print('PDF 加载错误: $err'),
params: const PdfViewerParams(
padding: 10,
minScale: 1.0,
),
),
);
}
}
7.2 带控制器的 PDF 查看器
class ControlledPdfViewer extends StatefulWidget {
final String assetPath;
const ControlledPdfViewer({super.key, required this.assetPath});
State<ControlledPdfViewer> createState() => _ControlledPdfViewerState();
}
class _ControlledPdfViewerState extends State<ControlledPdfViewer> {
final controller = PdfViewerController();
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ValueListenableBuilder<Matrix4>(
valueListenable: controller,
builder: (context, _, child) => Text(
controller.isReady
? '第 ${controller.currentPageNumber} 页 / 共 ${controller.pageCount} 页'
: 'PDF 查看器',
),
),
),
body: PdfViewer.openAsset(
widget.assetPath,
viewerController: controller,
onError: (err) => print('PDF 加载错误: $err'),
params: const PdfViewerParams(
padding: 10,
minScale: 1.0,
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'first',
child: const Icon(Icons.first_page),
onPressed: () => controller.ready?.goToPage(pageNumber: 1),
),
const SizedBox(height: 10),
FloatingActionButton(
heroTag: 'last',
child: const Icon(Icons.last_page),
onPressed: () => controller.ready?.goToPage(
pageNumber: controller.pageCount,
),
),
],
),
);
}
}
7.3 双击缩放功能
class ZoomablePdfViewer extends StatefulWidget {
final String assetPath;
const ZoomablePdfViewer({super.key, required this.assetPath});
State<ZoomablePdfViewer> createState() => _ZoomablePdfViewerState();
}
class _ZoomablePdfViewerState extends State<ZoomablePdfViewer> {
final controller = PdfViewerController();
TapDownDetails? _doubleTapDetails;
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('双击缩放 PDF')),
body: GestureDetector(
onDoubleTapDown: (details) => _doubleTapDetails = details,
onDoubleTap: () {
if (controller.isReady) {
final newZoom = controller.zoomRatio < 2.0
? controller.zoomRatio * 1.5
: 1.0;
controller.setZoomRatio(
zoomRatio: newZoom,
center: _doubleTapDetails!.localPosition,
);
}
},
child: PdfViewer.openAsset(
widget.assetPath,
viewerController: controller,
params: const PdfViewerParams(
padding: 10,
minScale: 1.0,
maxScale: 3.0,
),
),
),
);
}
}
7.4 从网络加载 PDF
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
class NetworkPdfViewer extends StatefulWidget {
final String url;
const NetworkPdfViewer({super.key, required this.url});
State<NetworkPdfViewer> createState() => _NetworkPdfViewerState();
}
class _NetworkPdfViewerState extends State<NetworkPdfViewer> {
final controller = PdfViewerController();
bool _isLoading = true;
String? _error;
void initState() {
super.initState();
_loadPdf();
}
Future<void> _loadPdf() async {
try {
final file = await DefaultCacheManager().getSingleFile(widget.url);
setState(() {
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_error = e.toString();
});
}
}
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('网络 PDF')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text('加载失败: $_error'))
: PdfViewer.openFutureFile(
() async {
final file = await DefaultCacheManager()
.getSingleFile(widget.url);
return file.path;
},
viewerController: controller,
onError: (err) => print('加载错误: $err'),
params: const PdfViewerParams(
padding: 10,
minScale: 1.0,
),
),
);
}
}
7.5 低级 API 渲染
Future<void> renderPdfPage(String assetPath) async {
final doc = await PdfDocument.openAsset(assetPath);
try {
print('PDF 版本: ${doc.verMajor}.${doc.verMinor}');
print('总页数: ${doc.pageCount}');
print('是否加密: ${doc.isEncrypted}');
for (int i = 1; i <= doc.pageCount; i++) {
final page = await doc.getPage(i);
print('第 $i 页: ${page.width} x ${page.height} 点');
final image = await page.render(
width: (page.width * 2).toInt(),
height: (page.height * 2).toInt(),
);
print('渲染尺寸: ${image.width}x${image.height}');
print('像素数据: ${image.pixels.lengthInBytes} bytes');
}
} finally {
doc.dispose();
}
}
7.6 页面缩略图列表
class PdfThumbnailList extends StatefulWidget {
final String assetPath;
const PdfThumbnailList({super.key, required this.assetPath});
State<PdfThumbnailList> createState() => _PdfThumbnailListState();
}
class _PdfThumbnailListState extends State<PdfThumbnailList> {
PdfDocument? _doc;
final Map<int, Uint8List> _thumbnails = {};
String? _error;
void initState() {
super.initState();
_loadPdf();
}
Future<void> _loadPdf() async {
try {
final doc = await PdfDocument.openAsset(widget.assetPath);
setState(() => _doc = doc);
for (int i = 1; i <= doc.pageCount; i++) {
final page = await doc.getPage(i);
final image = await page.render(
width: 100,
height: (100 * page.height / page.width).toInt(),
);
if (mounted) {
setState(() {
_thumbnails[i] = image.pixels;
});
}
}
} catch (e) {
setState(() => _error = e.toString());
}
}
void dispose() {
_doc?.dispose();
super.dispose();
}
Widget build(BuildContext context) {
if (_error != null) {
return Center(child: Text('加载失败: $_error'));
}
if (_doc == null) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: _doc!.pageCount,
itemBuilder: (context, index) {
final pageNum = index + 1;
return ListTile(
leading: _thumbnails[pageNum] != null
? Image.memory(
_thumbnails[pageNum]!,
width: 50,
height: 70,
fit: BoxFit.cover,
)
: const SizedBox(
width: 50,
height: 70,
child: Center(child: CircularProgressIndicator()),
),
title: Text('第 $pageNum 页'),
subtitle: Text('点击查看'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ControlledPdfViewer(
assetPath: widget.assetPath,
),
),
);
},
);
},
);
}
}
7.7 自定义页面布局
class CustomLayoutPdfViewer extends StatelessWidget {
final String assetPath;
const CustomLayoutPdfViewer({super.key, required this.assetPath});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('水平滚动 PDF')),
body: PdfViewer.openAsset(
assetPath,
params: PdfViewerParams(
padding: const EdgeInsets.all(16),
minScale: 0.5,
maxScale: 4.0,
scrollDirection: Axis.horizontal,
layoutPages: (Size contentViewSize, List<Size> pageSizes) {
final List<Rect> rects = [];
double x = 0;
for (final size in pageSizes) {
rects.add(Rect.fromLTWH(x, 0, size.width, size.height));
x += size.width + 20;
}
return rects;
},
),
),
);
}
}
八、最佳实践
8.1 资源释放
PDF 文档对象需要手动释放,建议使用 try-finally:
Future<void> processPdf(String path) async {
final doc = await PdfDocument.openAsset(path);
try {
// 使用文档...
final page = await doc.getPage(1);
// 处理页面...
} finally {
doc.dispose();
}
}
8.2 页码管理
PDF 页码从 1 开始,注意边界检查:
Future<PdfPage?> safeGetPage(PdfDocument doc, int pageNumber) async {
if (pageNumber < 1 || pageNumber > doc.pageCount) {
return null;
}
return await doc.getPage(pageNumber);
}
8.3 错误处理
Future<void> loadPdfWithErrorHandling(String path) async {
try {
final doc = await PdfDocument.openAsset(path);
// 处理文档...
doc.dispose();
} catch (e) {
print('PDF 加载失败: $e');
}
}
8.4 缩放比例管理
class ZoomManager {
static const List<double> zoomLevels = [0.5, 0.75, 1.0, 1.5, 2.0, 3.0];
static double getNextZoom(double current) {
final index = zoomLevels.indexOf(current);
if (index < 0 || index >= zoomLevels.length - 1) {
return zoomLevels.first;
}
return zoomLevels[index + 1];
}
static double getPreviousZoom(double current) {
final index = zoomLevels.indexOf(current);
if (index <= 0) {
return zoomLevels.last;
}
return zoomLevels[index - 1];
}
}
8.5 页面缓存
class PdfPageCache {
final PdfDocument document;
final Map<int, PdfPage> _cache = {};
PdfPageCache(this.document);
Future<PdfPage> getPage(int pageNumber) async {
if (_cache.containsKey(pageNumber)) {
return _cache[pageNumber]!;
}
final page = await document.getPage(pageNumber);
_cache[pageNumber] = page;
return page;
}
void clear() {
_cache.clear();
}
}
九、常见问题
Q1:PDF 文件无法加载怎么办?
检查以下几点:
-
确保 Asset 已正确配置
flutter: assets: - assets/hello.pdf -
确保文件路径正确
// Asset 路径不需要加 assets/ 前缀 PdfDocument.openAsset('assets/hello.pdf'); // 正确 PdfDocument.openAsset('hello.pdf'); // 错误 -
确保 PDF 文件格式正确,不是损坏的文件
Q2:页面渲染模糊怎么办?
提高渲染分辨率:
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
final image = await page.render(
width: (page.width * pixelRatio).toInt(),
height: (page.height * pixelRatio).toInt(),
);
Q3:如何获取当前显示的页面?
使用 PdfViewerController:
ValueListenableBuilder<Matrix4>(
valueListenable: controller,
builder: (context, _, child) {
if (controller.isReady) {
return Text('当前页: ${controller.currentPageNumber}');
}
return Text('加载中...');
},
)
Q4:如何实现页面跳转动画?
Future<void> animateToPage(int pageNumber) async {
final rect = controller.getPageRect(pageNumber);
if (rect != null) {
final matrix = controller.calculatePageFitMatrix(
pageNumber: pageNumber,
padding: 10,
);
await controller.goTo(
destination: matrix,
duration: const Duration(milliseconds: 300),
);
}
}
Q5:如何处理加密的 PDF?
final doc = await PdfDocument.openAsset('encrypted.pdf');
if (doc.isEncrypted) {
print('PDF 已加密,部分功能可能受限');
}
Q6:内存占用过高怎么办?
- 及时释放不需要的 PdfDocument
- 使用 Texture 渲染而非直接渲染
- 避免同时加载多个大文档
// 及时释放
doc.dispose();
// 使用 Texture
PdfViewer.openAsset('large.pdf'); // 内部使用 Texture
十、总结
pdf_render 库为 Flutter for OpenHarmony 开发提供了完整的 PDF 渲染能力。通过丰富的 API,开发者可以实现文档加载、页面渲染、缩放控制、页面跳转等功能。该库在鸿蒙平台上已经完成了完整的适配,支持所有核心功能,开发者可以放心使用。
核心特性:
- 多源加载:支持文件、Asset、二进制数据、网络等多种来源
- 高性能渲染:使用 Texture 机制,避免数据拷贝
- 丰富组件:提供 PdfViewer、PdfPageView 等开箱即用的组件
- 灵活控制:PdfViewerController 支持缩放、跳转等操作
- 低级 API:支持自定义渲染逻辑
注意事项:
- 页码从 1 开始,不是 0
- 文档对象需要手动释放
- 页面尺寸单位是"点",需要根据设备像素密度计算实际像素
十一、完整代码示例
以下是一个完整的可运行示例,展示了 pdf_render 库的核心功能:
main.dart
import 'package:flutter/material.dart';
import 'package:pdf_render_ohos/pdf_render.dart';
import 'package:pdf_render_ohos/pdf_render_widgets.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'PDF Render Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final controller = PdfViewerController();
TapDownDetails? _doubleTapDetails;
String _statusMessage = '';
void dispose() {
controller.dispose();
super.dispose();
}
void _showMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
void _goToFirstPage() {
if (controller.isReady) {
controller.goToPage(pageNumber: 1);
_showMessage('已跳转到第一页');
}
}
void _goToLastPage() {
if (controller.isReady) {
controller.goToPage(pageNumber: controller.pageCount);
_showMessage('已跳转到最后一页');
}
}
void _goToPreviousPage() {
if (controller.isReady && controller.currentPageNumber > 1) {
controller.goToPage(pageNumber: controller.currentPageNumber - 1);
}
}
void _goToNextPage() {
if (controller.isReady && controller.currentPageNumber < controller.pageCount) {
controller.goToPage(pageNumber: controller.currentPageNumber + 1);
}
}
void _zoomIn() {
if (controller.isReady) {
final newZoom = (controller.zoomRatio * 1.5).clamp(0.5, 5.0);
controller.setZoomRatio(zoomRatio: newZoom);
_showMessage('缩放: ${newZoom.toStringAsFixed(1)}x');
}
}
void _zoomOut() {
if (controller.isReady) {
final newZoom = (controller.zoomRatio / 1.5).clamp(0.5, 5.0);
controller.setZoomRatio(zoomRatio: newZoom);
_showMessage('缩放: ${newZoom.toStringAsFixed(1)}x');
}
}
void _resetZoom() {
if (controller.isReady) {
controller.setZoomRatio(zoomRatio: 1.0);
_showMessage('已重置缩放');
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ValueListenableBuilder<Matrix4>(
valueListenable: controller,
builder: (context, _, child) {
if (controller.isReady) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'第 ${controller.currentPageNumber} 页 / 共 ${controller.pageCount} 页',
style: const TextStyle(fontSize: 16),
),
Text(
'缩放: ${controller.zoomRatio.toStringAsFixed(1)}x',
style: const TextStyle(fontSize: 12),
),
],
);
}
return const Text('PDF 查看器');
},
),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: const Icon(Icons.zoom_out),
onPressed: _zoomOut,
tooltip: '缩小',
),
IconButton(
icon: const Icon(Icons.zoom_in),
onPressed: _zoomIn,
tooltip: '放大',
),
PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'reset':
_resetZoom();
break;
case 'first':
_goToFirstPage();
break;
case 'last':
_goToLastPage();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'reset',
child: Text('重置缩放'),
),
const PopupMenuItem(
value: 'first',
child: Text('跳转到第一页'),
),
const PopupMenuItem(
value: 'last',
child: Text('跳转到最后一页'),
),
],
),
],
),
body: GestureDetector(
onDoubleTapDown: (details) => _doubleTapDetails = details,
onDoubleTap: () {
if (controller.isReady) {
final newZoom = controller.zoomRatio < 2.0
? controller.zoomRatio * 1.5
: 1.0;
controller.setZoomRatio(
zoomRatio: newZoom,
center: _doubleTapDetails!.localPosition,
);
}
},
child: Stack(
children: [
PdfViewer.openAsset(
'assets/hello.pdf',
viewerController: controller,
onError: (err) {
setState(() {
_statusMessage = 'PDF 加载错误: $err';
});
},
params: const PdfViewerParams(
padding: 10,
minScale: 0.5,
maxScale: 5.0,
),
),
if (_statusMessage.isNotEmpty)
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
color: Colors.red.shade100,
padding: const EdgeInsets.all(8),
child: Text(
_statusMessage,
style: const TextStyle(color: Colors.red),
),
),
),
ValueListenableBuilder<Matrix4>(
valueListenable: controller,
builder: (context, _, child) {
if (!controller.isReady) return const SizedBox();
final v = controller.viewRect;
final all = controller.fullSize;
final top = v.top / all.height * v.height;
final height = v.height / all.height * v.height;
return Positioned(
right: 0,
top: top,
height: height.clamp(20.0, double.infinity),
width: 8,
child: Container(
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
),
);
},
),
],
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'prev',
mini: true,
child: const Icon(Icons.keyboard_arrow_up),
onPressed: _goToPreviousPage,
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'next',
mini: true,
child: const Icon(Icons.keyboard_arrow_down),
onPressed: _goToNextPage,
),
const SizedBox(height: 16),
FloatingActionButton(
heroTag: 'first',
child: const Icon(Icons.first_page),
onPressed: _goToFirstPage,
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'last',
child: const Icon(Icons.last_page),
onPressed: _goToLastPage,
),
],
),
bottomNavigationBar: ValueListenableBuilder<Matrix4>(
valueListenable: controller,
builder: (context, _, child) {
if (!controller.isReady) {
return const SizedBox();
}
return Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text('页码: ${controller.currentPageNumber}/${controller.pageCount}'),
Text('缩放: ${controller.zoomRatio.toStringAsFixed(1)}x'),
Text(
'尺寸: ${controller.viewSize.width.toStringAsFixed(0)} x ${controller.viewSize.height.toStringAsFixed(0)}',
),
],
),
);
},
),
);
}
}
运行此示例后,您将看到一个完整的 PDF 查看器演示界面,包含页面导航、缩放控制、双击缩放、滚动位置指示器等功能。可以通过浮动按钮和菜单进行页面跳转和缩放操作。
更多推荐
所有评论(0)