flutter_long_screenshot 1.0.1
flutter_long_screenshot: ^1.0.1 copied to clipboard
A Flutter plugin for capturing long screenshots of widgets without auto-scrolling, with support for PDF conversion and sharing.
example/lib/main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_long_screenshot/flutter_long_screenshot.dart';
import 'package:flutter/foundation.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Long Screenshot Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const LongScreenshotDemo(),
);
}
}
class LongScreenshotDemo extends StatefulWidget {
const LongScreenshotDemo({super.key});
@override
State<LongScreenshotDemo> createState() => _LongScreenshotDemoState();
}
class _LongScreenshotDemoState extends State<LongScreenshotDemo> {
final GlobalKey _screenshotKey = GlobalKey();
double _quality = 1.0;
bool _isCapturing = false;
Future<void> _showSaveOptions() async {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
),
const SizedBox(height: 24),
Text('Save Screenshot', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Image Quality', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Slider(
value: _quality,
min: 0.1,
max: 1.0,
divisions: 9,
label: '${(_quality * 100).round()}%',
onChanged: (value) {
setState(() {
_quality = value;
});
},
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(16)),
child: Text(
'${(_quality * 100).round()}%',
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer, fontWeight: FontWeight.bold),
),
),
],
),
],
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
_buildOptionButton(
context,
icon: Icons.image,
label: 'Save as Image',
onTap: () {
Navigator.pop(context);
_captureAndSaveToDownloads();
},
),
const SizedBox(height: 12),
_buildOptionButton(
context,
icon: Icons.share,
label: 'Share as Image',
onTap: () {
Navigator.pop(context);
_captureAndShareImage();
},
),
const SizedBox(height: 12),
_buildOptionButton(
context,
icon: Icons.picture_as_pdf,
label: 'Save as PDF',
onTap: () {
Navigator.pop(context);
_captureAndSaveAsPdf();
},
),
const SizedBox(height: 12),
_buildOptionButton(
context,
icon: Icons.share,
label: 'Share as PDF',
onTap: () {
Navigator.pop(context);
_captureAndSharePdf();
},
),
],
),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget _buildOptionButton(BuildContext context, {required IconData icon, required String label, required VoidCallback onTap}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 16),
Text(label, style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
Icon(Icons.arrow_forward_ios, size: 16, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5)),
],
),
),
),
);
}
Future<void> _captureAndSharePdf() async {
_setCapturing(true);
try {
// Capture the screenshot
final Uint8List imageData = await FlutterLongScreenshot.captureLongScreenshot(key: _screenshotKey, pixelRatio: 3.0, quality: _quality);
// Convert to PDF and share
final String pdfPath = await FlutterLongScreenshot.convertToPdfAndShare(imageData, 'long_screenshot_${DateTime.now().millisecondsSinceEpoch}');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('PDF shared: $pdfPath'), duration: const Duration(seconds: 3)));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red));
}
} finally {
_setCapturing(false);
}
}
Future<void> _captureAndSaveAsPdf() async {
_setCapturing(true);
try {
// Capture the screenshot
final Uint8List imageData = await FlutterLongScreenshot.captureLongScreenshot(key: _screenshotKey, pixelRatio: 3.0, quality: _quality);
// Convert to PDF and save
final String pdfPath = await FlutterLongScreenshot.convertToPdfAndSave(
imageData: imageData,
fileName: 'long_screenshot_${DateTime.now().millisecondsSinceEpoch}',
);
if (mounted) {
final String platformMessage = Platform.isIOS ? 'PDF saved to Documents and shared' : 'PDF saved: $pdfPath';
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(platformMessage), duration: const Duration(seconds: 3)));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red));
}
} finally {
_setCapturing(false);
}
}
Future<void> _captureAndSaveToDownloads() async {
_setCapturing(true);
try {
// Capture the screenshot
final Uint8List imageData = await FlutterLongScreenshot.captureLongScreenshot(key: _screenshotKey, pixelRatio: 3.0, quality: _quality);
// Save using platform-appropriate method
final String filePath = await FlutterLongScreenshot.saveToDownloadsAndOpen(
imageData: imageData,
fileName: 'long_screenshot_${DateTime.now().millisecondsSinceEpoch}',
);
if (mounted) {
final String platformMessage = Platform.isIOS ? 'Screenshot saved to Documents and shared' : 'Screenshot saved to Downloads: $filePath';
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(platformMessage), duration: const Duration(seconds: 3)));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red));
}
} finally {
_setCapturing(false);
}
}
Future<void> _captureAndShareImage() async {
_setCapturing(true);
try {
// Capture the screenshot
final Uint8List imageData = await FlutterLongScreenshot.captureLongScreenshot(key: _screenshotKey, pixelRatio: 3.0, quality: _quality);
// Share the image
final String imagePath = await FlutterLongScreenshot.shareScreenshot(imageData, 'screenshot_${DateTime.now().millisecondsSinceEpoch}');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Screenshot shared: $imagePath'), duration: const Duration(seconds: 3)));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red));
}
} finally {
_setCapturing(false);
}
}
void _setCapturing(bool value) {
setState(() {
_isCapturing = value;
});
}
Widget _buildLongContent() {
// Sample user data
final List<Map<String, dynamic>> users = [
{
'name': 'John Smith',
'email': '[email protected]',
'role': 'Software Engineer',
'company': 'Tech Solutions Inc.',
'avatar': 'https://i.pravatar.cc/150?img=1',
'status': 'Active',
'lastActive': '2 hours ago',
},
{
'name': 'Sarah Johnson',
'email': '[email protected]',
'role': 'Product Designer',
'company': 'Creative Studios',
'avatar': 'https://i.pravatar.cc/150?img=5',
'status': 'Online',
'lastActive': 'Just now',
},
{
'name': 'Michael Chen',
'email': '[email protected]',
'role': 'Data Scientist',
'company': 'AI Research Lab',
'avatar': 'https://i.pravatar.cc/150?img=8',
'status': 'Away',
'lastActive': '30 minutes ago',
},
{
'name': 'Emma Wilson',
'email': '[email protected]',
'role': 'Marketing Manager',
'company': 'Global Brands',
'avatar': 'https://i.pravatar.cc/150?img=9',
'status': 'Active',
'lastActive': '1 hour ago',
},
{
'name': 'David Brown',
'email': '[email protected]',
'role': 'UX Researcher',
'company': 'User Experience Co.',
'avatar': 'https://i.pravatar.cc/150?img=11',
'status': 'Offline',
'lastActive': 'Yesterday',
},
{
'name': 'John Smith',
'email': '[email protected]',
'role': 'Software Engineer',
'company': 'Tech Solutions Inc.',
'avatar': 'https://i.pravatar.cc/150?img=1',
'status': 'Active',
'lastActive': '2 hours ago',
},
{
'name': 'Sarah Johnson',
'email': '[email protected]',
'role': 'Product Designer',
'company': 'Creative Studios',
'avatar': 'https://i.pravatar.cc/150?img=5',
'status': 'Online',
'lastActive': 'Just now',
},
{
'name': 'Michael Chen',
'email': '[email protected]',
'role': 'Data Scientist',
'company': 'AI Research Lab',
'avatar': 'https://i.pravatar.cc/150?img=8',
'status': 'Away',
'lastActive': '30 minutes ago',
},
{
'name': 'Emma Wilson',
'email': '[email protected]',
'role': 'Marketing Manager',
'company': 'Global Brands',
'avatar': 'https://i.pravatar.cc/150?img=9',
'status': 'Active',
'lastActive': '1 hour ago',
},
{
'name': 'David Brown',
'email': '[email protected]',
'role': 'UX Researcher',
'company': 'User Experience Co.',
'avatar': 'https://i.pravatar.cc/150?img=11',
'status': 'Offline',
'lastActive': 'Yesterday',
},
{
'name': 'John Smith',
'email': '[email protected]',
'role': 'Software Engineer',
'company': 'Tech Solutions Inc.',
'avatar': 'https://i.pravatar.cc/150?img=1',
'status': 'Active',
'lastActive': '2 hours ago',
},
{
'name': 'Sarah Johnson',
'email': '[email protected]',
'role': 'Product Designer',
'company': 'Creative Studios',
'avatar': 'https://i.pravatar.cc/150?img=5',
'status': 'Online',
'lastActive': 'Just now',
},
{
'name': 'Michael Chen',
'email': '[email protected]',
'role': 'Data Scientist',
'company': 'AI Research Lab',
'avatar': 'https://i.pravatar.cc/150?img=8',
'status': 'Away',
'lastActive': '30 minutes ago',
},
{
'name': 'Emma Wilson',
'email': '[email protected]',
'role': 'Marketing Manager',
'company': 'Global Brands',
'avatar': 'https://i.pravatar.cc/150?img=9',
'status': 'Active',
'lastActive': '1 hour ago',
},
{
'name': 'David Brown',
'email': '[email protected]',
'role': 'UX Researcher',
'company': 'User Experience Co.',
'avatar': 'https://i.pravatar.cc/150?img=11',
'status': 'Offline',
'lastActive': 'Yesterday',
},
{
'name': 'John Smith',
'email': '[email protected]',
'role': 'Software Engineer',
'company': 'Tech Solutions Inc.',
'avatar': 'https://i.pravatar.cc/150?img=1',
'status': 'Active',
'lastActive': '2 hours ago',
},
{
'name': 'Sarah Johnson',
'email': '[email protected]',
'role': 'Product Designer',
'company': 'Creative Studios',
'avatar': 'https://i.pravatar.cc/150?img=5',
'status': 'Online',
'lastActive': 'Just now',
},
{
'name': 'Michael Chen',
'email': '[email protected]',
'role': 'Data Scientist',
'company': 'AI Research Lab',
'avatar': 'https://i.pravatar.cc/150?img=8',
'status': 'Away',
'lastActive': '30 minutes ago',
},
{
'name': 'Emma Wilson',
'email': '[email protected]',
'role': 'Marketing Manager',
'company': 'Global Brands',
'avatar': 'https://i.pravatar.cc/150?img=9',
'status': 'Active',
'lastActive': '1 hour ago',
},
{
'name': 'David Brown',
'email': '[email protected]',
'role': 'UX Researcher',
'company': 'User Experience Co.',
'avatar': 'https://i.pravatar.cc/150?img=11',
'status': 'Offline',
'lastActive': 'Yesterday',
},
];
return Container(
color: Colors.white,
child: Column(
children: [
// Header section
Container(
padding: const EdgeInsets.all(16),
color: Theme.of(context).colorScheme.primaryContainer,
child: Row(
children: [
Icon(Icons.people, color: Theme.of(context).colorScheme.onPrimaryContainer),
const SizedBox(width: 12),
Text(
'User Directory',
style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),
),
const Spacer(),
Text(
'${users.length} Users',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7)),
),
],
),
),
// User list
...users.map((user) => _buildUserCard(user)),
],
),
);
}
Widget _buildUserCard(Map<String, dynamic> user) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2))],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
// Handle user card tap
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Selected ${user['name']}'), duration: const Duration(seconds: 1)));
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profile photo
CircleAvatar(radius: 30, backgroundImage: NetworkImage(user['avatar'])),
const SizedBox(width: 16),
// User details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(user['name'], style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
),
_buildStatusIndicator(user['status']),
],
),
const SizedBox(height: 4),
Text(
user['role'],
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)),
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.business, size: 16, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5)),
const SizedBox(width: 4),
Text(
user['company'],
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5)),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.email, size: 16, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5)),
const SizedBox(width: 4),
Text(
user['email'],
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5)),
),
],
),
],
),
),
],
),
),
),
),
);
}
Widget _buildStatusIndicator(String status) {
Color color;
switch (status.toLowerCase()) {
case 'online':
color = Colors.green;
break;
case 'away':
color = Colors.orange;
break;
case 'offline':
color = Colors.grey;
break;
default:
color = Colors.blue;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 4),
Text(
status,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w500),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Long Screenshot Demo'), centerTitle: true, elevation: 0),
body: Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Icon(Icons.screenshot, size: 48, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 16),
Text('Capture Long Screenshots', style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center),
const SizedBox(height: 8),
Text(
'Scroll through the content below and tap the button to capture a screenshot of the entire content.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)),
textAlign: TextAlign.center,
),
],
),
),
RepaintBoundary(key: _screenshotKey, child: _buildLongContent()),
],
),
),
if (_isCapturing)
Container(
color: Colors.black.withValues(alpha: 0.5),
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Capturing screenshot...', style: TextStyle(color: Colors.white, fontSize: 16)),
],
),
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _isCapturing ? null : _showSaveOptions,
icon: const Icon(Icons.camera_alt),
label: const Text('Capture'),
),
);
}
}