自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Flutter 地圖在攜程的最佳實(shí)踐

人工智能 新聞
本文將重點(diǎn)突出基于 flutter-boost 的混合工程,單引擎模式下接入 Flutter 地圖插件遇到的問(wèn)題和解決方案。

作者簡(jiǎn)介

Leo,攜程高級(jí)移動(dòng)開(kāi)發(fā)工程師,關(guān)注跨端技術(shù),致力于高效、高性能開(kāi)發(fā)。

Jarmon,攜程高級(jí)移動(dòng)開(kāi)發(fā)工程師,專(zhuān)注 Flutter、iOS 開(kāi)發(fā)。

本文將重點(diǎn)突出基于 flutter-boost 的混合工程,單引擎模式下接入 Flutter 地圖插件遇到的問(wèn)題和解決方案。

一、背景

隨著各種多端技術(shù)的蓬勃發(fā)展,項(xiàng)目主體從純 Native 項(xiàng)目,到 Native+RN,到現(xiàn)在的 Native+RN+Flutter?;谖覀兊臉I(yè)務(wù)都在 Flutter 技術(shù)棧上面,這要求我們需要嵌套展示地圖。目前,實(shí)現(xiàn)嵌套展示地圖的主要方案有二個(gè):

接入官方提供的 Flutter 地圖插件,主要面臨的問(wèn)題有:

  • 官方提供的插件成熟度不夠,有一些 Native 已有的 API 在 Flutter 上不支持;
  • 目前接入 Flutter 地圖插件的應(yīng)用很少,我們需要去蹚雷。
  • 由于官方適配的是純 Flutter 項(xiàng)目,混合工程可能遇到很多未知棘手問(wèn)題。

直接在 Flutter 頁(yè)面上展示 Native 的地圖

  • Native 地圖成熟,不會(huì)遇到很大的坑;
  • 主要問(wèn)題在于業(yè)務(wù)在 Flutter上,F(xiàn)lutter 需要大量的和地圖組件進(jìn)行交互、請(qǐng)求數(shù)據(jù)、聯(lián)動(dòng)。需要通過(guò)大量的橋方法去傳遞操作數(shù)據(jù);
  • 要嵌套 Native 地圖需要定制容器,Android 和 IOS 上各自得實(shí)現(xiàn)一遍橋、容器和地圖邏輯,增加了維護(hù)成本。

考慮維護(hù)成本、權(quán)衡再三我們還是選擇接入 Flutter 地圖插件。為了能更好的定制一些 API 和更快速的修復(fù)一些官方?jīng)]有及時(shí)更新的問(wèn)題。我們采用的是源碼接入 Flutter 地圖插件。本文將重點(diǎn)突出基于 flutter-boost 的混合工程,單引擎模式下接入 Flutter 地圖插件遇到的問(wèn)題和解決方案。

二、如何源碼集成

在混合項(xiàng)目中集成插件主要分 flutter 和原生兩側(cè),集成 Flutter 插件時(shí),官方 demo 中可以直接下載到插件的源碼。本文以接入 flutter 地圖插件 3.3.1 版本示例。

2.1 Flutter 端集成

獲取到官方 demo 后在該目錄下執(zhí)行 flutter pub get,然后去 flutter SDK 下找到 pub-cache 依賴(lài)緩存文件目錄,根據(jù)業(yè)務(wù)需要將每個(gè)插件 src 文件下的代碼導(dǎo)入到 flutter 工程中。

2.2 IOS 端集成

執(zhí)行完 flutter pub get 后,根據(jù)需要將每個(gè)插件 iOS/Classes/ 目錄下的代碼導(dǎo)入工程中。

2.3 Android 端集成

Android 的 Native 側(cè)的集成和 IOS 端是類(lèi)似的。在 Native 工程中新建一個(gè)地圖 Module。把地圖 Demo 中的地圖插件源碼 Android 部分放入工程即可。

三、地圖插件實(shí)現(xiàn)原理:platformView

地圖插件按功能分為 Map、Search、Util 等模塊,其基本實(shí)現(xiàn)類(lèi)似,使用 MethodChannel 與 native 通信,我們以 Map 為例分析其實(shí)現(xiàn)。插件使用了 PlatformView 將原生地圖嵌入到 flutter 頁(yè)面中,在 flutter 層為 UIKitView、AndroidView,native 在生成地圖后根據(jù) viewId 初始化 BMFMapViewController,包含對(duì)應(yīng)的 MethodChannel。BMFMapViewController 聚合了對(duì)地圖操作,派發(fā)到不同模塊調(diào)用地圖 native 方法。

