Flutter/flutter_boost 中頁面橫豎屏控制

轉載至 我的blog

最近項目中越來越多的頁面開始使用 Flutter 進行開發,這時一個棘手的問題就暴露出來,我們項目中對 Flutter 頁面橫豎屏的控制非常的僵硬,基本上就是在被跳轉的頁面的 init 方法中進行了橫豎屏的設置,然后在 dispose 的時候再改回去,當初這樣寫的原因在于任務緊張并且團隊初次使用了 Flutter 來開發,對 Flutter 中的各種機制還不是特別的了解,并且就幾個頁面,跳轉的邏輯十分的簡單,就將就了一下。

現在頁面多了,這樣寫的缺點就很大了。嚴重依賴頁面間的跳轉順序,一切的橫豎屏設置都是依賴于固定路徑的跳轉,否者在 dispose 的時候就無法設置正確的屏幕方向,在 Flutter 中最理想的是使用 route 來進行任意的跳轉,我們這種寫法對此而言簡直就是災難。

實現方案

正好最近有時間就研究了一下這個問題,并找到了一個相對可靠的解決方案,根據我的經驗,我想實現一種類似于 iOS 當中橫豎屏控制的機制。在 iOS 當中,每個 ViewController 都能重寫三個只讀屬性,來設置自己的屏幕方向。這樣每個頁面只需要聲明自己的屏幕方向,就能在顯示自身時獲得正確的顯示,無須關心導航棧中的其他頁面,屏幕方向的控制都交給 Navigator 來管理。

監聽導航棧

要實現上述功能,最重要的一點就是,我們要監聽導航棧的變動,在 Flutter 中,當我們 push 一個頁面和 pop 一個頁面的時候,我們需要監聽到兩個數據,一個是 push 時要去的頁面,一個是 pop 時所要返回的頁面。這樣我們就能通過讀取頁面的配置信息來修改屏幕方向,實現類似的效果。

在 Flutter 中我們創建一個 MaterialApp 時,有一個屬性是 final List<NavigatorObserver> navigatorObservers;, 其中的 NavigatorObserver 是一個類,里面定義了兩個重要的方法 void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { }void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { }。當 Flutter 頁面中的導航棧發生變化時,會通知所有的 navigatorObservers,并調用上面的兩個方法。

這里我們需要實現一個自己的 navigatorObserver 來監聽導航棧,這里我們定義一個:

class OrientationNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route route, Route previousRoute) {
    debugPrint(
        'didPush route: $route previousRoute: $previousRoute');
  }

  @override
  void didPop(Route route, Route previousRoute) {
    debugPrint(
        'didPop route: $route previousRoute: $previousRoute');
  }
}

OrientationNavigatorObserver 設置到 navigatorObservers 中以后,我們在兩個方法里面都打印出 routepreviousRoute,運行項目進行一些跳轉,觀察打印的信息,這里可能會出現多種情況:

首先,如果你在項目中使用了是這樣的跳轉方法:

Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
    return VipTaskRegular();
}));

直接使用 push 方法進行跳轉,打印出的信息中 settings 的值會是 (null, null),而這個 route 不會攜帶和你的 widget (這里是 VipTaskRegular)相關的信息,我們就無法確定跳轉的頁面,也就無法進行相關的設置,settings 中的兩個值,一個是 name(路由),還有 arguments (攜帶的參數),所以使用 push 直接進行跳轉的頁面我們是無能為力的。

其實使用 push 進行頁面跳轉不是最優方法,在現在的開發模式中,無論是原生應用還是 Flutter 都開始流行使用路由地址來進行頁面跳轉,就像 web 那樣。通過一個確定的 uri 我們在任何頁面都能跳轉到任何頁面。Flutter 也是推薦先在 MaterialApp 中的 routes 注冊路由,然后我們就能通過 pushNamed(context, routeName) 直接進行頁面跳轉,這種方案代碼更加簡潔,結構上也清晰。

與此同時,我們就能在 didPushdidPop 的 route 中捕獲到 route.settings.name 。因為路由地址和頁面是一一對應的,通過這個 routeName 我們就能知道對應的頁面是什么了。

didPush 中將要顯示的頁面是 route, 而在 didPop 中將要返回的頁面是 previousRoute

頁面配置

解決了監聽的問題,接下來就是如何讀取頁面的相關設置。最初的想法還是依賴于 iOS 中的機制(方案一):

  1. 我想要定義一個抽象類,里面定義上讀取橫豎屏方向的靜態屬性或方法(因為無法獲取 widget 實例,實例方法和屬性也就無從獲取了)
  2. 各個頁面實現抽象類中的屬性和方法,返回配置信息
  3. 在導航監聽中,通過 routeName 獲取才頁面的類型,并讀取配置
  4. 通過配置信息設置橫豎屏方向

