2 min read

【译】Flutter控制特定的屏幕方向

横屏有时候是免不了的,尤其是视频播放这种界面。

原文《Controlling screen orientation in Flutter apps on a per-screen basis》,作者是Roman Petrov

设置屏幕方向

作者首先介绍了一个SystemChrome全局类,通过调用下面的类方法可以设置整个应用的屏幕方向:

setPreferredOrientations(List<DeviceOrientation> orientations) → Future<void> 

然后封装了一个方法来简化操作:

enum ScreenOrientation {
  portraitOnly,
  landscapeOnly,
  rotating,
}

void _setOrientation(ScreenOrientation orientation) {
  List<DeviceOrientation> orientations;
  switch (orientation) {
    case ScreenOrientation.portraitOnly:
      orientations = [
        DeviceOrientation.portraitUp,
      ];
      break;
    case ScreenOrientation.landscapeOnly:
      orientations = [
        DeviceOrientation.landscapeLeft,
        DeviceOrientation.landscapeRight,
      ];
      break;
    case ScreenOrientation.rotating:
      orientations = [
        DeviceOrientation.portraitUp,
        DeviceOrientation.landscapeLeft,
        DeviceOrientation.landscapeRight,
      ];
      break;
  }
  SystemChrome.setPreferredOrientations(orientations);
}

监听导航事件

但重点是要在Navigator 进行push时设置屏幕方向,在pop的时候重置为上一个界面的屏幕方向。可以通过自定义一个NavigatorObserver,实现didPopdidPush 两个方法来达到目的。

class NavigatorObserverWithOrientation extends NavigatorObserver {
  @override
  void didPop(Route route, Route previousRoute) {
    if (previousRoute.settings.arguments is ScreenOrientation) {
      _setOrientation(previousRoute.settings.arguments);
    } else {
      // Portrait-only is the default option
      _setOrientation(ScreenOrientation.portraitOnly);
    }
  }

  @override
  void didPush(Route route, Route previousRoute) {
    if (route.settings.arguments is ScreenOrientation) {
      _setOrientation(route.settings.arguments);
    } else {
      _setOrientation(ScreenOrientation.portraitOnly);
    }
  }
}

注意:作者使用了RouteSettingsarguments 字段来存储屏幕方向,如果argumentsnull或者不是ScreenOrientation类型,比如自定义的值,那么只设置为portraitOnly

最后在生成路由的时候指定屏幕方向:

class AppRoutes {
  static final home = "/";
  static final portrait = "/portrait";
  static final landscape = "/landscape";
  static final rotating = "/rotating";
}

RouteSettings rotationSettings(RouteSettings settings, ScreenOrientation rotation) {
  return RouteSettings(name: settings.name, arguments: rotation);
}

class MyApp extends StatelessWidget {
  final _observer = NavigatorObserverWithOrientation();

  Route<dynamic> _onGenerateRoute(RouteSettings settings) {
    if (settings.name == AppRoutes.home) {
      return MaterialPageRoute(builder: (context) => HomeScreen());
    } else if (settings.name == AppRoutes.portrait) {
      return MaterialPageRoute(builder: (context) => PortraitScreen());
    } else if (settings.name == AppRoutes.landscape) {
      return MaterialPageRoute(
        builder: (context) => LandscapeScreen(),
        settings: rotationSettings(settings, ScreenOrientation.landscapeOnly),
      );
    } else if (settings.name == AppRoutes.rotating) {
      return MaterialPageRoute(
        builder: (context) => RotatingScreen(),
        settings: rotationSettings(settings, ScreenOrientation.rotating),
      );
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Orientation Demo',
      theme: ThemeData(primarySwatch: Colors.blue,),
      onGenerateRoute: _onGenerateRoute,
      navigatorObservers: [_observer],
    );
  }
}

总结

完整的工程在这里。作者最后提了一下,这个方案不是完美的,因为屏幕旋转时,路由转场动画也在同一时间发生,这可能导致一些意料之外的问题。但对作者来说,这已经够了,符合他的App需求。