提交 f296fd1c authored 作者: Sachin Ganesh's avatar Sachin Ganesh

Add support for invisible widgets

上级 bf306156
# screenshot <img src="https://github.com/SachinGanesh/screenshot/raw/master/assets/sc.png" alt="screenshot"/>
A simple plugin to capture widgets as Images. A simple package to capture widgets as Images. Now you can capture widgets that are not rendered on the screen!
This plugin wraps your widgets inside [RenderRepaintBoundary](https://docs.flutter.io/flutter/rendering/RenderRepaintBoundary-class.html) This package wraps your widgets inside [RenderRepaintBoundary](https://docs.flutter.io/flutter/rendering/RenderRepaintBoundary-class.html)
[Source](https://stackoverflow.com/a/51118088) [Source](https://stackoverflow.com/a/51118088)
<img src="https://github.com/SachinGanesh/screenshot/raw/master/assets/screenshot.gif" alt="screenshot" width="200"/>
---
## Getting Started ## Getting Started
This handy plugin can be used to capture any Widget including full screen screenshots & individual widgets like Text(). This handy package can be used to capture any Widget including full screen screenshots & individual widgets like Text().
1) Create Instance of Screenshot Controller 1) Create Instance of Screenshot Controller
...@@ -49,7 +52,27 @@ screenshotController.capture().then((Uint8List image) { ...@@ -49,7 +52,27 @@ screenshotController.capture().then((Uint8List image) {
print(onError); print(onError);
}); });
``` ```
---
## Capturing Widgets that are not in the widget tree
You can capture invisible widgets by pr
```dart
screenshotController
.captureFromWidget(Container(
padding: const EdgeInsets.all(30.0),
decoration: BoxDecoration(
border:
Border.all(color: Colors.blueAccent, width: 5.0),
color: Colors.redAccent,
),
child: Text("This is an invisible widget")))
.then((capturedImage) {
// Handle captured image
});
},
```
---
Example: Example:
```dart ```dart
...@@ -106,8 +129,9 @@ Example: ...@@ -106,8 +129,9 @@ Example:
} }
``` ```
<img src="assets/screenshot.png" alt="screenshot" width="400"/> <img src="https://github.com/SachinGanesh/screenshot/raw/master/assets/screenshot.png" alt="screenshot" width="400"/>
---
## Saving images to Specific Location ## Saving images to Specific Location
For this you can use captureAndSave method by passing directory location. By default, the captured image will be saved to Application Directory. Custom paths can be set using **path parameter**. Refer [path_provider](https://pub.dartlang.org/packages/path_provider) For this you can use captureAndSave method by passing directory location. By default, the captured image will be saved to Application Directory. Custom paths can be set using **path parameter**. Refer [path_provider](https://pub.dartlang.org/packages/path_provider)
...@@ -126,20 +150,22 @@ screenshotController.captureAndSave( ...@@ -126,20 +150,22 @@ screenshotController.captureAndSave(
fileName:fileName fileName:fileName
); );
``` ```
---
## Saving images to Gallery ## Saving images to Gallery
If you want to save captured image to Gallery, Please use https://github.com/hui-z/image_gallery_saver If you want to save captured image to Gallery, Please use https://github.com/hui-z/image_gallery_saver
Example app uses the same to save screenshots to gallery. Example app uses the same to save screenshots to gallery.
## Note: ### Note:
Captured image may look pixelated. You can overcome this issue by setting value for **pixelRatio** Captured image may look pixelated. You can overcome this issue by setting value for **pixelRatio**
>The pixelRatio describes the scale between the logical pixels and the size of the output image. It is independent of the window.devicePixelRatio for the device, so specifying 1.0 (the default) will give you a 1:1 mapping between logical pixels and the output pixels in the image. >The pixelRatio describes the scale between the logical pixels and the size of the output image. It is independent of the window.devicePixelRatio for the device, so specifying 1.0 (the default) will give you a 1:1 mapping between logical pixels and the output pixels in the image.
```dart ```dart
double pixelRatio = MediaQuery.of(context).devicePixelRatio;
screenshotController.capture( screenshotController.capture(
pixelRatio: 1.5 pixelRatio: pixelRatio
) )
``` ```
--- ---
...@@ -156,6 +182,6 @@ The solution is to add a small delay before capturing. ...@@ -156,6 +182,6 @@ The solution is to add a small delay before capturing.
screenshotController.capture(delay: Duration(milliseconds: 10)) screenshotController.capture(delay: Duration(milliseconds: 10))
``` ```
## Known Bugs ## Known Issues
- Image will not be updated if same filename is given multiple times - Platform Views are not supported. (Example: Google Maps, Camera etc)
...@@ -10,62 +10,29 @@ project 'Runner', { ...@@ -10,62 +10,29 @@ project 'Runner', {
'Release' => :release, 'Release' => :release,
} }
def parse_KV_file(file, separator='=') def flutter_root
file_abs_path = File.expand_path(file) generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
if !File.exists? file_abs_path unless File.exist?(generated_xcode_build_settings_path)
return []; raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end end
pods_ary = []
skip_line_start_symbols = ["#", "/"] File.foreach(generated_xcode_build_settings_path) do |line|
File.foreach(file_abs_path) { |line| matches = line.match(/FLUTTER_ROOT\=(.*)/)
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } return matches[1].strip if matches
plugin = line.split(pattern=separator) end
if plugin.length == 2 raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
pods_ary.push({:name => podname, :path => podpath});
else
puts "Invalid plugin specification: #{line}"
end
}
return pods_ary
end end
target 'Runner' do require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
use_frameworks!
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
# Flutter Pods flutter_ios_podfile_setup
generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig')
if generated_xcode_build_settings.empty?
puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first."
end
generated_xcode_build_settings.map { |p|
if p[:name] == 'FLUTTER_FRAMEWORK_DIR'
symlink = File.join('.symlinks', 'flutter')
File.symlink(File.dirname(p[:path]), symlink)
pod 'Flutter', :path => File.join(symlink, File.basename(p[:path]))
end
}
# Plugin Pods target 'Runner' do
plugin_pods = parse_KV_file('../.flutter-plugins') flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
plugin_pods.map { |p|
symlink = File.join('.symlinks', 'plugins', p[:name])
File.symlink(p[:path], symlink)
pod p[:name], :path => File.join(symlink, 'ios')
}
end end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
target.build_configurations.each do |config| flutter_additional_ios_build_settings(target)
config.build_settings['SWIFT_VERSION'] = '4.2'
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end end
end end
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
<Workspace <Workspace
version = "1.0"> version = "1.0">
<FileRef <FileRef
location = "group:Runner.xcodeproj"> location = "self:">
</FileRef> </FileRef>
</Workspace> </Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Original</string>
</dict>
</plist>
...@@ -32,16 +32,6 @@ class MyApp extends StatelessWidget { ...@@ -32,16 +32,6 @@ class MyApp extends StatelessWidget {
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key); MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title; final String title;
@override @override
...@@ -49,9 +39,6 @@ class MyHomePage extends StatefulWidget { ...@@ -49,9 +39,6 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
Uint8List _imageFile;
//Create an instance of ScreenshotController //Create an instance of ScreenshotController
ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
...@@ -61,17 +48,6 @@ class _MyHomePageState extends State<MyHomePage> { ...@@ -61,17 +48,6 @@ class _MyHomePageState extends State<MyHomePage> {
super.initState(); super.initState();
} }
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done // This method is rerun every time setState is called, for instance as done
...@@ -86,43 +62,76 @@ class _MyHomePageState extends State<MyHomePage> { ...@@ -86,43 +62,76 @@ class _MyHomePageState extends State<MyHomePage> {
// the App.build method, and use it to set our appbar title. // the App.build method, and use it to set our appbar title.
title: Text(widget.title), title: Text(widget.title),
), ),
body: Container( body: Center(
child: new Center( child: Column(
child: Screenshot( mainAxisAlignment: MainAxisAlignment.center,
controller: screenshotController, children: [
child: Text("HEllo"), Screenshot(
), controller: screenshotController,
child: Container(
padding: const EdgeInsets.all(30.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.blueAccent, width: 5.0),
color: Colors.amberAccent,
),
child: Text("This widget will be captured as an image")),
),
SizedBox(
height: 25,
),
ElevatedButton(
child: Text(
'Capture Above Widget',
),
onPressed: () {
screenshotController
.capture(delay: Duration(milliseconds: 10))
.then((Uint8List capturedImage) async {
ShowCapturedWidget(context, capturedImage);
}).catchError((onError) {
print(onError);
});
},
),
ElevatedButton(
child: Text(
'Capture An Invisible Widget',
),
onPressed: () {
screenshotController
.captureFromWidget(Container(
padding: const EdgeInsets.all(30.0),
decoration: BoxDecoration(
border:
Border.all(color: Colors.blueAccent, width: 5.0),
color: Colors.redAccent,
),
child: Text("This is an invisible widget")))
.then((capturedImage) {
ShowCapturedWidget(context, capturedImage);
});
},
),
],
),
),
);
}
Future<dynamic> ShowCapturedWidget(
BuildContext context, Uint8List capturedImage) {
return showDialog(
useSafeArea: false,
context: context,
builder: (context) => Scaffold(
appBar: AppBar(
title: Text("Captured widget screenshot"),
), ),
body: Center(
child: capturedImage != null
? Image.memory(capturedImage)
: Container()),
), ),
floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
_imageFile = null;
screenshotController
.capture(delay: Duration(milliseconds: 10))
.then((Uint8List image) async {
_imageFile = image;
showDialog(
context: context,
builder: (context) => Scaffold(
appBar: AppBar(
title: Text("CAPURED SCREENSHOT"),
),
body: Center(
child: Column(
children: [
_imageFile != null ? Image.memory(_imageFile) : Container(),
],
)),
),
);
}).catchError((onError) {
print(onError);
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
); );
} }
......
...@@ -8,6 +8,7 @@ description: A new Flutter project. ...@@ -8,6 +8,7 @@ description: A new Flutter project.
# build by specifying --build-name and --build-number, respectively. # build by specifying --build-name and --build-number, respectively.
# Read more about versioning at semver.org. # Read more about versioning at semver.org.
version: 1.0.0+1 version: 1.0.0+1
publish_to: none
environment: environment:
sdk: '>=2.8.0 <3.0.0' sdk: '>=2.8.0 <3.0.0'
...@@ -19,7 +20,7 @@ dependencies: ...@@ -19,7 +20,7 @@ dependencies:
screenshot: screenshot:
path: ../ path: ../
cupertino_icons: ^0.1.2 cupertino_icons: ^0.1.2
image_gallery_saver: ^1.1.0 # image_gallery_saver: ^1.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
......
...@@ -21,7 +21,7 @@ class ScreenshotController { ...@@ -21,7 +21,7 @@ class ScreenshotController {
_containerKey = GlobalKey(); _containerKey = GlobalKey();
} }
/// Captures image and saves to given path /// Captures image and saves to given path
Future<String> captureAndSave( Future<String> captureAndSave(
String directory, { String directory, {
String? fileName, String? fileName,
...@@ -49,8 +49,8 @@ class ScreenshotController { ...@@ -49,8 +49,8 @@ class ScreenshotController {
delay: Duration.zero, delay: Duration.zero,
pixelRatio: pixelRatio, pixelRatio: pixelRatio,
); );
ByteData byteData = ByteData byteData = await (image.toByteData(
await (image.toByteData(format: ui.ImageByteFormat.png) as FutureOr<ByteData>); format: ui.ImageByteFormat.png) as FutureOr<ByteData>);
Uint8List pngBytes = byteData.buffer.asUint8List(); Uint8List pngBytes = byteData.buffer.asUint8List();
return pngBytes; return pngBytes;
...@@ -79,6 +79,59 @@ class ScreenshotController { ...@@ -79,6 +79,59 @@ class ScreenshotController {
} }
}); });
} }
Future<Uint8List> captureFromWidget(Widget widget,
{Duration delay: const Duration(milliseconds: 20)}) async {
final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
Size logicalSize = ui.window.physicalSize / ui.window.devicePixelRatio;
Size imageSize = ui.window.physicalSize;
assert(logicalSize.aspectRatio == imageSize.aspectRatio);
final RenderView renderView = RenderView(
window: ui.window,
child: RenderPositionedBox(
alignment: Alignment.center, child: repaintBoundary),
configuration: ViewConfiguration(
size: logicalSize,
devicePixelRatio: 1.0,
),
);
final PipelineOwner pipelineOwner = PipelineOwner();
final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
pipelineOwner.rootNode = renderView;
renderView.prepareInitialFrame();
final RenderObjectToWidgetElement<RenderBox> rootElement =
RenderObjectToWidgetAdapter<RenderBox>(
container: repaintBoundary,
child: Directionality(
textDirection: TextDirection.ltr,
child: widget,
),
).attachToRenderTree(buildOwner);
buildOwner.buildScope(rootElement);
await Future.delayed(delay);
buildOwner.buildScope(rootElement);
buildOwner.finalizeTree();
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
final ui.Image image = await repaintBoundary.toImage(
pixelRatio: imageSize.width / logicalSize.width);
final ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
return byteData!.buffer.asUint8List();
}
} }
class Screenshot<T> extends StatefulWidget { class Screenshot<T> extends StatefulWidget {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论