但是,上面的設想中,第一條就無法實現,Dart 中的靜態方法是無法向下傳遞給子類的,所以無論是 extends 還是 implements,都是無效的。其次,存儲頁面的 Type 類型到 Map 中,再讀取出來時,也只能通過 switch 進行類型判斷。這都是因為我們無法實現一個可以定義靜態屬性和方法的抽象類。因為無法獲取到 widget 的實例,我們也無法像 iOS 那樣,通過讀取實例的屬性來獲取配置信息。

既然上面的行不通,其次也實現不了像 iOS 那樣的效果,畢竟如果讀取不到實例,也就無法在運行時讓實例通過屬性來自由控制屏幕的方向,我們就索性設置一個配置表,通過 routeName 和 config 的對應,我們讀取預設值進行相關的設置。這樣我們就只需要在一個集中的地方,統一注冊相關的設置就 ok 了

通過配置表統一設置,和通過實現協議各自配置,孰優孰劣就見仁見智了,iOS 中更推崇自己的事情自己干。

這里我直接把相關的代碼粘貼上來:

// Flutter page orientation config.
abstract class OrientationSupport {
  bool get available;
  List<DeviceOrientation> get preferredOrientations;
  List<SystemUiOverlay> get overlays;
}

// Default horizontal config.
class OrientationSupportHorizontal implements OrientationSupport {
  @override
  bool get available => true;
  @override
  List<DeviceOrientation> get preferredOrientations => Platform.isIOS
      ? [DeviceOrientation.landscapeRight]
      : [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight];

  @override
  List<SystemUiOverlay> get overlays =>
      [SystemUiOverlay.top, SystemUiOverlay.bottom];
}

// Default vertical config.
class OrientationSupportVertical implements OrientationSupport {
  @override
  bool get available => true;
  @override
  List<DeviceOrientation> get preferredOrientations =>
      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown];

  @override
  List<SystemUiOverlay> get overlays => [];
}

class OrientationSupportStay implements OrientationSupport {
  @override
  bool get available => false;
  @override
  List<DeviceOrientation> get preferredOrientations => [];
  @override
  List<SystemUiOverlay> get overlays => [];
}

OrientationSupportVertical _vertical() => OrientationSupportVertical();

class OrientationSupportManager {
  static Map<String, OrientationSupport> _list = {
    WordMainPage.name: _vertical(),
  };

  // Leo default page orientation is horizontal.
  static OrientationSupport get defaultOrientationSupport =>
      OrientationSupportHorizontal();

  static void register(String route, OrientationSupport orientationSupport) =>
      _list[route] = orientationSupport;

  static void registerVertical(String route) =>
      register(route, OrientationSupportVertical());

  static OrientationSupport retrieve(String route) => _list[route];
}

OrientationSupport 是配置類,里面定義了屏幕方向和狀態欄等顯示狀態,這些都是在屏幕轉換時需要改變的設定。下面就分別定義了橫屏和豎屏的常用配置,和一個不觸發任何轉屏操作的類(它的作用主要是給那些橫豎屏都兼容的頁面使用的,比如彈窗)OrientationSupportManager 中用來注冊各自頁面的橫豎屏設置。

為了方便使用,在我們的項目中,因為大部分的屏幕是橫屏的,所以我會設定一個 defaultOrientationSupport 為橫屏,這樣在后續處理的時候,我就只用注冊豎屏的界面,而不用把所有的頁面都設置一遍。

接下來的操作就是在 NavigatorObserver 中根據監聽來修改屏幕的方向了,代碼如下:

class OrientationNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route route, Route previousRoute) {
    debugPrint('didPush route: $route previousRoute: $previousRoute');
    _setupOrientation(route.settings.name);
  }

  @override
  void didPop(Route route, Route previousRoute) {
    debugPrint('didPop route: $route previousRoute: $previousRoute');
    _setupOrientation(previousRoute.settings.name);
  }

  void _setupOrientation(String route) {
    // assert(route != null, 'The page have not register route');
    if (route != null) {
      var orientationSupport = OrientationSupportManager.retrieve(route);
      orientationSupport ??=
          OrientationSupportManager.defaultOrientationSupport;
      if (!orientationSupport.available) return;
      // assert(orientationSupport != null,
      // '$route have not register orientation support');
      SystemChrome.setPreferredOrientations(
          orientationSupport.preferredOrientations);
      SystemChrome.setEnabledSystemUIOverlays(orientationSupport.overlays);
    }
  }
}