3.1 什么是PlatformView

PlatformView 是允許原生組件嵌入到 Flutter 頁(yè)面的一種技術(shù),能夠讓我們將一些原生成熟組件、flutter UI 框架難以實(shí)現(xiàn)的地圖、WebView 等組件展示在 flutter 頁(yè)面中。

Flutter 提供了 Virtual Display、Hybrid Composition 兩種方式實(shí)現(xiàn) PlatformView。Virtual Display 模式將 native view 加載到內(nèi)存當(dāng)中,隨著 flutter Widget 一起渲染出來(lái)。Hybrid Composition 模式是直接將 native view 添加到 flutter view 圖層上。iOS采用了 Hybrid Composition 模式,Android 采用了 Virtual Display 和 Hybrid Composition 兩種模式。

3.2 PlatformView 實(shí)現(xiàn)原理

1)flutter 渲染流程

在介紹 Hybrid Composition 實(shí)現(xiàn)之前,先通過(guò)下圖大致了解下 flutter 的渲染流程。

在收到 VSync 信號(hào)之后,Dart 層在 UI Thread 完成 Widget Tree、Element Tree、RenderObject Tree 三棵樹(shù)的更新與生成,然后生成包含繪制信息的 layer Tree 交給 Engine 去渲染,最后在 GPU Thread 經(jīng)歷 Compositor、Skia 將 flutter 視圖渲染出來(lái)。

2)Hybrid Composition 模式分析

以 iOS 為例逐步分析 Hybird Composition 模式執(zhí)行流程。首先 Dart 層提供了 UIKitView 組件來(lái)展示 native view,didChangeDependencies 方法中通過(guò) channel 初始化一次 native view,生成唯一標(biāo)識(shí) native view 的 viewId,并將 native view 緩存在 root_views_ 中。在實(shí)際組裝 layer 層時(shí),dart 層會(huì)傳輸給 engine 展示 native view 的坐標(biāo)和大小,并生成一個(gè) PlatformViewLayer,也就是說(shuō) native view 的位置、大小信息是由 dart 層控制的。

void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) {
  NSDictionary<NSString*, id>* args = [call arguments];
  long viewId = [args[@"id"] longValue];
  NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero                                          viewIdentifier:viewId                                               arguments:params]; // 初始化
  UIView* platform_view = [embedded_view view]; 


  FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
                  initWithEmbeddedView:platform_view
               platformViewsController:GetWeakPtr()
gestureRecognizersBlockingPolicy:gesture_recognizers_blocking_policies[viewType]]
      autorelease];
  ChildClippingView* clipping_view =
      [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease];
  [clipping_view addSubview:touch_interceptor];
  root_views_[viewId] = fml::scoped_nsobject<UIView>([clipping_view retain]); // 緩存
}

生成當(dāng)前幀的 Layer Tree 之后,會(huì)進(jìn)入到 Rasterizer 流程。首先會(huì)調(diào)用 BeginFrame 渲染一幀,觸發(fā) PlatformViewLayer::Preroll,PlatformViewLayer 標(biāo)記出當(dāng)前幀有 PlatformView ,然后調(diào)用 FlutterPlatformViewsController::PrerollCompositeEmbeddedView 更新 view_params_,包含 Platform View 坐標(biāo)、size 等信息,最后在 SubmitFrame 方法中取出 native view 添加到 flutter view 中,完成渲染。

void PlatformViewLayer::Preroll(PrerollContext* context,
                                const SkMatrix& matrix) {
  set_paint_bounds(SkRect::MakeXYWH(offset_.x(), offset_.y(), size_.width(),
                                    size_.height()));
  context->has_platform_view = true;
  set_subtree_has_platform_view(true); // 標(biāo)記當(dāng)前幀存在Platform View
  std::unique_ptr<EmbeddedViewParams> params =
      std::make_unique<EmbeddedViewParams>(matrix, size_,
                                           context->mutators_stack);  context->view_embedder->PrerollCompositeEmbeddedView(view_id_,
                                                       std::move(params));
}

3.3 PlatformView 是如何實(shí)現(xiàn)幀同步?

