2020-05-13 17:33:34
Flutter翰守,百度地圖,原生插件
簡(jiǎn)介
先介紹一下蜡峰,這個(gè)插件只是教學(xué)目的,并不完整湿颅。
目前提供了
- 顯示地圖
- 獲取地圖中心坐標(biāo)
- 定位
- 距離計(jì)算
寫(xiě)這個(gè)插件的時(shí)候载绿,
我只學(xué)了 Flutter 一個(gè)月,Android 沒(méi)學(xué)過(guò)油航,也沒(méi)掌握 Java 崭庸。
沒(méi)想到文章越寫(xiě)越多,分成了3部分谊囚,這是最后一部分怕享,
前2部分鏈接:
地圖插件的關(guān)鍵是 Flutter 端的 AndroidView 和 Android 端的 PlatformView 。
Flutter 如何顯示 Android 視圖
要想讓 Flutter 顯示一個(gè)安卓視圖 镰踏。
- 安卓端需要提供一個(gè) PlatformView 來(lái)生成安卓視圖函筋,即 View 。
- PlatformView 需要用 PlatformViewFactory 來(lái)創(chuàng)建奠伪。
- 最后通過(guò) PlatformViewRegistry 登記 PlatformViewFactory 跌帐。
- 在Flutter 端用一個(gè) AndroidView 去請(qǐng)求 Android 端的 PlatformView 。
Flutter 端
AndroidView 常用的參數(shù):
- viewType
一個(gè)字符串绊率,指定 Android 端登記的某個(gè)視圖谨敛。就像 MethodChannel 的 name 參數(shù)一樣。 - onPlatformViewCreated
一個(gè)回調(diào)函數(shù)滤否,在安卓端的視圖準(zhǔn)備好后調(diào)用脸狸。接受一個(gè) int 型參數(shù)。 - creationParams
傳遞給安卓端的參數(shù)顽聂。類(lèi)型為 dynamic 肥惭《⒁牵可選紊搪。 - creationParamsCodec
給 creationParams 編碼的編碼器,一般是 StandardMessageCodec 全景。如果指定了 creationParams 耀石,這個(gè)不能為空 。
Flutter 請(qǐng)求安卓視圖的時(shí)候,會(huì)傳遞以下參數(shù):
- id 這個(gè) id 從0開(kāi)始滞伟,每次 AndroidView 重建后會(huì)加一揭鳞。重建前會(huì)檢查 viewType 和之前的是否相同。
- viewType
- width
- height
- direction
- params 如果 AndroidView 的 creationParams 不會(huì)空梆奈,會(huì)加上這個(gè)野崇。
向安卓發(fā)送請(qǐng)求,就是通過(guò)我們熟悉的 MethodChannel 亩钟。
安卓端會(huì)返回一個(gè) textureId 供 TextureLayer 使用乓梨。
Android 端
Android 端收到請(qǐng)求后,
會(huì)在已登記的 PlatformViewFactory 中找到和傳過(guò)來(lái)的 viewType 對(duì)應(yīng)的那個(gè)清酥。
有了 PlatformViewFactory 扶镀,就可以得到我們的地圖了。還會(huì)創(chuàng)建一個(gè) SurfaceTexture 焰轻,并給它分配一個(gè) ID 臭觉。這個(gè) ID 也是從 0 開(kāi)始,每次加一辱志。
這個(gè) ID 將作為 Flutter 端請(qǐng)求的結(jié)果返回蝠筑。
看網(wǎng)上的文章,有看到過(guò) GPU 中的紋理 ID 揩懒。會(huì)是這個(gè)從0開(kāi)始的 ID 嗎菱肖?
有了這個(gè) ID ,F(xiàn)lutter 就可以直接顯示 Android 渲染的視圖旭从,不需要經(jīng)過(guò) CPU 傳數(shù)據(jù)了嗎稳强?
不去研究了。
感覺(jué) SurfaceTexture 和 View 應(yīng)該有著什么關(guān)系退疫。
不過(guò)目前沒(méi)發(fā)現(xiàn)鸽素,也沒(méi)覺(jué)得需要知道。
下面從登記開(kāi)始馍忽,說(shuō)一下需要知道的。
PlatformViewRegistry
要想登記 PlatformViewFactory 坝冕,
需要用到 PlatformViewRegistry 的 registerViewFactory()
方法瓦呼。
PlatformViewRegistry 是在應(yīng)用啟動(dòng)后,創(chuàng)建 FlutterEngine 時(shí)創(chuàng)建的磨澡。
怎樣得到它?
如果我們可以直接得到 FlutterEngine 稳摄。
例如在 FlutterActivity 的configureFlutterEngine()
方法中。
那么可以先用 FlutterEngine 的getPlatformViewsController()
方法
得到 PlatformViewsController 尉共。
然后用 PlatformViewsController 的getRegistry()
得到 PlatformViewRegistry 弃锐。如果我們?cè)趯?xiě)插件,可以在
onAttachedToEngine()
中
通過(guò) FlutterPluginBinding 的getPlatformViewRegistry()
獲得剧蚣。
PlatformViewRegistry 的 registerViewFactory()
方法接受2個(gè)參數(shù)旋廷,
- 第一個(gè)是 viewType ,字符串類(lèi)型目尖。是一個(gè)唯一的標(biāo)識(shí)符,用來(lái)標(biāo)識(shí)一個(gè) PlatformViewFactory 瑟曲。
- 第二個(gè)是 PlatformViewFactory 豪治。用來(lái)創(chuàng)建 PlatformView 。
它有個(gè) boolean 類(lèi)型的返回值烦衣。
登記的時(shí)候會(huì)通過(guò) viewType 檢查是否已經(jīng)登記過(guò)掩浙,
如果登記過(guò)返回 false,否則返回 true 厨姚。
PlatformViewFactory
創(chuàng)建 PlatformViewFactory 需要指定一個(gè)解碼器,用于解碼 Flutter 端傳遞過(guò)來(lái)的參數(shù)矾麻。
這個(gè)解碼器要與 Flutter 端的編碼器對(duì)應(yīng)芭梯,一般是 StandardMessageCodec 。
我們需要重寫(xiě) create()
方法甩牺。
有3個(gè)可用的參數(shù):
- context
最開(kāi)始是個(gè) FlutterActivity 累奈,經(jīng)過(guò)層層包裝,最后是一個(gè) ContextWrapper 澎媒。
包含了 InputMethodManager ,InvocationHandler戒努,WindowManager, 重寫(xiě)了getSystemService()
,
這些我都不懂。 - viewId
Flutter 端傳遞過(guò)來(lái)的 id 侍筛。 - args
解碼過(guò)的 Flutter 端傳遞過(guò)來(lái)的參數(shù)
需要返回一個(gè) PlatformView 。
PlatformView
需要重寫(xiě)2個(gè)方法:
-
getView()
返回一個(gè) View 撒穷,就是我們的百度地圖啦。 -
dispose()
做些清理工作禽笑。
當(dāng) Flutter 端的 AndroidView 被 dispose 或重建前,會(huì)調(diào)用這里的 dispose()
蒲每。
下面獻(xiàn)上一個(gè)實(shí)例喻括。
新建一個(gè) Flutter 插件項(xiàng)目
我這里項(xiàng)目名字是 flutter_plugin_demo
。
特定平臺(tái)的包名是 xyz.waixingjiandie.flutter_plugin_demo
望蜡。
新項(xiàng)目的目錄結(jié)構(gòu)
插件的 pubspec.yaml
:
# /pubspec.yaml
name: flutter_plugin_demo
flutter:
plugin:
platforms:
android:
package: xyz.waixingjiandie.flutter_plugin_demo
pluginClass: FlutterPluginDemoPlugin # 指定了 Android 插件的類(lèi)名字
ios:
pluginClass: FlutterPluginDemoPlugin
/lib/flutter_plugin_demo.dart
這個(gè)是插件的API,對(duì)應(yīng) “app-facing package”脖律。
/android/
這個(gè)目錄是Android平臺(tái)的代碼腕侄,對(duì)應(yīng) “platform package”芦疏。
/example/
這里是一個(gè)依賴(lài)我們插件的 Flutter 應(yīng)用示例酸茴,向使用者展示如何使用我們的插件。
這個(gè)示例是可以直接運(yùn)行的兢交,官網(wǎng)建議編寫(xiě)插件前,先運(yùn)行一遍配喳。
打開(kāi)Android模塊
在 Writing custom platform-specific code 這個(gè)不是插件的教程中。
打開(kāi)的路徑是 /android/
被济。
而 Developing packages & plugins 這個(gè)插件教程中涧团,
打開(kāi)的是 /example/android/
。
沒(méi)時(shí)間學(xué)太多 Android 知識(shí)少欺,就不管為什么了。
如果打開(kāi)的是 /android/
畏陕,會(huì)發(fā)現(xiàn)安卓代碼中導(dǎo)入的 Flutter 包無(wú)效仿滔。
打開(kāi) /example/android/
的話,也可以訪問(wèn) /android/
鞠绰,并且 Flutter 包也沒(méi)問(wèn)題。
在我們的項(xiàng)目上右鍵飒焦,選擇 Flutter -> Open Android module in Android Studio 。
打開(kāi)的項(xiàng)目和選擇 /example/android/
是一樣的牺荠。
那我就以這種方式打開(kāi)安卓模塊吧翁巍。
打開(kāi)后灶壶,我們要面向的路徑是 /android/
杈曲。
添加百度地圖SDK
根據(jù)百度地圖文檔 將地圖 SDK 添加到項(xiàng)目中胸懈。
刪掉多余的東西
打開(kāi) FlutterPluginDemoPlugin 類(lèi)所在的文件恰响。
示例插件實(shí)現(xiàn)了 FlutterPlugin
和 MethodCallHandler
。
之所以實(shí)現(xiàn) MethodCallHandler
是因?yàn)槭纠绦蛴玫搅嗽椒ā?br>
為了兼容羔挡,類(lèi)中還有個(gè)多余的 registerWith()
方法间唉。
把多余的去掉后:
package xyz.waixingjiandie.flutter_plugin_demo;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
public class FlutterPluginDemoPlugin implements FlutterPlugin {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
}
參考 百度地圖文檔 ,要顯示地圖是需要 Activity 的利术。
那么我們的插件就要實(shí)現(xiàn) ActivityAware 。
實(shí)現(xiàn) ActivityAware
public class FlutterPluginDemoPlugin implements FlutterPlugin, ActivityAware {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivity() {
}
}
我們的插件又多了4個(gè)要重寫(xiě)的方法被冒。
初始化地圖 SDK
要在哪里初始化 SDK 呢轮蜕?
重寫(xiě)的6個(gè)方法中,
onAttachedToEngine()
是在 FlutterPlugin 關(guān)聯(lián)到 FlutterEngine 時(shí)調(diào)用的跃洛。
發(fā)生在登記插件時(shí) PluginRegistry 的 add()
方法中。
FlutterPlugin 關(guān)聯(lián)到 FlutterEngine 后葱蝗,
會(huì)檢查這個(gè) FlutterPlugin 是否實(shí)現(xiàn)了 ActivityAware 细燎。
如果是,再看 FlutterEngine 是否關(guān)聯(lián)了一個(gè) Activity 玻驻。
如果是,那么執(zhí)行 ActivityAware 中的 onAttachedToActivity()
佛析。
有2個(gè)檢查彪蓬,由于我實(shí)現(xiàn)了 ActivityAware ,所以第一個(gè)檢查通過(guò)档冬。
對(duì)于第2個(gè)桃纯,要從應(yīng)用的啟動(dòng)說(shuō)起披坏。
啟動(dòng)一個(gè)安卓應(yīng)用,啟動(dòng)的通常是一個(gè) Activity 棒拂。
我們 /example/
中的示例是可以啟動(dòng)的。
從示例中可以看到這個(gè) Activity 是 FlutterActivity 谜诫。
如果沒(méi)有重寫(xiě) FlutterActivity 的 shouldAttachEngineToActivity()
方法的話攻旦,
第二個(gè)檢查也是通過(guò)的。
6個(gè)方法中且预,這2個(gè)就是最先執(zhí)行的2個(gè),
onAttachedToEngine()
先于 onAttachedToActivity()
锋谐,
但是不能獲得 Activity 截酷。
初始化地圖 SDK 是不需要 Activity 的。需要的是 ApplicationContext 多搀。
放在哪里都行灾部,但是在 onAttachedToEngine()
獲取 ApplicationContext 方便點(diǎn)。
初始化代碼
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
// 在使用SDK各組件之前初始化context信息赌髓,傳入ApplicationContext
SDKInitializer.initialize(flutterPluginBinding.getApplicationContext());
// 自4.3.0起,百度地圖SDK所有接口均支持百度坐標(biāo)和國(guó)測(cè)局坐標(biāo)夷野,用此方法設(shè)置您使用的坐標(biāo)類(lèi)型.
// 包括BD09LL和GCJ02兩種坐標(biāo)悯搔,默認(rèn)是BD09LL坐標(biāo)。
SDKInitializer.setCoordType(CoordType.BD09LL);
}
通過(guò) FlutterPluginBinding 的 getApplicationContext()
就能得到 ApplicationContext 妒貌。
如果在 onAttachedToActivity()
初始化,
可以用 ActivityPluginBinding 的 getActivity() 得到 Activity 后灌曙,
在用 Activity 的 getApplicationContext()
得到 ApplicationContext 。
SDK 初始化后逆害,創(chuàng)建一個(gè)用于顯示地圖的對(duì)象蚣驼。
PlatformView
PlatformView 需要返回一個(gè) View 隙姿,這個(gè) View 是由百度地圖 SDK 創(chuàng)建的厂捞。
查看百度地圖文檔,
地圖容器分為 GLSurfaceView 和 TextureView 兩種靡馁。
- GLSurfaceView
包括 MapView,MapFragment 和 SupportMapFragment 三種容器臭墨。- TextureView
包括 TexureMapView,TextureMapFragment 和 TextureSupportMapFragment 三種容器尤误。
好像 TextureView 更好一點(diǎn)结缚,我選擇 TexureMapView 。
public class BaiduMapControler implements PlatformView {
private final TextureMapView mapView;
public BaiduMapControler(Activity activity) {
this.mapView = new TextureMapView(activity);
}
@Override
public View getView() {
return mapView;
}
@Override
public void dispose() {
}
}
PlatformViewFactory
public class BaiduMapFactory extends PlatformViewFactory {
private final Activity activity;
public BaiduMapFactory(Activity activity) {
super(StandardMessageCodec.INSTANCE);
this.activity = activity;
}
@Override
public PlatformView create(Context context, int viewId, Object args) {
return new BaiduMapControler(activity);
}
}
登記 PlatformViewFactory
因?yàn)榘俣鹊貓D需要 Activity 红竭,所以在 onAttachedToActivity()
中登記茵宪。
public class FlutterPluginDemoPlugin implements FlutterPlugin, ActivityAware {
// 為了在 onAttachedToActivity() 中得到 PlatformViewRegistry
private FlutterPluginBinding pluginBinding;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
pluginBinding = flutterPluginBinding;
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
pluginBinding.getPlatformViewRegistry().registerViewFactory(
"baidu_map",
new BaiduMapFactory(binding.getActivity()));
}
}
在 Flutter 端顯示百度地圖
/lib/flutter_plugin_demo.dart
export 'src/baidu_map.dart';
/lib/src/baidu_map.dart
import 'package:flutter/material.dart';
class BaiduMapView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AndroidView(
viewType: "baidu_map",
);
}
}
/example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_plugin_demo/flutter_plugin_demo.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: BaiduMapView(),
),
),
);
}
}
運(yùn)行后應(yīng)該就可以看到地圖了稀火,如果只能看到格子,需要從百度獲取開(kāi)發(fā)密鑰(AK)凰狞。
在百度地圖官網(wǎng)文檔中:
類(lèi) TextureMapView
使用這個(gè)類(lèi)必須按照它的生命周期進(jìn)行操控钦听,你必須參照以下方法onCreate(Bundle)阀蒂、 onResume()褂始、onPause()痴突、onDestroy()原押。等聲明周期函數(shù)圾另。在使用地圖組件之前請(qǐng)確保已經(jīng)調(diào)用了 SDKInitializer.initialize(Context) 函數(shù)以提供全局 Context 信息。
所以要想辦法獲取生命周期雕沉。
地圖生命周期
和生命周期有關(guān)的方法在 Activity 中集乔,要重寫(xiě)里面的方法好像有點(diǎn)不太方便。
向谷歌地圖插件學(xué)習(xí)坡椒,讓我們的 BaiduMapControler 再實(shí)現(xiàn)一個(gè) DefaultLifecycleObserver 扰路。
public class BaiduMapControler implements PlatformView, DefaultLifecycleObserver {
private final TextureMapView mapView;
@Override
public View getView() {
return mapView;
}
@Override
public void dispose() {
}
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
}
@Override
public void onPause(@NonNull LifecycleOwner owner) {
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
}
}
多出的的方法正是和生命周期相關(guān)的。
DefaultLifecycleObserver 是什么呢倔叼?
DefaultLifecycleObserver
它可以作為 Activity 生命周期狀態(tài)的觀察者汗唱。
當(dāng) Activity 生命周期變化時(shí),會(huì)調(diào)用觀察者中相應(yīng)的方法丈攒。
生命周期的觀察者被保存在 Lifecycle 類(lèi)中哩罪。
要觀察某個(gè) Activity 的生命周期,就要把觀察者添加到 Activity 所擁有的 Lifecycle 中巡验。
獲取 Lifecycle
方法1
之前我們使用 ActivityPluginBinding 的 getActivity()
獲取了 Activity 际插。
它也有個(gè) getLifecycle()
方法用來(lái)獲取 Lifecycle ,不過(guò)返回的類(lèi)型是 Object 显设。
如果把它強(qiáng)制轉(zhuǎn)換成 Lifecycle 框弛。
會(huì)出現(xiàn)異常:
“io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference cannot be cast to androidx.lifecycle.Lifecycle”。
看下 HiddenLifecycleReference 的文檔:
An Object that can be used to obtain a Lifecycle reference.
DO NOT USE THIS CLASS IN AN APP OR A PLUGIN.
This class is used by the flutter_android_lifecycle package to provide access to a Lifecycle in a way that makes it easier for Flutter and the Flutter plugin ecosystem to handle breaking changes in Lifecycle libraries.
特別強(qiáng)調(diào)了不讓用敷硅。說(shuō)是給“flutter_android_lifecycle”這個(gè)包用的。
谷歌地圖插件好像就用了那個(gè)包力奋。
看了那個(gè)包的代碼,實(shí)現(xiàn)是很簡(jiǎn)單的猿挚。
所以可行的方法是這樣:
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
//lifecycle = (Lifecycle) binding.getLifecycle();
HiddenLifecycleReference reference = (HiddenLifecycleReference) binding.getLifecycle();
lifecycle = reference.getLifecycle();
}
方法2
FlutterActivity 有個(gè) getLifecycle()
方法是可以返回 Lifecycle 的铣墨。
我們傳遞給 BaiduMapControler 的 Activity ,其實(shí)就是 FlutterActivity 屡律。
下面是使用方法2的例子超埋。
最終的代碼
public class BaiduMapControler implements PlatformView, DefaultLifecycleObserver {
private static final String TAG = "BaiduMapController";
private final TextureMapView mapView;
public BaiduMapControler(FlutterActivity activity) {
this.mapView = new TextureMapView(activity);
activity.getLifecycle().addObserver(this);
}
@Override
public View getView() {
return mapView;
}
@Override
public void dispose() {
}
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onCreate");
mapView.onCreate(null, null);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onStart");
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onResume");
mapView.onResume();
}
@Override
public void onPause(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onPause");
mapView.onPause();
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onStop");
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onDestroy");
mapView.onDestroy();
}
}
參考資料
插件
- Developing packages & plugins
- Flutter之在Flutter布局中嵌入原生組件Android篇
-
Flutter 顯示百度地圖 Native 組件 -
chinesejar/flutter_with_map - 在Flutter中嵌入Native組件的正確姿勢(shì)是...
紋理
- 萬(wàn)萬(wàn)沒(méi)想到——flutter這樣外接紋理
- Flutter 實(shí)時(shí)視頻渲染:Texture與PlatformView
- 5分鐘徹底搞懂Flutter中PlatFormView與Texture
- Flutter浪潮下的音視頻研發(fā)探索