前言
在有 pigeon | Dart Package (flutter-io.cn) 之前,想写一个原生交互,是相当麻烦的一件事情。项目里面用 pigeon 来写了几个原生插件,用下来感觉还是很方便的,但是在开始使用的时候,我发现这方面的文章几乎没有,单端选手(几乎都是经常安卓端)很多,而且常常只是介绍 Flutter
端到原生端的通信,而原生端到 Flutter
端的几乎看不到介绍。
做项目的时候遇到用 RepaintBoundary
无法给 Webview
截屏的问题,查了下,是个已知的问题。
RepaintBoundary Cannot take Screenshot of Platform Views
然后上 Dart packages (flutter-io.cn) 试用了几个,不能说不能用,而是完全不能用。
- 依然使用的
FlutterView/FlutterRenderer
去获取的getBitmap
,结果当然还是失败。 - 报错
Software rendering doesn't support hardware bitmaps
。 Google 下要用 PixelCopy 。android - java.lang.IllegalStateException: Software rendering doesn't support hardware bitmaps - Stack Overflow - 项目用了
flutter_boost
,导致插件获取Controller
失败。
最后还是决定自己写好了。
创建一个插件
flutter create -i objc -a java --org com.fluttercandies.plugins --template plugin --platforms ios,android ff_native_screenshot
使用 pigeon
添加引用
dev_dependencies:
pigeon: ^3.1.0
增加生成类
-
@HostApi()
标记的,是用于 Flutter 调用原生的方法。 -
@FlutterApi()
标记的,是用于原生调用 Flutter 的方法。 -
@async
如果原生的方法,是异步回调那种,你就可以使用这个标记 - 只支持
dart
的基础类型
/// Flutter call Native
@HostApi()
abstract class ScreenshotHostApi {
@async
Uint8List? takeScreenshot();
void startListeningScreenshot();
void stopListeningScreenshot();
}
/// Native call Flutter
@FlutterApi()
abstract class ScreenshotFlutterApi {
void onTakeScreenshot(Uint8List? data);
}
命令脚本
我习惯放在脚本里面执行,免得每次都要手打。
执行 ./pigeon.sh
脚本内容如下:
flutter pub run pigeon \
--input pigeons/ff_native_screenshot.dart \
--dart_out lib/src/ff_native_screenshot.dart \
--objc_header_out ios/Classes/ScreenshotApi.h \
--objc_source_out ios/Classes/ScreenshotApi.m \
--objc_prefix FLT \
--java_out android/src/main/java/com/fluttercandies/plugins/ff_native_screenshot/ScreenshotApi.java \
--java_package "com.fluttercandies.plugins.ff_native_screenshot"
iOS端实现
修改之前:
FfNativeScreenshotPlugin.h
#import
**@interface** FfNativeScreenshotPlugin : NSObject
**@end**
FfNativeScreenshotPlugin.m
#import "FfNativeScreenshotPlugin.h"
**@implementation** FfNativeScreenshotPlugin
+ (**void**)registerWithRegistrar:(NSObject*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"ff_native_screenshot"
binaryMessenger:[registrar messenger]];
FfNativeScreenshotPlugin* instance = [[FfNativeScreenshotPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (**void**)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
**if** ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} **else** {
result(FlutterMethodNotImplemented);
}
}
**@end**
ScreenshotHostApi
FfNativeScreenshotPlugin.h
增加继承 FLTScreenshotHostApi
#import
#import "ScreenshotApi.h""
**@interface** FfNativeScreenshotPlugin : NSObject
**@end**
FfNativeScreenshotPlugin.m
#import "FfNativeScreenshotPlugin.h"
**@implementation** FfNativeScreenshotPlugin
+ (**void**)registerWithRegistrar:(NSObject*)registrar {
FfNativeScreenshotPlugin* instance = [[FfNativeScreenshotPlugin alloc] init];
FLTScreenshotHostApiSetup(registrar.messenger,instance);
}
- (**void**)startListeningScreenshotWithError:(FlutterError * **_Nullable** **__autoreleasing** * **_Nonnull**)error {
}
- (**void**)stopListeningScreenshotWithError:(FlutterError * **_Nullable** **__autoreleasing** * **_Nonnull**)error {
}
- (**void**)takeScreenshotWithCompletion:(**nonnull** **void** (^)(FlutterStandardTypedData * **_Nullable**, FlutterError * **_Nullable**))completion {
}
**@end**
ScreenshotFlutterApi
我们需要注册并且保存 ScreenshotFlutterApi
,用于原生给 Flutter
发送消息
FfNativeScreenshotPlugin.m
#import "FfNativeScreenshotPlugin.h"
**static** FLTScreenshotFlutterApi *screenshotFlutterApi;
**@implementation** FfNativeScreenshotPlugin
+ (**void**)registerWithRegistrar:(NSObject*)registrar {
screenshotFlutterApi = [[FLTScreenshotFlutterApi alloc] initWithBinaryMessenger: registrar.messenger ];
}
**@end**
安卓端实现
修改之前:
public class FfNativeScreenshotPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "ff_native_screenshot");
channel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}
ScreenshotHostApi
我们需要将 MethodCallHandler
替换成 ScreenshotHostApi
, 移除无用代码
public class FfNativeScreenshotPlugin implements FlutterPlugin, ScreenshotApi.ScreenshotHostApi {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
ScreenshotApi.ScreenshotHostApi.setup(flutterPluginBinding.getBinaryMessenger(), this);
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
ScreenshotApi.ScreenshotHostApi.setup(binding.getBinaryMessenger(), null);
}
@Override
public void takeScreenshot(ScreenshotApi.Result result) {
}
@Override
public void startListeningScreenshot() {
}
@Override
public void stopListeningScreenshot() {
}
}
ScreenshotFlutterApi
我们需要注册并且保存 ScreenshotFlutterApi
,用于原生给 Flutter
发送消息
private ScreenshotApi.ScreenshotFlutterApi screenshotFlutterApi;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
screenshotFlutterApi = new ScreenshotApi.ScreenshotFlutterApi(flutterPluginBinding.getBinaryMessenger());
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
screenshotFlutterApi = null;
}
剩下的工作就是对这些方法进行实现,感谢 google
和 pub.dev
上的各个插件大佬, 让我能学习(白嫖)到原生知识,也感谢群里大佬们对原生部分代码的检查和建议。
封装和使用
封装
我们可以做一些封装,将 lib/ff_native_screenshot.dart
修改为如下:
library ff_native_screenshot;
import 'dart:typed_data';
import 'src/ff_native_screenshot.dart';
export 'src/ff_native_screenshot.dart';
/// The util of NativeScreenshot
class FfNativeScreenshot {
factory FfNativeScreenshot() => _ffNativeScreenshot;
FfNativeScreenshot._();
static final FfNativeScreenshot _ffNativeScreenshot = FfNativeScreenshot._();
final ScreenshotHostApi _flutterScreenshotApi = ScreenshotHostApi();
/// take screenshot by native
Future takeScreenshot() => _flutterScreenshotApi.takeScreenshot();
/// ScreenshotFlutterApi setup
void setup(ScreenshotFlutterApi api) => ScreenshotFlutterApi.setup(api);
bool _listening = false;
/// whether is listening Screenshot
bool get listening => _listening;
/// start listening Screenshot
void startListeningScreenshot() {
_listening = true;
_flutterScreenshotApi.startListeningScreenshot();
}
/// stop listening Screenshot
void stopListeningScreenshot() {
_listening = false;
_flutterScreenshotApi.stopListeningScreenshot();
}
}
使用
在 pubspec.yaml
中加入引入
dependencies:
ff_native_screenshot: any
截图
Uint8List? data = await FfNativeScreenshot().takeScreenshot();
监听系统截图
我们需要去实现 ScreenshotFlutterApi
,在 onTakeScreenshot
方法中获取到系统截图时候返回的字节流。
@override
void initState() {
super.initState();
FfNativeScreenshot().setup(ScreenshotFlutterApiImplements());
FfNativeScreenshot().startListeningScreenshot();
}
@override
void dispose() {
FfNativeScreenshot().stopListeningScreenshot();
super.dispose();
}
class ScreenshotFlutterApiImplements extends ScreenshotFlutterApi {
ScreenshotFlutterApiImplements();
@override
Future onTakeScreenshot(Uint8List? data) async {
// if it has something error
// you can call takeScreenshot
data ??= await FfNativeScreenshot().takeScreenshot();
}
}
结语
实际上,插件只要你第一次双端实现好了,之后想加方法,改方法,都是很方便的事情。比起来需要手写 if else
, 我更喜欢pigeon
的多端接口一致性,对于不太懂原生的开发,还是很友好的。
当然,写一个 Flutter
的原生插件,你将需要掌握:
- dart
- java
- object-c
- kotlin
- swift
别问为什么这么多,你在 google
上面搜索答案的时候就会发现,大佬们的答案往往是各种语言都有 o(╯□╰)o。
一次开发,5倍快乐,快来一起写 Flutter
原生插件吧!
ff_native_screenshot | Flutter Package (flutter-io.cn) 有需求的童鞋自取。