在原生開(kāi)發(fā)中,我們知道UI操作不能在其他線程執(zhí)行,會(huì)出現(xiàn)幀不同步的問(wèn)題。flutter Engine 中有 platform、ui、raster、io四個(gè)線程,native view 是在 Platform Thread(主線程)渲染,而 flutter 渲染正常情況在 Raster Thread 執(zhí)行的,flutter 又是如何保證幀同步的呢? 

flutter 解決幀同步是通過(guò)線程合并的方案。上圖 Raster 流程 PostPrerollAction 方法中,會(huì)判斷如果有 PlatformView 存在,在接下來(lái)的繪制過(guò)程中 Raster Thread 與 Platform Thread 會(huì)合并,將 Raster 隊(duì)列任務(wù)放到 Platform 隊(duì)列中。這樣所有的渲染任務(wù)都在 Platform Thread 中執(zhí)行,保證了畫(huà)面的同步。

PostPrerollResult FlutterPlatformViewsController::PostPrerollAction(
    fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
  if (!HasPlatformViewThisOrNextFrame()) { // 沒(méi)有Platform View不用處理
    return PostPrerollResult::kSuccess;
  }
  if (!raster_thread_merger->IsMerged()) { // 線程還沒(méi)有并不用處理
    CancelFrame(); // 取消繪制當(dāng)前幀
    return PostPrerollResult::kSkipAndRetryFrame; // 合并后完成當(dāng)前幀
  }
  BeginCATransaction();
  raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration);
  return PostPrerollResult::kSuccess;
}
// 合并隊(duì)列
bool MessageLoopTaskQueues::Merge(TaskQueueId owner, TaskQueueId subsumed) {
  if (owner == subsumed) {
    return true;
  }
  std::lock_guard guard(queue_mutex_);
  auto& owner_entry = queue_entries_.at(owner);
  auto& subsumed_entry = queue_entries_.at(subsumed);
  auto& subsumed_set = owner_entry->owner_of;
  if (subsumed_set.find(subsumed) != subsumed_set.end()) {
    return true;
  }
  owner_entry->owner_of.insert(subsumed);
  subsumed_entry->subsumed_by = owner;
  if (HasPendingTasksUnlocked(owner)) {
    WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner));
  }
  return true;
}

四、問(wèn)題及解決方案

4.1 IOS 頁(yè)面切換 Map 組件白屏問(wèn)題

在使用 flutter_boost 混合開(kāi)發(fā)時(shí),當(dāng) A 頁(yè)面中使用 platformview,開(kāi)啟新容器跳轉(zhuǎn)到 flutter B 頁(yè)面,platformView 會(huì)出現(xiàn)短暫的白屏,從 A 頁(yè)面跳轉(zhuǎn) native 頁(yè)面不會(huì)出現(xiàn)。根據(jù)表象首先猜測(cè)是單引擎導(dǎo)致的。flutter A頁(yè)面跳轉(zhuǎn)到其他頁(yè)面時(shí)都會(huì)觸發(fā) SceneBuilder::pushTransform 重新渲染一次 A 頁(yè)面。

void SceneBuilder::pushTransform(Dart_Handle layer_handle,
                                 tonic::Float64List& matrix4,
                                 fml::RefPtr<EngineLayer> oldLayer) {
  SkMatrix sk_matrix = ToSkMatrix(matrix4);
  auto layer = std::make_shared<flutter::TransformLayer>(sk_matrix);
  PushLayer(layer);
  // matrix4 has to be released before we can return another Dart object
  matrix4.Release();
  EngineLayer::MakeRetained(layer_handle, layer);
  if (oldLayer && oldLayer->Layer()) {
    layer->AssignOldLayer(oldLayer->Layer().get());
  }
}

flutter  A頁(yè)面在創(chuàng)建新容器 push 到 flutter B 頁(yè)面時(shí),首先會(huì)觸發(fā) viewDidLayoutSubviews,方法內(nèi)部會(huì)修改 engine 對(duì)應(yīng)的 viewController flutterView,SceneBuilder::pushTransform 是在 viewDidLayoutSubviews 之后還會(huì)觸發(fā),而 platformView 是在 native 渲染,重新渲染 A 頁(yè)面時(shí)就找不到對(duì)應(yīng)的 platformView,導(dǎo)致白屏的問(wèn)題。push 到非 flutter 頁(yè)面時(shí)不會(huì)觸發(fā) surfaceUpdated,所以不會(huì)出現(xiàn)該問(wèn)題。