如果路由為空的話,就不做任何響應,正常的情況下任何頁面的跳轉都必須通過 pushNamed 來進行,這樣的 routeName 是不會為空的。不過也不排除像 alert 彈窗這樣的頁面,在 Flutter 中也是用 push 顯示,但是因為沒有特殊的意義且生命周期短暫就沒有用 pushNamed 來跳轉的事例。然后就是通過 routeName 進行檢索,拿到配置,如果沒有獲取到,就使用默認配置。最后通過 SystemChrome 來設置屏幕方向。

flutter_boost 中如何使用

我們的項目屬于原生中嵌入 Flutter 頁面,在處理 Native 和 Flutter 的導航關系時,使用了 alibaba/flutter_boost。這里也分為兩個場景的使用:

場景一: flutter 內部跳轉也使用 flutter_boost

在 Flutter 內部的跳轉也使用了 flutter_boost 來進行處理的話,通過設置 MaterialApp 的 navigatorObservers 是沒有用的,所有的導航跳轉都被 flutter_boost 接管了,我們需要通過 FlutterBoost.singleton.addContainerObserver(BoostContainerObserver observer) 進行監聽設置。 BoostContainerObserver 就是一個 block 函數,它提供的信息和原來的 NavigatorObserver 差不多。

typedef BoostContainerObserver = void Function(
    ContainerOperation operation, BoostContainerSettings settings);

enum ContainerOperation { Push, Onstage, Pop, Remove }

class BoostContainerSettings {
  const BoostContainerSettings({
    this.uniqueId = 'default',
    this.name = 'default',
    this.params,
    this.builder,
  });

  final String uniqueId;
  final String name;
  final Map<String, dynamic> params;
  final WidgetBuilder builder;
}

和原來的方法一樣,我們需要監聽的是 PushPop 操作,而 settings 里的 name 就是 rotueName。

場景二: flutter 內部跳轉不使用 flutter_boost

還有一種情況就是,在 flutter -> native 和 native -> flutter 中使用 flutter_boost。但是在 flutter -> flutter 時,還是使用 flutter 內部的 push 方法進行跳轉。

在這種情況下,我們使用 FlutterBoost.singleton.addBoostNavigatorObserver 來添加一個 NavigatorObserver, 然后在項目中運行,flutter 內部相互之間的跳轉是沒有問題的,但是當你從 native 跳轉到 flutter 頁面的時候,你會發現進入的 flutter 頁面橫豎屏設置沒有生效,但是再后續的 flutter 內部的跳轉,橫豎屏的設置是生效的。

進一步研究,我們發現從 native A 跳轉 flutter B 的時候,無論這個 B 頁面的 routeName 在 flutter_boostMaterialApp 中的注冊路由為何值,在我們的監聽中,routeName 都會變為 /。這就導致對 B 頁面的橫豎屏設置失效了。要解決這個問題,我們就需要同時監聽 FlutterBoost.singleton.addContainerObserver

當從 native -> flutter 的時候,在 Push 方法中,我們能監聽到 B 頁面正確的 routeName。這里我們就能讀取到相對應的配置信息,然后把配置信息更新到配置表中,key 不是 routeName 而是 /。通過覆蓋 / 的默認配置,我們就能修復這個問題。在 OrientationNavigatorObserver 添加這個監聽:

NavigatorObserver 的調用時機晚于 ContainerObserver

static void flutterContainerObserver(
      ContainerOperation operation, BoostContainerSettings settings) {
    debugPrint(
        'operation: $operation, settings: name -> ${settings.name} params -> ${settings.params}');
    if (operation == ContainerOperation.Push) {
      var routeName = settings.name;
      OrientationSupport orientationSupport =
          OrientationSupportManager.retrieve(routeName) ?? OrientationSupportManager.defaultOrientationSupport;
      OrientationSupportManager.register('/', orientationSupport);
    }
  }

總結

通過使用集中的配置將橫豎屏的設置代碼從各個頁面中抽離出來,各個頁面無感知的實現了頁面橫豎屏的設定。因為依賴使用 pushNamed 來進行頁面跳轉,也使得團隊的代碼風格更加統一,所有的頁面都能通過 routeName 來進行跳轉,方便了以后的開發和維護。現階段的局限是,因為無法獲取到頁面實例,也就無法通過頁面實例內部的屬性,來動態的調整橫豎屏。不過因為配置項是一個接口,通過自定義某個頁面的特殊配置類,然后再在頁面中修改這個配置類的靜態屬性的話,應該也能實現動態的修改,不過這樣的場景較為少見。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容