page_flip_view 0.5.2
page_flip_view: ^0.5.2 copied to clipboard
Turn pages like a real book. A diagonal corner curl that follows the finger, for any widgets and for PDF files (rendered with pdfrx).
import 'package:flutter/material.dart';
import 'package:page_flip_view/pdf.dart';
void main() => runApp(const DemoApp());
const samplePdfUrl =
'https://raw.githubusercontent.com/mozilla/pdf.js/master/web/compressed.tracemonkey-pldi-09.pdf';
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.brown, useMaterial3: true),
home: DefaultTabController(
length: 3,
child: Scaffold(
backgroundColor: const Color(0xFF323232),
appBar: AppBar(
toolbarHeight: 0,
bottom: const TabBar(
tabs: [
Tab(text: 'PDF asset'),
Tab(text: 'PDF network'),
Tab(text: 'Widgets'),
],
),
),
body: const SafeArea(
top: false,
child: TabBarView(
physics: NeverScrollableScrollPhysics(),
children: [AssetPdfDemo(), NetworkPdfDemo(), WidgetsDemo()],
),
),
),
),
);
}
}
/// A bundled PDF with page buttons and a reading mode switch.
class AssetPdfDemo extends StatefulWidget {
const AssetPdfDemo({super.key});
@override
State<AssetPdfDemo> createState() => _AssetPdfDemoState();
}
class _AssetPdfDemoState extends State<AssetPdfDemo> {
final controller = PageFlipController();
int page = 0;
int total = 0;
ReadingMode mode = ReadingMode.normal;
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DemoFrame(
controller: controller,
label: 'Page ${page + 1} of $total',
trailing: TextButton.icon(
style: TextButton.styleFrom(foregroundColor: Colors.white),
onPressed: () => setState(() => mode = mode.next),
icon: const Icon(Icons.contrast),
label: Text(mode.label),
),
child: PdfFlipBook.asset(
'assets/sample.pdf',
controller: controller,
onLoaded: (count) => setState(() => total = count),
onPageChanged: (value) => setState(() => page = value),
colorFilter: mode.filter,
paperColor: mode.paper,
),
);
}
}
/// Each reading mode pairs a colour filter with the paper colour that
/// matches it, so the back of a turning page has the right colour.
enum ReadingMode {
normal('Normal', null, Colors.white),
dim('Dim', PdfFlipBook.dim, PdfFlipBook.dimPaper),
sepia('Sepia', PdfFlipBook.sepia, PdfFlipBook.sepiaPaper),
night('Night', PdfFlipBook.nightMode, PdfFlipBook.nightPaper);
const ReadingMode(this.label, this.filter, this.paper);
final String label;
final ColorFilter? filter;
final Color paper;
ReadingMode get next => values[(index + 1) % values.length];
}
/// A PDF downloaded from a URL, with a download progress indicator.
class NetworkPdfDemo extends StatefulWidget {
const NetworkPdfDemo({super.key});
@override
State<NetworkPdfDemo> createState() => _NetworkPdfDemoState();
}
class _NetworkPdfDemoState extends State<NetworkPdfDemo> {
final controller = PageFlipController();
int page = 0;
int total = 0;
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DemoFrame(
controller: controller,
label: total == 0 ? 'Downloading…' : 'Page ${page + 1} of $total',
child: PdfFlipBook.network(
Uri.parse(samplePdfUrl),
controller: controller,
onLoaded: (count) => setState(() => total = count),
onPageChanged: (value) => setState(() => page = value),
loadingBuilder: (context, progress) => Center(
child: SizedBox(
width: 200,
child: LinearProgressIndicator(value: progress),
),
),
errorBuilder: (context, error, retry) => Center(
child: FilledButton.icon(
onPressed: retry,
icon: const Icon(Icons.refresh),
label: const Text('Could not download. Retry'),
),
),
),
);
}
}
/// Plain widgets as pages: anything works, not only PDFs.
class WidgetsDemo extends StatelessWidget {
const WidgetsDemo({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: PageFlipView(
itemCount: 8,
itemBuilder: (context, index) => ColoredBox(
color: Colors.primaries[index % Colors.primaries.length].shade100,
child: Center(
child: Text(
'${index + 1}',
style: Theme.of(context).textTheme.displayLarge,
),
),
),
),
);
}
}
/// The book with previous / next buttons and a page label under it.
class DemoFrame extends StatelessWidget {
const DemoFrame({
super.key,
required this.controller,
required this.label,
required this.child,
this.trailing,
});
final PageFlipController controller;
final String label;
final Widget child;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Padding(padding: const EdgeInsets.all(16), child: child),
),
Row(
children: [
IconButton(
color: Colors.white,
onPressed: controller.previous,
icon: const Icon(Icons.chevron_left),
),
Expanded(
child: Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
),
),
if (trailing != null) trailing!,
IconButton(
color: Colors.white,
onPressed: controller.next,
icon: const Icon(Icons.chevron_right),
),
],
),
],
);
}
}