- (void)viewDidLayoutSubviews {
  ...
  if (firstViewBoundsUpdate && applicationIsActive && _engine) {
    [self surfaceUpdated:YES];
  }
  ...
}
- (void)surfaceUpdated:(BOOL)appeared {
  if (appeared) {
    [self installFirstFrameCallback];
    [_engine.get() platformViewsController]->SetFlutterView(_flutterView.get());
    [_engine.get()     platformViewsController]->SetFlutterViewController(self);
    [_engine.get() iosPlatformView]->NotifyCreated();
  }
}

一開(kāi)始的方案是在 viewWillAppear 中調(diào)用 sufaceUpdated,但是在 release 環(huán)境中會(huì)出現(xiàn)卡死的現(xiàn)象。另一方案是 [super bridge_viewWillAppear:animated]; 改為 [super viewWillAppear:animated];  [super viewWillAppear:animated]; 會(huì)調(diào)用父類(lèi)的方法,父類(lèi)方法又會(huì)調(diào)用 sufaceUpdated,就可以解決白屏的問(wèn)題。

4.2 Android 地圖卡死不能操作問(wèn)題

1)問(wèn)題描述

A 頁(yè)面內(nèi)嵌地圖,跳轉(zhuǎn)到 B 頁(yè)面。然后返回 A 頁(yè)面,地圖就不能滑動(dòng)。

結(jié)合上文提到的 Flutter 地圖插件其實(shí)是通過(guò) MathodChannel 將操作傳遞到 Native 的地圖視圖處理的。我們調(diào)試 Native 的代碼發(fā)現(xiàn) PlatformViewsController 類(lèi)里面的 onTouch()方法中,context 報(bào)了一個(gè)Attempt to invoke virtual method 'android.content.res.Resources android.content.Context.getResources()' on a null object reference。

public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
          final float density = context.getResources().getDisplayMetrics().density;
          }

2)分析問(wèn)題

由于 context 對(duì)象被回收,造成的報(bào)錯(cuò)?,F(xiàn)在我們只有分析出來(lái)為什么 context 對(duì)象會(huì)被回收掉了就能找出問(wèn)題了,讀源碼發(fā)現(xiàn)只有在 detach() 方法中才會(huì)回收 context 對(duì)象。

public void detach() {
    context = null;
  }

結(jié)合日志輸出,確實(shí)發(fā)現(xiàn)回到 A 頁(yè)面是執(zhí)行了 attach() 方法,但是馬上又執(zhí)行了 detach() 方法?,F(xiàn)在就是要找出,為什么 A 頁(yè)面的 PlatformViewsController 會(huì)被執(zhí)行 datach()。

從B頁(yè)面 返回A頁(yè)面
2022-08-22 15:13:08.126 21878-21878/ctrip.flutter.demo D/PlatformViewsController: B===>detach()
2022-08-22 15:13:08.135 21878-21878/ctrip.flutter.demo D/PlatformViewsController: A====>attach()
2022-08-22 15:13:08.249 21878-21878/ctrip.flutter.demo D/PlatformViewsController: A=====>detach()

查看調(diào)用鏈:

逐個(gè)類(lèi)讀源碼我們發(fā)現(xiàn)在 FlutterActivityAndFragmentDelegate的OnDetach() 方法中如果引擎的生命周期和 Activity 的生命周期是綁定的。頁(yè)面結(jié)束時(shí),引擎就會(huì)被銷(xiāo)毀掉。

void onDetach() {
    if (host.shouldAttachEngineToActivity()) {
      if (host.getActivity().isChangingConfigurations()) {
flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges();
} else {
flutterEngine.getActivityControlSurface().detachFromActivity();
      }
    }

3)解決問(wèn)題

