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阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容