├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ ├── checker.yml │ ├── pub_dry_run.yml │ ├── pub_publish.yml │ └── pub_publish_manually.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── build.gradle ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── fluttercandies │ └── plugins │ └── ff_native_screenshot │ ├── FfNativeScreenshotPlugin.java │ ├── ScreenshotApi.java │ └── ScreenshotDetector.java ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── lib │ └── main.dart └── pubspec.yaml ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── FfNativeScreenshotPlugin.h │ ├── FfNativeScreenshotPlugin.m │ ├── ScreenshotApi.h │ └── ScreenshotApi.m └── ff_native_screenshot.podspec ├── lib ├── ff_native_screenshot.dart └── src │ └── ff_native_screenshot.dart ├── pigeon.sh ├── pigeons └── ff_native_screenshot.dart └── pubspec.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: zmtzawqlp 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | custom: http://zmtzawqlp.gitee.io/my_images/images/qrcode.png 13 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: true -------------------------------------------------------------------------------- /.github/workflows/checker.yml: -------------------------------------------------------------------------------- 1 | name: No Free usage issue checker 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Check issue actor 15 | uses: ./ 16 | with: 17 | repo: $GITHUB_REPOSITORY 18 | user: $GITHUB_ACTOR 19 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/pub_dry_run.yml: -------------------------------------------------------------------------------- 1 | name: Pub Publish dry run 2 | 3 | on: [push] 4 | 5 | jobs: 6 | publish: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | - name: Publish 14 | uses: sakebook/actions-flutter-pub-publisher@v1.3.0 15 | with: 16 | credential: ${{ secrets.CREDENTIAL_JSON }} 17 | flutter_package: true 18 | skip_test: true 19 | dry_run: true 20 | -------------------------------------------------------------------------------- /.github/workflows/pub_publish.yml: -------------------------------------------------------------------------------- 1 | name: Pub Publish plugin 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | - name: Publish 16 | uses: sakebook/actions-flutter-pub-publisher@v1.3.0 17 | with: 18 | credential: ${{ secrets.CREDENTIAL_JSON }} 19 | flutter_package: true 20 | skip_test: true 21 | dry_run: false 22 | -------------------------------------------------------------------------------- /.github/workflows/pub_publish_manually.yml: -------------------------------------------------------------------------------- 1 | name: Pub Publish plugin 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | - name: Publish 14 | uses: sakebook/actions-flutter-pub-publisher@v1.3.0 15 | with: 16 | credential: ${{ secrets.CREDENTIAL_JSON }} 17 | flutter_package: true 18 | skip_test: true 19 | dry_run: false 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7e9793dee1b85a243edd0e06cb1658e98b077561 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | * android: breaking change, need READ_EXTERNAL_STORAGE permission for android now(some android versions need it). 4 | * android: ContentObserver instead of FileObserver 5 | * android: avoid multiple triggers 6 | 7 | ## 0.0.4 8 | 9 | * change Screenshot ImageType from JPEG to PNG on Android 10 | 11 | ## 0.0.3 12 | 13 | * dispose screenshotFlutterApi when onDetachedFromEngine 14 | 15 | ## 0.0.2 16 | 17 | * dart format 18 | 19 | ## 0.0.1 20 | 21 | * support to take or listen screenshot(support Platform Views) 22 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zmtzawqlp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zmtzawqlp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ff_native_screenshot 2 | 3 | A Flutter plugin to take or listen screenshot(support Platform Views) for Android and iOS with native code. 4 | 5 | It's a workaround for the issue [RepaintBoundary can't take screenshot of Platform Views](https://github.com/flutter/flutter/issues/102866) . 6 | 7 | [![pub package](https://img.shields.io/pub/v/ff_native_screenshot.svg)](https://pub.dartlang.org/packages/ff_native_screenshot) [![GitHub stars](https://img.shields.io/github/stars/fluttercandies/ff_native_screenshot)](https://github.com/fluttercandies/ff_native_screenshot/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/ff_native_screenshot)](https://github.com/fluttercandies/ff_native_screenshot/network) [![GitHub license](https://img.shields.io/github/license/fluttercandies/ff_native_screenshot)](https://github.com/fluttercandies/ff_native_screenshot/blob/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/fluttercandies/ff_native_screenshot)](https://github.com/fluttercandies/ff_native_screenshot/issues) 8 | 9 | ## Usage 10 | 11 | ``` yaml 12 | dependencies: 13 | ff_native_screenshot: any 14 | # only for android 15 | permission_handler: any 16 | ``` 17 | 18 | ## Take Screenshot 19 | 20 | ``` dart 21 | Uint8List? data = await FfNativeScreenshot().takeScreenshot(); 22 | ``` 23 | 24 | ## Listen Screenshot 25 | 26 | ``` dart 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | init(); 32 | } 33 | 34 | Future init() async { 35 | if (Platform.isAndroid) { 36 | await Permission.storage.request(); 37 | } 38 | FfNativeScreenshot().setup(ScreenshotFlutterApiImplements(context)); 39 | await FfNativeScreenshot().startListeningScreenshot(); 40 | 41 | if (mounted) { 42 | setState(() {}); 43 | } 44 | } 45 | 46 | @override 47 | void dispose() { 48 | FfNativeScreenshot().stopListeningScreenshot(); 49 | super.dispose(); 50 | } 51 | 52 | bool? listening; 53 | @override 54 | void didChangeAppLifecycleState(AppLifecycleState state) { 55 | super.didChangeAppLifecycleState(state); 56 | switch (state) { 57 | case AppLifecycleState.resumed: 58 | if (listening == true && !FfNativeScreenshot().listening) { 59 | FfNativeScreenshot().startListeningScreenshot(); 60 | } 61 | break; 62 | case AppLifecycleState.paused: 63 | listening = FfNativeScreenshot().listening; 64 | if (listening == true) { 65 | FfNativeScreenshot().stopListeningScreenshot(); 66 | } 67 | 68 | break; 69 | default: 70 | } 71 | } 72 | 73 | class ScreenshotFlutterApiImplements extends ScreenshotFlutterApi { 74 | ScreenshotFlutterApiImplements(); 75 | @override 76 | Future onTakeScreenshot(Uint8List? data) async { 77 | // if it has something error 78 | // you can call takeScreenshot 79 | data ??= await FfNativeScreenshot().takeScreenshot(); 80 | } 81 | } 82 | 83 | ``` 84 | 85 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.fluttercandies.plugins.ff_native_screenshot' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:4.1.0' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 31 26 | 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | 32 | defaultConfig { 33 | minSdkVersion 16 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ff_native_screenshot' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/java/com/fluttercandies/plugins/ff_native_screenshot/FfNativeScreenshotPlugin.java: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.plugins.ff_native_screenshot; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.Rect; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.os.Environment; 12 | import android.os.FileObserver; 13 | import android.os.Handler; 14 | import android.os.Looper; 15 | import android.util.Log; 16 | import android.view.PixelCopy; 17 | import android.view.View; 18 | import android.view.Window; 19 | import android.webkit.MimeTypeMap; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.annotation.Nullable; 23 | 24 | import java.io.ByteArrayOutputStream; 25 | import java.io.File; 26 | import java.io.FileInputStream; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | import io.flutter.embedding.engine.plugins.FlutterPlugin; 31 | import kotlin.jvm.internal.Intrinsics; 32 | 33 | /** 34 | * FfNativeScreenshotPlugin 35 | */ 36 | public class FfNativeScreenshotPlugin implements FlutterPlugin, ScreenshotApi.ScreenshotHostApi { 37 | 38 | private ScreenshotApi.ScreenshotFlutterApi screenshotFlutterApi; 39 | private Context context; 40 | private ActivityLifecycleCallbacks callbacks = new ActivityLifecycleCallbacks(); 41 | private Handler handler; 42 | //private FileObserver fileObserver; 43 | private ScreenshotDetector detector; 44 | //private String lastScreenshotFileName; 45 | 46 | @Override 47 | public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { 48 | ScreenshotApi.ScreenshotHostApi.setup(flutterPluginBinding.getBinaryMessenger(), this); 49 | screenshotFlutterApi = new ScreenshotApi.ScreenshotFlutterApi(flutterPluginBinding.getBinaryMessenger()); 50 | context = flutterPluginBinding.getApplicationContext(); 51 | if (context instanceof Application) { 52 | Application application = (Application) context; 53 | application.registerActivityLifecycleCallbacks(callbacks); 54 | } 55 | } 56 | 57 | 58 | @Override 59 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { 60 | ScreenshotApi.ScreenshotHostApi.setup(binding.getBinaryMessenger(), null); 61 | screenshotFlutterApi = null; 62 | } 63 | 64 | 65 | @Override 66 | public void takeScreenshot(ScreenshotApi.Result result) { 67 | 68 | // ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE); 69 | // ComponentName cn = am.getRunningTasks(1).get(0).topActivity; 70 | if (callbacks.currentActivity != null) { 71 | Activity activity = callbacks.currentActivity; 72 | Window window = activity.getWindow(); 73 | View view = window.getDecorView(); 74 | final Bitmap bitmap; 75 | try { 76 | if (Build.VERSION.SDK_INT >= 26) { 77 | bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); 78 | int[] location = new int[2]; 79 | view.getLocationInWindow(location); 80 | PixelCopy.request(window, new Rect(location[0], location[1], location[0] + view.getWidth(), location[1] + view.getHeight()), bitmap, (PixelCopy.OnPixelCopyFinishedListener) (new PixelCopy.OnPixelCopyFinishedListener() { 81 | public final void onPixelCopyFinished(int it) { 82 | if (it == 0) { 83 | takeScreenshotResult(bitmap, result); 84 | } else { 85 | result.error(new Exception("fail to take screenshot")); 86 | } 87 | } 88 | }), new Handler(Looper.getMainLooper())); 89 | } else { 90 | bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.RGB_565); 91 | Canvas canvas = new Canvas(bitmap); 92 | view.draw(canvas); 93 | canvas.setBitmap((Bitmap) null); 94 | Intrinsics.checkNotNullExpressionValue(bitmap, "tBitmap"); 95 | takeScreenshotResult(bitmap, result); 96 | } 97 | 98 | } catch (Exception e) { 99 | Log.e("takeScreenshot", e.getMessage()); 100 | result.error(e); 101 | } finally { 102 | 103 | } 104 | } else { 105 | result.success(null); 106 | } 107 | } 108 | 109 | @Override 110 | public void startListeningScreenshot() { 111 | handler = new Handler(Looper.getMainLooper()); 112 | 113 | detector = new ScreenshotDetector(context, () -> { 114 | handler.post(() -> { 115 | onTakeScreenshot(); 116 | }); 117 | }); 118 | detector.start(); 119 | // 120 | // if (Build.VERSION.SDK_INT >= 29) { 121 | // final List files = new ArrayList<>(); 122 | // final List paths = new ArrayList<>(); 123 | // for (Path path : Path.values()) { 124 | // files.add(new File(path.getPath())); 125 | // paths.add(path.getPath()); 126 | // } 127 | // fileObserver = new FileObserver(files) { 128 | // @Override 129 | // public void onEvent(int event, final String filename) { 130 | // if (event == FileObserver.CREATE) { 131 | // handler.post(() -> { 132 | // for (String fullPath : paths) { 133 | // File file = new File(fullPath + filename); 134 | // handleScreenshot(file); 135 | // } 136 | // }); 137 | // } 138 | // } 139 | // }; 140 | // fileObserver.startWatching(); 141 | // } else { 142 | // for (final Path path : Path.values()) { 143 | // fileObserver = new FileObserver(path.getPath()) { 144 | // @Override 145 | // public void onEvent(int event, final String filename) { 146 | // 147 | // File file = new File(path.getPath() + filename); 148 | // if (event == FileObserver.CREATE) { 149 | // handler.post(() -> { 150 | // handleScreenshot(file); 151 | // }); 152 | // } 153 | // } 154 | // }; 155 | // fileObserver.startWatching(); 156 | // } 157 | // } 158 | } 159 | 160 | // private void handleScreenshot(File file) { 161 | // if (file.exists()) { 162 | // String path = file.getPath(); 163 | // if (lastScreenshotFileName!=path && getMimeType(file.getPath()).contains("image")) { 164 | // lastScreenshotFileName = path; 165 | // onTakeScreenshot(); 166 | // } 167 | // } 168 | // } 169 | 170 | @Override 171 | public void stopListeningScreenshot() { 172 | 173 | // if (fileObserver != null) fileObserver.stopWatching(); 174 | // lastScreenshotFileName = null; 175 | if (detector != null) { 176 | detector.stop(); 177 | detector = null; 178 | } 179 | } 180 | 181 | private void takeScreenshotResult(Bitmap bitmap, ScreenshotApi.Result result) { 182 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 183 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); 184 | byte[] imageInByte = stream.toByteArray(); 185 | result.success(imageInByte); 186 | } 187 | 188 | private void onTakeScreenshot(String path) { 189 | try { 190 | File imageFile = new File(path); 191 | FileInputStream inputStream = new FileInputStream(imageFile); 192 | byte[] buffer = new byte[1024]; 193 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 194 | int len = 0; 195 | while ((len = inputStream.read(buffer)) != -1) { 196 | outputStream.write(buffer, 0, len); 197 | } 198 | byte[] imageInByte = outputStream.toByteArray(); 199 | if (screenshotFlutterApi != null) { 200 | screenshotFlutterApi.onTakeScreenshot(imageInByte, (v) -> { 201 | }); 202 | } 203 | } catch (Exception e) { 204 | Log.e("onTakeScreenshot", e.getMessage()); 205 | } finally { 206 | 207 | } 208 | } 209 | 210 | 211 | private void onTakeScreenshot() { 212 | takeScreenshot(new TakeScreenshotResult()); 213 | } 214 | 215 | class TakeScreenshotResult implements ScreenshotApi.Result { 216 | @Override 217 | public void success(byte[] result) { 218 | if (screenshotFlutterApi != null) { 219 | screenshotFlutterApi.onTakeScreenshot(result, (v) -> { 220 | }); 221 | } 222 | } 223 | 224 | @Override 225 | public void error(Throwable error) { 226 | Log.e("takeScreenshot", error.getMessage()); 227 | } 228 | } 229 | 230 | class ActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { 231 | 232 | private Activity currentActivity; 233 | 234 | @Override 235 | public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { 236 | 237 | } 238 | 239 | @Override 240 | public void onActivityStarted(@NonNull Activity activity) { 241 | 242 | } 243 | 244 | @Override 245 | public void onActivityResumed(@NonNull Activity activity) { 246 | currentActivity = activity; 247 | } 248 | 249 | @Override 250 | public void onActivityPaused(@NonNull Activity activity) { 251 | 252 | } 253 | 254 | @Override 255 | public void onActivityStopped(@NonNull Activity activity) { 256 | if (currentActivity == activity) { 257 | currentActivity = null; 258 | } 259 | } 260 | 261 | @Override 262 | public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { 263 | 264 | } 265 | 266 | @Override 267 | public void onActivityDestroyed(@NonNull Activity activity) { 268 | 269 | } 270 | } 271 | 272 | public static String getMimeType(String url) { 273 | String type = null; 274 | String extension = MimeTypeMap.getFileExtensionFromUrl(url); 275 | if (extension != null) { 276 | type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 277 | } 278 | return type; 279 | } 280 | 281 | public enum Path { 282 | DCIM(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator + "Screenshots" + File.separator), 283 | PICTURES(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "Screenshots" + File.separator); 284 | 285 | final private String path; 286 | 287 | public String getPath() { 288 | return path; 289 | } 290 | 291 | Path(String path) { 292 | this.path = path; 293 | } 294 | } 295 | } 296 | 297 | -------------------------------------------------------------------------------- /android/src/main/java/com/fluttercandies/plugins/ff_native_screenshot/ScreenshotApi.java: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v3.1.0), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | 4 | package com.fluttercandies.plugins.ff_native_screenshot; 5 | 6 | import android.util.Log; 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | import io.flutter.plugin.common.BasicMessageChannel; 10 | import io.flutter.plugin.common.BinaryMessenger; 11 | import io.flutter.plugin.common.MessageCodec; 12 | import io.flutter.plugin.common.StandardMessageCodec; 13 | import java.io.ByteArrayOutputStream; 14 | import java.nio.ByteBuffer; 15 | import java.util.Arrays; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.HashMap; 20 | 21 | /** Generated class from Pigeon. */ 22 | @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) 23 | public class ScreenshotApi { 24 | 25 | public interface Result { 26 | void success(T result); 27 | void error(Throwable error); 28 | } 29 | private static class ScreenshotHostApiCodec extends StandardMessageCodec { 30 | public static final ScreenshotHostApiCodec INSTANCE = new ScreenshotHostApiCodec(); 31 | private ScreenshotHostApiCodec() {} 32 | } 33 | 34 | /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ 35 | public interface ScreenshotHostApi { 36 | void takeScreenshot(Result result); 37 | void startListeningScreenshot(); 38 | void stopListeningScreenshot(); 39 | 40 | /** The codec used by ScreenshotHostApi. */ 41 | static MessageCodec getCodec() { 42 | return ScreenshotHostApiCodec.INSTANCE; 43 | } 44 | 45 | /** Sets up an instance of `ScreenshotHostApi` to handle messages through the `binaryMessenger`. */ 46 | static void setup(BinaryMessenger binaryMessenger, ScreenshotHostApi api) { 47 | { 48 | BasicMessageChannel channel = 49 | new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScreenshotHostApi.takeScreenshot", getCodec()); 50 | if (api != null) { 51 | channel.setMessageHandler((message, reply) -> { 52 | Map wrapped = new HashMap<>(); 53 | try { 54 | Result resultCallback = new Result() { 55 | public void success(byte[] result) { 56 | wrapped.put("result", result); 57 | reply.reply(wrapped); 58 | } 59 | public void error(Throwable error) { 60 | wrapped.put("error", wrapError(error)); 61 | reply.reply(wrapped); 62 | } 63 | }; 64 | 65 | api.takeScreenshot(resultCallback); 66 | } 67 | catch (Error | RuntimeException exception) { 68 | wrapped.put("error", wrapError(exception)); 69 | reply.reply(wrapped); 70 | } 71 | }); 72 | } else { 73 | channel.setMessageHandler(null); 74 | } 75 | } 76 | { 77 | BasicMessageChannel channel = 78 | new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScreenshotHostApi.startListeningScreenshot", getCodec()); 79 | if (api != null) { 80 | channel.setMessageHandler((message, reply) -> { 81 | Map wrapped = new HashMap<>(); 82 | try { 83 | api.startListeningScreenshot(); 84 | wrapped.put("result", null); 85 | } 86 | catch (Error | RuntimeException exception) { 87 | wrapped.put("error", wrapError(exception)); 88 | } 89 | reply.reply(wrapped); 90 | }); 91 | } else { 92 | channel.setMessageHandler(null); 93 | } 94 | } 95 | { 96 | BasicMessageChannel channel = 97 | new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScreenshotHostApi.stopListeningScreenshot", getCodec()); 98 | if (api != null) { 99 | channel.setMessageHandler((message, reply) -> { 100 | Map wrapped = new HashMap<>(); 101 | try { 102 | api.stopListeningScreenshot(); 103 | wrapped.put("result", null); 104 | } 105 | catch (Error | RuntimeException exception) { 106 | wrapped.put("error", wrapError(exception)); 107 | } 108 | reply.reply(wrapped); 109 | }); 110 | } else { 111 | channel.setMessageHandler(null); 112 | } 113 | } 114 | } 115 | } 116 | private static class ScreenshotFlutterApiCodec extends StandardMessageCodec { 117 | public static final ScreenshotFlutterApiCodec INSTANCE = new ScreenshotFlutterApiCodec(); 118 | private ScreenshotFlutterApiCodec() {} 119 | } 120 | 121 | /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ 122 | public static class ScreenshotFlutterApi { 123 | private final BinaryMessenger binaryMessenger; 124 | public ScreenshotFlutterApi(BinaryMessenger argBinaryMessenger){ 125 | this.binaryMessenger = argBinaryMessenger; 126 | } 127 | public interface Reply { 128 | void reply(T reply); 129 | } 130 | static MessageCodec getCodec() { 131 | return ScreenshotFlutterApiCodec.INSTANCE; 132 | } 133 | 134 | public void onTakeScreenshot(@Nullable byte[] dataArg, Reply callback) { 135 | BasicMessageChannel channel = 136 | new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScreenshotFlutterApi.onTakeScreenshot", getCodec()); 137 | channel.send(new ArrayList(Arrays.asList(dataArg)), channelReply -> { 138 | callback.reply(null); 139 | }); 140 | } 141 | } 142 | private static Map wrapError(Throwable exception) { 143 | Map errorMap = new HashMap<>(); 144 | errorMap.put("message", exception.toString()); 145 | errorMap.put("code", exception.getClass().getSimpleName()); 146 | errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); 147 | return errorMap; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /android/src/main/java/com/fluttercandies/plugins/ff_native_screenshot/ScreenshotDetector.java: -------------------------------------------------------------------------------- 1 | package com.fluttercandies.plugins.ff_native_screenshot; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ContentResolver; 5 | import android.content.Context; 6 | import android.database.ContentObserver; 7 | import android.database.Cursor; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.os.Handler; 12 | import android.os.Looper; 13 | import android.provider.MediaStore; 14 | import android.text.TextUtils; 15 | import android.util.Log; 16 | 17 | import java.util.LinkedHashMap; 18 | 19 | public class ScreenshotDetector { 20 | private static final long SCREENSHOT_TIMEOUT = 10000L; 21 | 22 | private static final String[] KEYWORDS = { 23 | "screenshot", "screen_shot", "screen-shot", "screen shot", 24 | "screencapture", "screen_capture", "screen-capture", "screen capture", 25 | "screencap", "screen_cap", "screen-cap", "screen cap", "截屏"}; 26 | 27 | private static final String[] MEDIA_PROJECTIONS = { 28 | MediaStore.Images.ImageColumns.DATA, 29 | MediaStore.Images.ImageColumns.DATE_TAKEN, 30 | MediaStore.Images.ImageColumns.DATE_ADDED, 31 | MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}; 32 | 33 | private final DataCache lastDatas = new DataCache(4); 34 | 35 | private ContentResolver contentResolver; 36 | 37 | ScreenshotDetector(Context context, Runnable onScreenShot) { 38 | this.context = context; 39 | this.onScreenShot = onScreenShot; 40 | } 41 | 42 | private Context context; 43 | private Runnable onScreenShot; 44 | private ContentObserver internalObserver; 45 | private ContentObserver externalObserver; 46 | private boolean isRegistered; 47 | 48 | void start() { 49 | if (!isRegistered) { 50 | try { 51 | contentResolver = context.getContentResolver(); 52 | if (internalObserver == null) { 53 | internalObserver = new ScreenshotDetector.MediaContentObserver( 54 | MediaStore.Images.Media.INTERNAL_CONTENT_URI, null, onScreenShot); 55 | } 56 | 57 | if (externalObserver == null) { 58 | externalObserver = new ScreenshotDetector.MediaContentObserver( 59 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, onScreenShot); 60 | } 61 | 62 | contentResolver.registerContentObserver( 63 | MediaStore.Images.Media.INTERNAL_CONTENT_URI, 64 | true, 65 | internalObserver); 66 | 67 | contentResolver.registerContentObserver( 68 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 69 | true, 70 | externalObserver); 71 | isRegistered = true; 72 | } catch (Exception e) { 73 | Log.e("startListening", e.getMessage()); 74 | } 75 | } 76 | } 77 | 78 | 79 | void stop() { 80 | if (isRegistered) { 81 | try { 82 | if (internalObserver != null) { 83 | contentResolver.unregisterContentObserver(internalObserver); 84 | } 85 | 86 | if (externalObserver != null) { 87 | contentResolver.unregisterContentObserver(externalObserver); 88 | } 89 | contentResolver = null; 90 | isRegistered = false; 91 | } catch (Exception e) { 92 | Log.e("stopListening", e.getMessage()); 93 | } 94 | } 95 | } 96 | 97 | private class MediaContentObserver extends ContentObserver { 98 | private final Uri uri; 99 | private Runnable onScreenShot; 100 | 101 | MediaContentObserver(Uri uri, Handler handler, Runnable onScreenShot) { 102 | super(handler); 103 | this.uri = uri; 104 | this.onScreenShot = onScreenShot; 105 | } 106 | 107 | @Override 108 | public void onChange(boolean selfChange) { 109 | new Thread(() -> { 110 | Cursor cursor = null; 111 | try { 112 | 113 | boolean success = false; 114 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 115 | final Bundle queryArgs = new Bundle(); 116 | queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 1); 117 | queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, MediaStore.Images.ImageColumns.DATE_ADDED + " DESC"); 118 | cursor = contentResolver.query( 119 | uri, 120 | MEDIA_PROJECTIONS, 121 | queryArgs, 122 | null); 123 | } else { 124 | cursor = contentResolver.query( 125 | uri, 126 | MEDIA_PROJECTIONS, 127 | null, 128 | null, 129 | MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"); 130 | } 131 | 132 | if (cursor != null && cursor.moveToFirst()) { 133 | @SuppressLint("Range") final String data = cursor.getString( 134 | cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); 135 | @SuppressLint("Range") final long dateTaken = cursor.getLong( 136 | cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)); 137 | success = handleScreenshot(data, dateTaken); 138 | } 139 | 140 | 141 | if (!success) { 142 | if (cursor != null && !cursor.isClosed()) { 143 | cursor.close(); 144 | } 145 | final String where = MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME + " like?"; 146 | final String[] whereArgs = new String[]{"%screen%shot%"}; 147 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 148 | final Bundle queryArgs = new Bundle(); 149 | queryArgs.putInt(ContentResolver.QUERY_ARG_SQL_LIMIT, 1); 150 | queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, where); 151 | queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, whereArgs); 152 | queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, MediaStore.Images.ImageColumns.DATE_ADDED + " DESC"); 153 | cursor = contentResolver.query( 154 | uri, 155 | MEDIA_PROJECTIONS, 156 | queryArgs, 157 | null); 158 | } else { 159 | cursor = contentResolver.query( 160 | uri, 161 | MEDIA_PROJECTIONS, 162 | where, 163 | whereArgs, 164 | MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"); 165 | } 166 | 167 | 168 | if (cursor != null && cursor.moveToFirst()) { 169 | @SuppressLint("Range") final String data = cursor.getString( 170 | cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); 171 | @SuppressLint("Range") final long dateTaken = cursor.getLong( 172 | cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)); 173 | success = handleScreenshot(data, dateTaken); 174 | } 175 | } 176 | } catch (Throwable e) { 177 | Log.e("onTakeScreenshotEvent", e.getMessage()); 178 | } finally { 179 | if (cursor != null && !cursor.isClosed()) { 180 | try { 181 | cursor.close(); 182 | } catch (Exception e) { 183 | e.printStackTrace(); 184 | } 185 | } 186 | } 187 | }).start(); 188 | 189 | } 190 | 191 | private boolean handleScreenshot(final String data, long dateTaken) { 192 | if (isScreenshot(data, dateTaken)) { 193 | lastDatas.put(data); 194 | onScreenShot.run(); 195 | return true; 196 | } 197 | return false; 198 | } 199 | 200 | private boolean isScreenshot(String data, long dateTaken) { 201 | final Long time = System.currentTimeMillis() - dateTaken; 202 | if (time > SCREENSHOT_TIMEOUT) { 203 | return false; 204 | } 205 | 206 | if (!TextUtils.isEmpty(data)) { 207 | if (lastDatas.containsKey(data)) { 208 | return false; 209 | } 210 | data = data.toLowerCase(); 211 | 212 | for (String keyWork : KEYWORDS) { 213 | if (data.contains(keyWork)) { 214 | return true; 215 | } 216 | } 217 | } 218 | return false; 219 | } 220 | } 221 | 222 | 223 | private class DataCache extends LinkedHashMap { 224 | private int cacheSize; 225 | 226 | private DataCache(int cacheSize) { 227 | this.cacheSize = cacheSize; 228 | } 229 | 230 | private void put(String data) { 231 | put(data, true); 232 | } 233 | 234 | @Override 235 | protected boolean removeEldestEntry(Entry eldest) { 236 | return size() > this.cacheSize; 237 | } 238 | } 239 | 240 | } 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | /android 48 | /ios 49 | pubspec.lock 50 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7e9793dee1b85a243edd0e06cb1658e98b077561 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # ff_native_screenshot_example 2 | 3 | Demonstrates how to use the ff_native_screenshot plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:ff_native_screenshot/ff_native_screenshot.dart'; 7 | import 'package:permission_handler/permission_handler.dart'; 8 | import 'package:webview_flutter/webview_flutter.dart'; 9 | 10 | void main() { 11 | runApp(const MyApp()); 12 | } 13 | 14 | class MyApp extends StatefulWidget { 15 | const MyApp({Key? key}) : super(key: key); 16 | 17 | @override 18 | State createState() => _MyAppState(); 19 | } 20 | 21 | class _MyAppState extends State { 22 | @override 23 | void initState() { 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return const MaterialApp(home: HomePage()); 30 | } 31 | } 32 | 33 | class HomePage extends StatefulWidget { 34 | const HomePage({Key? key}) : super(key: key); 35 | 36 | @override 37 | State createState() => _HomePageState(); 38 | } 39 | 40 | class _HomePageState extends State with WidgetsBindingObserver { 41 | @override 42 | void initState() { 43 | super.initState(); 44 | init(); 45 | WidgetsBinding.instance.addObserver(this); 46 | } 47 | 48 | Future init() async { 49 | if (Platform.isAndroid) { 50 | await Permission.storage.request(); 51 | } 52 | FfNativeScreenshot().setup(ScreenshotFlutterApiImplements(context)); 53 | await FfNativeScreenshot().startListeningScreenshot(); 54 | 55 | if (mounted) { 56 | setState(() {}); 57 | } 58 | } 59 | 60 | @override 61 | void dispose() { 62 | FfNativeScreenshot().stopListeningScreenshot(); 63 | WidgetsBinding.instance.removeObserver(this); 64 | super.dispose(); 65 | } 66 | 67 | bool? listening; 68 | @override 69 | void didChangeAppLifecycleState(AppLifecycleState state) { 70 | super.didChangeAppLifecycleState(state); 71 | switch (state) { 72 | case AppLifecycleState.resumed: 73 | if (listening == true && !FfNativeScreenshot().listening) { 74 | FfNativeScreenshot().startListeningScreenshot(); 75 | } 76 | break; 77 | case AppLifecycleState.paused: 78 | listening = FfNativeScreenshot().listening; 79 | if (listening == true) { 80 | FfNativeScreenshot().stopListeningScreenshot(); 81 | } 82 | 83 | break; 84 | default: 85 | } 86 | } 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | return Scaffold( 91 | appBar: AppBar( 92 | title: const Text('Native Screenshot'), 93 | actions: [ 94 | IconButton( 95 | onPressed: () { 96 | takeScreenshot(context); 97 | }, 98 | icon: const Icon( 99 | Icons.screenshot, 100 | ), 101 | ), 102 | IconButton( 103 | onPressed: () { 104 | _listeningScreenshotTapped(); 105 | }, 106 | icon: FfNativeScreenshot().listening 107 | ? const Icon( 108 | Icons.stop, 109 | ) 110 | : const Icon(Icons.start)) 111 | ], 112 | ), 113 | body: WebView( 114 | initialUrl: 'https://flutter.cn', 115 | // 116 | // android 117 | // initExpensiveAndroidView 118 | // On some Android devices, transparent backgrounds can cause 119 | // rendering issues on the non hybrid composition 120 | // AndroidViewSurface. This switches the WebView to Hybrid 121 | // Composition when the background color is not 100% opaque. 122 | // hybridComposition: 123 | // backgroundColor != null && backgroundColor.opacity < 1.0, 124 | 125 | // if we don't use initExpensiveAndroidView, can't get shotshot from currentActivity 126 | backgroundColor: 127 | Platform.isAndroid ? Colors.white.withOpacity(0.99) : null, 128 | ), 129 | ); 130 | } 131 | 132 | Future _listeningScreenshotTapped() async { 133 | if (FfNativeScreenshot().listening) { 134 | await FfNativeScreenshot().stopListeningScreenshot(); 135 | } else { 136 | await FfNativeScreenshot().startListeningScreenshot(); 137 | } 138 | if (mounted) { 139 | setState(() {}); 140 | } 141 | } 142 | 143 | Future takeScreenshot(BuildContext context) async { 144 | Uint8List? data = await FfNativeScreenshot().takeScreenshot(); 145 | showScreenshotDialog(data, context); 146 | } 147 | 148 | static void showScreenshotDialog(Uint8List? bytes, BuildContext context) { 149 | if (bytes != null) { 150 | showDialog( 151 | context: context, 152 | builder: (context) { 153 | return GestureDetector( 154 | behavior: HitTestBehavior.translucent, 155 | onTap: () { 156 | Navigator.of(context).pop(); 157 | }, 158 | child: Padding( 159 | padding: const EdgeInsets.all(50), 160 | child: Image.memory( 161 | bytes, 162 | fit: BoxFit.contain, 163 | ), 164 | ), 165 | ); 166 | }, 167 | ); 168 | } 169 | } 170 | } 171 | 172 | class ScreenshotFlutterApiImplements extends ScreenshotFlutterApi { 173 | ScreenshotFlutterApiImplements(this.context); 174 | final BuildContext context; 175 | @override 176 | Future onTakeScreenshot(Uint8List? data) async { 177 | if (kDebugMode) { 178 | print('onTakeScreenshot:${data?.length}'); 179 | } 180 | data ??= await FfNativeScreenshot().takeScreenshot(); 181 | _HomePageState.showScreenshotDialog(data, context); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ff_native_screenshot_example 2 | description: Demonstrates how to use the ff_native_screenshot plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.16.1 <3.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | permission_handler: ^9.2.0 21 | ff_native_screenshot: 22 | # When depending on this package from a real application you should use: 23 | # ff_native_screenshot: ^x.y.z 24 | # See https://dart.dev/tools/pub/dependencies#version-constraints 25 | # The example app is bundled with the plugin so we use a path dependency on 26 | # the parent directory to use the current plugin's version. 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.2 32 | webview_flutter: any 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | 37 | # The "flutter_lints" package below contains a set of recommended lints to 38 | # encourage good coding practices. The lint set provided by the package is 39 | # activated in the `analysis_options.yaml` file located at the root of your 40 | # package. See that file for information about deactivating specific lint 41 | # rules and activating additional ones. 42 | flutter_lints: ^1.0.0 43 | 44 | # For information on the generic Dart part of this file, see the 45 | # following page: https://dart.dev/tools/pub/pubspec 46 | 47 | # The following section is specific to Flutter. 48 | flutter: 49 | 50 | # The following line ensures that the Material Icons font is 51 | # included with your application, so that you can use the icons in 52 | # the material Icons class. 53 | uses-material-design: true 54 | 55 | # To add assets to your application, add an assets section, like this: 56 | # assets: 57 | # - images/a_dot_burr.jpeg 58 | # - images/a_dot_ham.jpeg 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.dev/assets-and-images/#resolution-aware. 62 | 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.dev/assets-and-images/#from-packages 65 | 66 | # To add custom fonts to your application, add a fonts section here, 67 | # in this "flutter" section. Each entry in this list should have a 68 | # "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.dev/custom-fonts/#from-packages 85 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/ff_native_screenshot/74525b1426c1b5417ce39b6728923405d2f59a00/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/FfNativeScreenshotPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "ScreenshotApi.h" 3 | 4 | @interface FfNativeScreenshotPlugin : NSObject 5 | @end 6 | -------------------------------------------------------------------------------- /ios/Classes/FfNativeScreenshotPlugin.m: -------------------------------------------------------------------------------- 1 | #import "FfNativeScreenshotPlugin.h" 2 | static FLTScreenshotFlutterApi *screenshotFlutterApi; 3 | @implementation FfNativeScreenshotPlugin 4 | + (void)registerWithRegistrar:(NSObject*)registrar { 5 | FfNativeScreenshotPlugin* instance = [[FfNativeScreenshotPlugin alloc] init]; 6 | FLTScreenshotHostApiSetup(registrar.messenger,instance); 7 | screenshotFlutterApi = [[FLTScreenshotFlutterApi alloc] initWithBinaryMessenger: registrar.messenger ]; 8 | } 9 | 10 | 11 | - (void)takeScreenshotWithCompletion:(nonnull void (^)(FlutterStandardTypedData * _Nullable, FlutterError * _Nullable))completion { 12 | FlutterStandardTypedData *data = [self takeScreenshot]; 13 | completion(data,nil); 14 | } 15 | 16 | - (void)startListeningScreenshotWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { 17 | [[NSNotificationCenter defaultCenter] addObserver: self 18 | selector:@selector(onTakeScreenShoot:) 19 | name:UIApplicationUserDidTakeScreenshotNotification object:nil]; 20 | } 21 | 22 | 23 | - (void)stopListeningScreenshotWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error{ 24 | [[NSNotificationCenter defaultCenter] removeObserver: self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; 25 | } 26 | 27 | - (void)onTakeScreenShoot:(NSNotification *)notification{ 28 | 29 | FlutterStandardTypedData *data = [self takeScreenshot]; 30 | [screenshotFlutterApi onTakeScreenshotData:data completion:^(NSError * _Nullable error) { 31 | 32 | }]; 33 | } 34 | 35 | - (FlutterStandardTypedData *)takeScreenshot { 36 | @try { 37 | UIApplication *app= [UIApplication sharedApplication]; 38 | UIView *view= [[[[app delegate] window] rootViewController] view]; 39 | 40 | UIGraphicsBeginImageContextWithOptions([view bounds].size,[view isOpaque],[UIScreen mainScreen].scale); 41 | 42 | [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:TRUE]; 43 | 44 | UIImage *image= UIGraphicsGetImageFromCurrentImageContext(); 45 | 46 | UIGraphicsEndImageContext(); 47 | 48 | 49 | NSData *imageData= UIImageJPEGRepresentation(image, 1.0f); 50 | FlutterStandardTypedData *data = [FlutterStandardTypedData typedDataWithBytes:imageData]; 51 | return data; 52 | } @catch (NSException *exception) { 53 | NSLog(@"%@",exception); 54 | return nil; 55 | } @finally { 56 | 57 | } 58 | 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /ios/Classes/ScreenshotApi.h: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v3.1.0), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | #import 4 | @protocol FlutterBinaryMessenger; 5 | @protocol FlutterMessageCodec; 6 | @class FlutterError; 7 | @class FlutterStandardTypedData; 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | 12 | /// The codec used by FLTScreenshotHostApi. 13 | NSObject *FLTScreenshotHostApiGetCodec(void); 14 | 15 | @protocol FLTScreenshotHostApi 16 | - (void)takeScreenshotWithCompletion:(void(^)(FlutterStandardTypedData *_Nullable, FlutterError *_Nullable))completion; 17 | - (void)startListeningScreenshotWithError:(FlutterError *_Nullable *_Nonnull)error; 18 | - (void)stopListeningScreenshotWithError:(FlutterError *_Nullable *_Nonnull)error; 19 | @end 20 | 21 | extern void FLTScreenshotHostApiSetup(id binaryMessenger, NSObject *_Nullable api); 22 | 23 | /// The codec used by FLTScreenshotFlutterApi. 24 | NSObject *FLTScreenshotFlutterApiGetCodec(void); 25 | 26 | @interface FLTScreenshotFlutterApi : NSObject 27 | - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; 28 | - (void)onTakeScreenshotData:(nullable FlutterStandardTypedData *)data completion:(void(^)(NSError *_Nullable))completion; 29 | @end 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /ios/Classes/ScreenshotApi.m: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v3.1.0), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | #import "ScreenshotApi.h" 4 | #import 5 | 6 | #if !__has_feature(objc_arc) 7 | #error File requires ARC to be enabled. 8 | #endif 9 | 10 | static NSDictionary *wrapResult(id result, FlutterError *error) { 11 | NSDictionary *errorDict = (NSDictionary *)[NSNull null]; 12 | if (error) { 13 | errorDict = @{ 14 | @"code": (error.code ?: [NSNull null]), 15 | @"message": (error.message ?: [NSNull null]), 16 | @"details": (error.details ?: [NSNull null]), 17 | }; 18 | } 19 | return @{ 20 | @"result": (result ?: [NSNull null]), 21 | @"error": errorDict, 22 | }; 23 | } 24 | static id GetNullableObject(NSDictionary* dict, id key) { 25 | id result = dict[key]; 26 | return (result == [NSNull null]) ? nil : result; 27 | } 28 | static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) { 29 | id result = array[key]; 30 | return (result == [NSNull null]) ? nil : result; 31 | } 32 | 33 | 34 | 35 | @interface FLTScreenshotHostApiCodecReader : FlutterStandardReader 36 | @end 37 | @implementation FLTScreenshotHostApiCodecReader 38 | @end 39 | 40 | @interface FLTScreenshotHostApiCodecWriter : FlutterStandardWriter 41 | @end 42 | @implementation FLTScreenshotHostApiCodecWriter 43 | @end 44 | 45 | @interface FLTScreenshotHostApiCodecReaderWriter : FlutterStandardReaderWriter 46 | @end 47 | @implementation FLTScreenshotHostApiCodecReaderWriter 48 | - (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { 49 | return [[FLTScreenshotHostApiCodecWriter alloc] initWithData:data]; 50 | } 51 | - (FlutterStandardReader *)readerWithData:(NSData *)data { 52 | return [[FLTScreenshotHostApiCodecReader alloc] initWithData:data]; 53 | } 54 | @end 55 | 56 | NSObject *FLTScreenshotHostApiGetCodec() { 57 | static dispatch_once_t sPred = 0; 58 | static FlutterStandardMessageCodec *sSharedObject = nil; 59 | dispatch_once(&sPred, ^{ 60 | FLTScreenshotHostApiCodecReaderWriter *readerWriter = [[FLTScreenshotHostApiCodecReaderWriter alloc] init]; 61 | sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; 62 | }); 63 | return sSharedObject; 64 | } 65 | 66 | 67 | void FLTScreenshotHostApiSetup(id binaryMessenger, NSObject *api) { 68 | { 69 | FlutterBasicMessageChannel *channel = 70 | [[FlutterBasicMessageChannel alloc] 71 | initWithName:@"dev.flutter.pigeon.ScreenshotHostApi.takeScreenshot" 72 | binaryMessenger:binaryMessenger 73 | codec:FLTScreenshotHostApiGetCodec() ]; 74 | if (api) { 75 | NSCAssert([api respondsToSelector:@selector(takeScreenshotWithCompletion:)], @"FLTScreenshotHostApi api (%@) doesn't respond to @selector(takeScreenshotWithCompletion:)", api); 76 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 77 | [api takeScreenshotWithCompletion:^(FlutterStandardTypedData *_Nullable output, FlutterError *_Nullable error) { 78 | callback(wrapResult(output, error)); 79 | }]; 80 | }]; 81 | } 82 | else { 83 | [channel setMessageHandler:nil]; 84 | } 85 | } 86 | { 87 | FlutterBasicMessageChannel *channel = 88 | [[FlutterBasicMessageChannel alloc] 89 | initWithName:@"dev.flutter.pigeon.ScreenshotHostApi.startListeningScreenshot" 90 | binaryMessenger:binaryMessenger 91 | codec:FLTScreenshotHostApiGetCodec() ]; 92 | if (api) { 93 | NSCAssert([api respondsToSelector:@selector(startListeningScreenshotWithError:)], @"FLTScreenshotHostApi api (%@) doesn't respond to @selector(startListeningScreenshotWithError:)", api); 94 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 95 | FlutterError *error; 96 | [api startListeningScreenshotWithError:&error]; 97 | callback(wrapResult(nil, error)); 98 | }]; 99 | } 100 | else { 101 | [channel setMessageHandler:nil]; 102 | } 103 | } 104 | { 105 | FlutterBasicMessageChannel *channel = 106 | [[FlutterBasicMessageChannel alloc] 107 | initWithName:@"dev.flutter.pigeon.ScreenshotHostApi.stopListeningScreenshot" 108 | binaryMessenger:binaryMessenger 109 | codec:FLTScreenshotHostApiGetCodec() ]; 110 | if (api) { 111 | NSCAssert([api respondsToSelector:@selector(stopListeningScreenshotWithError:)], @"FLTScreenshotHostApi api (%@) doesn't respond to @selector(stopListeningScreenshotWithError:)", api); 112 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 113 | FlutterError *error; 114 | [api stopListeningScreenshotWithError:&error]; 115 | callback(wrapResult(nil, error)); 116 | }]; 117 | } 118 | else { 119 | [channel setMessageHandler:nil]; 120 | } 121 | } 122 | } 123 | @interface FLTScreenshotFlutterApiCodecReader : FlutterStandardReader 124 | @end 125 | @implementation FLTScreenshotFlutterApiCodecReader 126 | @end 127 | 128 | @interface FLTScreenshotFlutterApiCodecWriter : FlutterStandardWriter 129 | @end 130 | @implementation FLTScreenshotFlutterApiCodecWriter 131 | @end 132 | 133 | @interface FLTScreenshotFlutterApiCodecReaderWriter : FlutterStandardReaderWriter 134 | @end 135 | @implementation FLTScreenshotFlutterApiCodecReaderWriter 136 | - (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { 137 | return [[FLTScreenshotFlutterApiCodecWriter alloc] initWithData:data]; 138 | } 139 | - (FlutterStandardReader *)readerWithData:(NSData *)data { 140 | return [[FLTScreenshotFlutterApiCodecReader alloc] initWithData:data]; 141 | } 142 | @end 143 | 144 | NSObject *FLTScreenshotFlutterApiGetCodec() { 145 | static dispatch_once_t sPred = 0; 146 | static FlutterStandardMessageCodec *sSharedObject = nil; 147 | dispatch_once(&sPred, ^{ 148 | FLTScreenshotFlutterApiCodecReaderWriter *readerWriter = [[FLTScreenshotFlutterApiCodecReaderWriter alloc] init]; 149 | sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; 150 | }); 151 | return sSharedObject; 152 | } 153 | 154 | 155 | @interface FLTScreenshotFlutterApi () 156 | @property (nonatomic, strong) NSObject *binaryMessenger; 157 | @end 158 | 159 | @implementation FLTScreenshotFlutterApi 160 | 161 | - (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { 162 | self = [super init]; 163 | if (self) { 164 | _binaryMessenger = binaryMessenger; 165 | } 166 | return self; 167 | } 168 | - (void)onTakeScreenshotData:(nullable FlutterStandardTypedData *)arg_data completion:(void(^)(NSError *_Nullable))completion { 169 | FlutterBasicMessageChannel *channel = 170 | [FlutterBasicMessageChannel 171 | messageChannelWithName:@"dev.flutter.pigeon.ScreenshotFlutterApi.onTakeScreenshot" 172 | binaryMessenger:self.binaryMessenger 173 | codec:FLTScreenshotFlutterApiGetCodec()]; 174 | [channel sendMessage:@[arg_data ?: [NSNull null]] reply:^(id reply) { 175 | completion(nil); 176 | }]; 177 | } 178 | @end 179 | -------------------------------------------------------------------------------- /ios/ff_native_screenshot.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint ff_native_screenshot.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'ff_native_screenshot' 7 | s.version = '0.0.1' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.public_header_files = 'Classes/**/*.h' 18 | s.dependency 'Flutter' 19 | s.platform = :ios, '9.0' 20 | 21 | # Flutter.framework does not contain a i386 slice. 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 23 | end 24 | -------------------------------------------------------------------------------- /lib/ff_native_screenshot.dart: -------------------------------------------------------------------------------- 1 | library ff_native_screenshot; 2 | 3 | import 'dart:typed_data'; 4 | import 'src/ff_native_screenshot.dart'; 5 | 6 | export 'src/ff_native_screenshot.dart'; 7 | 8 | /// The util of NativeScreenshot 9 | class FfNativeScreenshot { 10 | factory FfNativeScreenshot() => _ffNativeScreenshot; 11 | FfNativeScreenshot._(); 12 | static final FfNativeScreenshot _ffNativeScreenshot = FfNativeScreenshot._(); 13 | final ScreenshotHostApi _flutterScreenshotApi = ScreenshotHostApi(); 14 | 15 | /// take screenshot by native 16 | Future takeScreenshot() => _flutterScreenshotApi.takeScreenshot(); 17 | 18 | /// ScreenshotFlutterApi setup 19 | void setup(ScreenshotFlutterApi api) => ScreenshotFlutterApi.setup(api); 20 | 21 | bool _listening = false; 22 | 23 | /// whether is listening Screenshot 24 | bool get listening => _listening; 25 | 26 | /// start listening Screenshot 27 | Future startListeningScreenshot() async { 28 | try { 29 | if (!_listening) { 30 | _listening = true; 31 | _flutterScreenshotApi.startListeningScreenshot(); 32 | } 33 | } catch (e) { 34 | _listening = false; 35 | } 36 | } 37 | 38 | /// stop listening Screenshot 39 | Future stopListeningScreenshot() async { 40 | try { 41 | if (_listening) { 42 | _listening = false; 43 | _flutterScreenshotApi.stopListeningScreenshot(); 44 | } 45 | } catch (e) { 46 | _listening = true; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/ff_native_screenshot.dart: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v3.1.0), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name 4 | // @dart = 2.12 5 | import 'dart:async'; 6 | import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; 7 | 8 | import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; 9 | import 'package:flutter/services.dart'; 10 | 11 | class _ScreenshotHostApiCodec extends StandardMessageCodec { 12 | const _ScreenshotHostApiCodec(); 13 | } 14 | 15 | class ScreenshotHostApi { 16 | /// Constructor for [ScreenshotHostApi]. The [binaryMessenger] named argument is 17 | /// available for dependency injection. If it is left null, the default 18 | /// BinaryMessenger will be used which routes to the host platform. 19 | ScreenshotHostApi({BinaryMessenger? binaryMessenger}) 20 | : _binaryMessenger = binaryMessenger; 21 | 22 | final BinaryMessenger? _binaryMessenger; 23 | 24 | static const MessageCodec codec = _ScreenshotHostApiCodec(); 25 | 26 | Future takeScreenshot() async { 27 | final BasicMessageChannel channel = BasicMessageChannel( 28 | 'dev.flutter.pigeon.ScreenshotHostApi.takeScreenshot', codec, 29 | binaryMessenger: _binaryMessenger); 30 | final Map? replyMap = 31 | await channel.send(null) as Map?; 32 | if (replyMap == null) { 33 | throw PlatformException( 34 | code: 'channel-error', 35 | message: 'Unable to establish connection on channel.', 36 | ); 37 | } else if (replyMap['error'] != null) { 38 | final Map error = 39 | (replyMap['error'] as Map?)!; 40 | throw PlatformException( 41 | code: (error['code'] as String?)!, 42 | message: error['message'] as String?, 43 | details: error['details'], 44 | ); 45 | } else { 46 | return (replyMap['result'] as Uint8List?); 47 | } 48 | } 49 | 50 | Future startListeningScreenshot() async { 51 | final BasicMessageChannel channel = BasicMessageChannel( 52 | 'dev.flutter.pigeon.ScreenshotHostApi.startListeningScreenshot', codec, 53 | binaryMessenger: _binaryMessenger); 54 | final Map? replyMap = 55 | await channel.send(null) as Map?; 56 | if (replyMap == null) { 57 | throw PlatformException( 58 | code: 'channel-error', 59 | message: 'Unable to establish connection on channel.', 60 | ); 61 | } else if (replyMap['error'] != null) { 62 | final Map error = 63 | (replyMap['error'] as Map?)!; 64 | throw PlatformException( 65 | code: (error['code'] as String?)!, 66 | message: error['message'] as String?, 67 | details: error['details'], 68 | ); 69 | } else { 70 | return; 71 | } 72 | } 73 | 74 | Future stopListeningScreenshot() async { 75 | final BasicMessageChannel channel = BasicMessageChannel( 76 | 'dev.flutter.pigeon.ScreenshotHostApi.stopListeningScreenshot', codec, 77 | binaryMessenger: _binaryMessenger); 78 | final Map? replyMap = 79 | await channel.send(null) as Map?; 80 | if (replyMap == null) { 81 | throw PlatformException( 82 | code: 'channel-error', 83 | message: 'Unable to establish connection on channel.', 84 | ); 85 | } else if (replyMap['error'] != null) { 86 | final Map error = 87 | (replyMap['error'] as Map?)!; 88 | throw PlatformException( 89 | code: (error['code'] as String?)!, 90 | message: error['message'] as String?, 91 | details: error['details'], 92 | ); 93 | } else { 94 | return; 95 | } 96 | } 97 | } 98 | 99 | class _ScreenshotFlutterApiCodec extends StandardMessageCodec { 100 | const _ScreenshotFlutterApiCodec(); 101 | } 102 | 103 | abstract class ScreenshotFlutterApi { 104 | static const MessageCodec codec = _ScreenshotFlutterApiCodec(); 105 | 106 | void onTakeScreenshot(Uint8List? data); 107 | static void setup(ScreenshotFlutterApi? api, 108 | {BinaryMessenger? binaryMessenger}) { 109 | { 110 | final BasicMessageChannel channel = BasicMessageChannel( 111 | 'dev.flutter.pigeon.ScreenshotFlutterApi.onTakeScreenshot', codec, 112 | binaryMessenger: binaryMessenger); 113 | if (api == null) { 114 | channel.setMessageHandler(null); 115 | } else { 116 | channel.setMessageHandler((Object? message) async { 117 | assert(message != null, 118 | 'Argument for dev.flutter.pigeon.ScreenshotFlutterApi.onTakeScreenshot was null.'); 119 | final List args = (message as List?)!; 120 | final Uint8List? arg_data = (args[0] as Uint8List?); 121 | api.onTakeScreenshot(arg_data); 122 | return; 123 | }); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pigeon.sh: -------------------------------------------------------------------------------- 1 | 2 | # run_pigeon.h 3 | flutter pub run pigeon \ 4 | --input pigeons/ff_native_screenshot.dart \ 5 | --dart_out lib/src/ff_native_screenshot.dart \ 6 | --objc_header_out ios/Classes/ScreenshotApi.h \ 7 | --objc_source_out ios/Classes/ScreenshotApi.m \ 8 | --objc_prefix FLT \ 9 | --java_out android/src/main/java/com/fluttercandies/plugins/ff_native_screenshot/ScreenshotApi.java \ 10 | --java_package "com.fluttercandies.plugins.ff_native_screenshot" 11 | 12 | 13 | -------------------------------------------------------------------------------- /pigeons/ff_native_screenshot.dart: -------------------------------------------------------------------------------- 1 | import 'package:pigeon/pigeon.dart'; 2 | 3 | /// Flutter call Native 4 | @HostApi() 5 | abstract class ScreenshotHostApi { 6 | @async 7 | Uint8List? takeScreenshot(); 8 | 9 | void startListeningScreenshot(); 10 | void stopListeningScreenshot(); 11 | } 12 | 13 | /// Native call Flutter 14 | @FlutterApi() 15 | abstract class ScreenshotFlutterApi { 16 | void onTakeScreenshot(Uint8List? data); 17 | } 18 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ff_native_screenshot 2 | description: A Flutter plugin to take or listen screenshot(support Platform Views) for Android and iOS with native code. 3 | version: 1.0.0 4 | homepage: https://github.com/fluttercandies/ff_native_screenshot 5 | 6 | environment: 7 | sdk: ">=2.16.1 <3.0.0" 8 | flutter: ">=2.5.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | flutter_lints: ^1.0.0 18 | pigeon: ^3.1.0 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://dart.dev/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | # This section identifies this Flutter project as a plugin project. 25 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 26 | # be modified. They are used by the tooling to maintain consistency when 27 | # adding or updating assets for this project. 28 | plugin: 29 | platforms: 30 | android: 31 | package: com.fluttercandies.plugins.ff_native_screenshot 32 | pluginClass: FfNativeScreenshotPlugin 33 | ios: 34 | pluginClass: FfNativeScreenshotPlugin 35 | 36 | # To add assets to your plugin package, add an assets section, like this: 37 | # assets: 38 | # - images/a_dot_burr.jpeg 39 | # - images/a_dot_ham.jpeg 40 | # 41 | # For details regarding assets in packages, see 42 | # https://flutter.dev/assets-and-images/#from-packages 43 | # 44 | # An image asset can refer to one or more resolution-specific "variants", see 45 | # https://flutter.dev/assets-and-images/#resolution-aware. 46 | 47 | # To add custom fonts to your plugin package, add a fonts section here, 48 | # in this "flutter" section. Each entry in this list should have a 49 | # "family" key with the font family name, and a "fonts" key with a 50 | # list giving the asset and other descriptors for the font. For 51 | # example: 52 | # fonts: 53 | # - family: Schyler 54 | # fonts: 55 | # - asset: fonts/Schyler-Regular.ttf 56 | # - asset: fonts/Schyler-Italic.ttf 57 | # style: italic 58 | # - family: Trajan Pro 59 | # fonts: 60 | # - asset: fonts/TrajanPro.ttf 61 | # - asset: fonts/TrajanPro_Bold.ttf 62 | # weight: 700 63 | # 64 | # For details regarding fonts in packages, see 65 | # https://flutter.dev/custom-fonts/#from-packages 66 | 67 | # flutter create -i objc -a java --org com.fluttercandies.plugins --template plugin --platforms ios,android ff_native_screenshot 68 | --------------------------------------------------------------------------------