設(shè)置 shouldAttachEngineToActivity 返回 flase 使得 Flutter 引擎將在應(yīng)用程序的整個(gè)生命周期內(nèi)持久化存在,并獨(dú)立于 Activity,當(dāng) Activity 被銷(xiāo)毀時(shí),F(xiàn)lutter 引擎不被銷(xiāo)毀 。問(wèn)題就解決了。產(chǎn)生問(wèn)題的原因是我們新開(kāi) B 頁(yè)面是通過(guò)新開(kāi)容器的方式創(chuàng)建的。B 頁(yè)面 FlutterFragment 中 onDetach() 方法在 A 頁(yè)面 onAttach() 之后被執(zhí)行的。純 Flutter 工程或者是采用 Push 的方式打開(kāi)新頁(yè)面,不新開(kāi)容器都能規(guī)避掉這個(gè)問(wèn)題。

public boolean shouldAttachEngineToActivity() {
        return false;
    }

4.3 Android 地圖內(nèi)存溢出問(wèn)題

1)問(wèn)題描述

多次打開(kāi) Android Flutter 地圖頁(yè)面會(huì)越來(lái)越卡,到后面整個(gè)地圖都黑一下,顯然是有內(nèi)存溢出了。通過(guò) Android Studio IDE 自帶的內(nèi)存工具 Android Profiler 可以很明顯的看出來(lái),每打開(kāi)一次頁(yè)面,內(nèi)存占有都會(huì)上升,結(jié)束頁(yè)面內(nèi)存沒(méi)有得到釋放。

2)分析問(wèn)題

Flutter Boost 和地圖插件如此大量的第三方代碼,我們?nèi)绾稳ザㄎ粏?wèn)題呢?是插件引起的,還是框架引起的呢?借助 LeakCanary 就能很好的找到內(nèi)存泄露的地方了。

接入也非常的簡(jiǎn)單,在 Android build.gradle引入leakcanary。

debugImplementation'com.squareup.leakcanary:leakcanary-android:2.6'

然后運(yùn)行應(yīng)用,反復(fù)操作問(wèn)題復(fù)現(xiàn)流程,直到 LeakCanary 提示。查看 leaks 內(nèi)存溢出的堆棧信息。是由于 SingleViewPresentation 一直持有了容器 TripFlutterActivity  的 context 對(duì)象。懷疑是 MapView 的生命周期有問(wèn)題。是不是沒(méi)有執(zhí)行 dispose。調(diào)試下來(lái)的情況 PlatformViewsHandler handler 對(duì)象空了,后面的流程都不會(huì)執(zhí)行。

3)解決問(wèn)題

查看源碼只有 PaltformViewsController detach() 方法會(huì)把 handler 設(shè)置為 null。

public void detach() {
    if (platformViewsChannel != null) {
      platformViewsChannel.setPlatformViewsHandler(null);
    }
    }

調(diào)試下來(lái) FlutterActivity 容器結(jié)束,調(diào)用了 onDestroy() 方法的時(shí)候  PaltformViewsController detach() 就已經(jīng)被執(zhí)行了。容器的 onDestroy() 在 MapView 的 dispos e之前,造成了 handler 對(duì)象空了。

解決問(wèn)題的思路很簡(jiǎn)單,在 onDestroy() 的時(shí)候先保留 handler 對(duì)象,然后找個(gè)時(shí)機(jī)清除一下。采用 viewIdSet 自己維護(hù)一份 View 的數(shù)據(jù)。在 creat 方法中  disposeArgs.get("id")  執(zhí)行過(guò) dispose 方法的就刪除掉 viewIdSet.remove(viewId)。setPlatformViewsHandler 為空的情況判斷一下,有沒(méi)有執(zhí)行 dispose 的 view handler  先不回收。如下:

public void setPlatformViewsHandler(@Nullable PlatformViewsHandler handler) {
    if(handler == null && viewIdSet != null && viewIdSet.size() > 0) {
      needReset = true;
      return;
    }
    this.handler = handler;
  }

目前是執(zhí)行 dispose 的時(shí)候 needReset 為 true 時(shí)會(huì)將 handler 設(shè)置為 null。為什么官方的 Demo 是沒(méi)有問(wèn)題的呢?主要原因還是我們接入了 FlutterBoost 默認(rèn)是單引擎的,官方 Demo 是的純 Flutter 項(xiàng)目多引擎。頁(yè)面結(jié)束,通過(guò)銷(xiāo)毀 engine 把問(wèn)題覆蓋了,所以?xún)?nèi)存回收表現(xiàn)的很平滑。 

五、自定義文本 BitMap Marker

地圖業(yè)務(wù)中自定義 marker 是比較常見(jiàn)的需求,由于地圖是通過(guò) PlatformView 實(shí)現(xiàn)的,最容易想到的做法是,通過(guò) Channel 傳入 marker 對(duì)應(yīng)的樣式 Id 和展示所需數(shù)據(jù),在各端繪制 marker,這種做法會(huì)增加人工成本,樣式也可能存在不一致的情況,失去了 flutter 框架的優(yōu)勢(shì)。

地圖插件在 v3.0(v3.0 之前需要自己實(shí)現(xiàn))提供了 iconData 參數(shù)傳入圖片 data 信息,在 flutter 側(cè)將文本、圖片繪制出來(lái)生成一張圖,將生成圖片 Data 傳遞給原生,該實(shí)現(xiàn)并不需要改動(dòng)各端代碼,繪制時(shí)要注意視圖大小是物理像素點(diǎn),而不是邏輯像素點(diǎn)。

Future<Uint8List?> customMark(String name, BuildContext context) async {
  final scale = MediaQuery.of(context).devicePixelRatio;
  final recorder = PictureRecorder();
  final canvas = Canvas(recorder);
  final paint = Paint();
  final textPainter = TextPainter(textDirection: TextDirection.ltr);
  ...
  final path = Path();
  canvas.drawPath(path, paint);
  // 繪制圖片
  final imageInfo = await UIImageLoader.imageInfoByAsset(HotelListImage.mapPoiMark);
  paintImage(canvas: canvas,rect: rect,image: imageInfo.image);
  // 生成繪制圖片
  final image = await recorder.endRecording().toImage(
      width.toInt(), (textBgHeight + arrowHeight + iconHeight + 2).toInt());
  final data = await image.toByteData(format: ImageByteFormat.png);
  return data?.buffer.asUint8List();
}

從 flutter 2 升級(jí)到 flutter 3 出現(xiàn)了小插曲,iOS debug 環(huán)境調(diào)用 toImage 進(jìn)程會(huì)被終止。flutter 升級(jí)之后對(duì)弱引用指針調(diào)用做了線程檢查,創(chuàng)建和使用不是在同一線程在 debug 環(huán)境進(jìn)程會(huì)被終止。toImage() 方法內(nèi)使用了 fml::WeakPtr<SnapshotDelegate> snapshot_delegate 弱引用指針,由于 snapshot_delegate 在 raster 線程中被創(chuàng)建,正常調(diào)用也應(yīng)該是在 raster 線程,當(dāng)在 flutter 頁(yè)面中嵌入 PlatformView 時(shí),為了保證渲染的一致性,會(huì)將 raster 線程與主線程合并,造成了 snapshot_delegate 在主線程調(diào)用的情況,觸發(fā)了線程檢查終止進(jìn)程,但并不影響 release 環(huán)境。

class WeakPtr {
    T* operator->() const {
    CheckThreadSafety();
    return get();
  }
}


if (0 == pthread_getname_np(current_thread, actual_thread,
                                  buffer_length) &&
          0 == pthread_getname_np(self_, expected_thread, buffer_length)) {
        FML_DLOG(ERROR) << "IsCreationThreadCurrent expected thread: '"
                        << expected_thread << "' actual thread:'" // Object被創(chuàng)建的線程
                        << actual_thread << "'";  // 實(shí)際執(zhí)行線程
}

六、自定義讓 Marker 展示在可見(jiàn)范圍

在地圖上添加 marker 之后,將已添加的 marker 全部展示在可視范圍內(nèi)也是常見(jiàn)的需求。插件提供了支持 iOS 的 showmarkers 方法,這顯然不能夠滿(mǎn)足需求。我們思考通過(guò) setVisibleMapRectWithPadding 指定顯示地圖地理范圍,該方法要求我們傳入?yún)?shù) visibleMapBounds,設(shè)置地理范圍的東北坐標(biāo)、西南坐標(biāo)。由于右上角、左下角經(jīng)緯度分為可視地理范圍最大、最小,即可拿到東北、西南坐標(biāo)。

BMFCoordinateBounds? getMarkersVisibleMapBounds(List<BMFMarker> markers) {
  if (markers.isEmpty) return null;
  final firstPosition = markers.first.position;
  double maxLatitude = firstPosition.latitude;
  double minLatitude = firstPosition.latitude;
  double maxLongitude = firstPosition.longitude;
  double minLongitude = firstPosition.longitude;
  for (final marker in markers) {
    final lat = marker.position.latitude;
    final lon = marker.position.longitude;
    maxLatitude = max(maxLatitude, lat);
    minLatitude = min(minLatitude, lat);
    maxLongitude = max(maxLongitude, lon);
    minLongitude = min(minLongitude, lon);
  }
  return BMFCoordinateBounds(
      northeast: BMFCoordinate(maxLatitude, maxLongitude),
      southwest: BMFCoordinate(minLatitude, minLongitude));
}

隨著業(yè)務(wù)的迭代,需要將大地圖融合到列表中。為了將大地圖與小地圖切換動(dòng)畫(huà)更加流暢,當(dāng)小地圖被加載時(shí),地圖 size 實(shí)際已經(jīng)渲染成和大地圖同樣大小,下半部分被列表遮擋。這意味小地圖需要設(shè)置可見(jiàn)范圍的偏移量,但 inserts 參數(shù) iOS、Android 計(jì)算方式不一樣,iOS 是根據(jù) point 計(jì)算,Android 是通過(guò) pixel 計(jì)算,要區(qū)分平臺(tái)做一次轉(zhuǎn)換。

Future<bool> setAllMarkersVisibleWithPadding(
  List<BMFMarker> markers,
  BuildContext context, {
  EdgeInsets insets = const EdgeInsets.all(20.0),
}) async {
  final bounds = getMarkersVisibleMapBounds(markers);
  if (bounds == null) return false;
  if (Util.isAndroid()) {
    final scale = MediaQuery.of(context).devicePixelRatio;
    insets = EdgeInsets.only(
        top: insets.top * scale,
        bottom: insets.bottom * scale,
        left: insets.left * scale,
        right: insets.right * scale);
  }
  return await setVisibleMapRectWithPadding(
      visibleMapBounds: bounds, insets: insets, animated: true);
}

七、總結(jié)

Flutter 地圖插件基于Native地圖 Android 和 iOS SDK 二次封裝而成,通過(guò)在 Flutter 使用MethodChannel交互實(shí)現(xiàn)地圖的顯示、交互、覆蓋物繪制和事件響應(yīng)等功能。混合項(xiàng)目接入Flutter地圖容易發(fā)生問(wèn)題的點(diǎn),基本集中在PlatformView這一塊。通常是容器和View的事件、生命周期同步問(wèn)題。

本文主要介紹FlutterBoost的混合工程,在接入Flutter地圖插件遇到的各種問(wèn)題和解決方案。闡述了PlatformView的工作原理,方便我們更好的理解Flutter地圖插件。同時(shí)也介紹了如何用Android Studio 自帶的工具直觀地看內(nèi)存異常。并且推薦leakcanary定位內(nèi)存溢出的類(lèi)和方法,希望對(duì)你接入Flutter地圖插件有一定的幫助。

責(zé)任編輯:張燕妮 來(lái)源: 攜程技術(shù)
相關(guān)推薦

2022-07-08 09:38:27

攜程酒店Flutter技術(shù)跨平臺(tái)整合

2023-02-08 16:34:05

數(shù)據(jù)庫(kù)工具

2022-04-07 17:30:31

Flutter攜程火車(chē)票渲染

2023-06-06 16:01:00

Web優(yōu)化

2022-11-29 20:32:07

2022-08-06 08:23:47

云計(jì)算公有云廠商成本

2017-10-09 09:12:35

攜程運(yùn)維架構(gòu)

2022-05-13 09:27:55

Widget機(jī)票業(yè)務(wù)App

2022-07-15 12:58:02

鴻蒙攜程華為

2022-03-30 18:39:51

TiDBHTAPCDP

2022-08-20 07:46:03

Dynamo攜程數(shù)據(jù)庫(kù)

2022-07-15 09:20:17

性能優(yōu)化方案

2022-08-12 08:34:32

攜程數(shù)據(jù)庫(kù)上云

2024-04-26 09:38:36

2024-07-05 15:05:00

2022-06-17 10:44:49

實(shí)體鏈接系統(tǒng)旅游AI知識(shí)圖譜攜程

2023-11-12 11:54:55

UX性能widget

2022-06-03 09:21:47

Svelte前端攜程

2023-04-14 10:29:24

小程序實(shí)踐

2023-12-15 10:05:58

攜程網(wǎng)絡(luò)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)