场景:flutter开发一个app,非module形式,即:app内部大部分页面是横屏,有部分页面是需要视屏显示(不参与喷子:写一个空控件旋转90度不就好了?但是这样的话状态栏之前的状态,如果你不需要状态栏的话那也没关系。我们不扯远,这里只是单纯的做技术的屏幕可旋转实现,来实现flutter控制iOS设备屏幕可旋转的可设定方位的限制)
下面我来讲一下关于iOS屏幕旋转的有效实现的三种方式,都是可以实现的,只是不同场景,由易到复杂递增,可根据不同的需求来选择参考使用,也希望能对你的成长有所帮助。
屏幕旋转控制方案我总结起来有三种(在不同场景下读是可以满足的):
一,开发工具直接限制可转动方向(不推荐),虽然我们开发的过程中经常是这样用的,方便快捷。但是这个前提下不满足)
二、用代码控制单个屏幕旋转与否(你可以统一继承,这里是从原理上是单个页面的处理方式来讲的)
override var shouldAutorotate: Bool
override var supportedInterfaceOrientations: UIInterfaceOrientationMask
这两个方法就可以控制屏幕的旋转和可支持的旋转方向了,具体实现应该网上很多了,就不再赘述了。
三、flutter app开发,通过桥接开控制,仿照现有的SystemChrome通过桥接控制,
那就会有小朋友讲:为什么不用SystemChrome来控制,答案是现在还没有兼容iOS,你可以试试就会发现这个问题。不然也不会有这篇文章的出现了。😄!
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeRight,
]);
也会有人说,不是还有一个orientation。答案是一样的效果。目前都是对安卓可以,但是iOS不支持,看也有给flutter官方提出这个问题,但是还没有得到解决。
那么接下来,我们就仿照官方推荐的SystemChrome来建立一个iOS的桥接来实现:
第一步我们先实现iOS这边的代码和桥接:
创建一个继承与NSObject的一个工具类FlutterIOSDevicePlugin来同一处理桥接事件接收
//
// FlutterIOSDevicePlugin.h
// Runner
//
// Created by 曹世鑫 on 2020/7/9.
//
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface FlutterIOSDevicePlugin : NSObject
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar flutterViewController:(nullable FlutterViewController*) controller;
- (instancetype)newInstance:(NSObject<FlutterPluginRegistrar>*)registrar flutterViewController:(nullable FlutterViewController*) controller;
@end
NS_ASSUME_NONNULL_END
//
// FlutterIOSDevicePlugin.m
// Runner
//
// Created by 曹世鑫 on 2020/7/9.
//
#import "FlutterIOSDevicePlugin.h"
#import <UIKit/UIKit.h>
@interface FlutterIOSDevicePlugin () {
NSObject<FlutterPluginRegistrar> *_registrar;
FlutterViewController *_controller;
UIDeviceOrientation iOSOrientation;
UIDeviceOrientation lastLandOrientation;
}
@end
static NSString *const CHANNEL_NAME = @"flutter_ios_device_rotation";
static NSString *const METHOD_CHANGE_ORIENTATION = @"change_screen_orientation";
@implementation FlutterIOSDevicePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel *channel = [FlutterMethodChannel
methodChannelWithName:CHANNEL_NAME
binaryMessenger:[registrar messenger]];
FlutterIOSDevicePlugin *instance = [[FlutterIOSDevicePlugin alloc] newInstance:registrar flutterViewController:nil];
[registrar addMethodCallDelegate:instance channel:channel];
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar flutterViewController:(FlutterViewController*) controller {
FlutterMethodChannel *channel = [FlutterMethodChannel
methodChannelWithName:CHANNEL_NAME
binaryMessenger:[registrar messenger]];
FlutterIOSDevicePlugin *instance = [[FlutterIOSDevicePlugin alloc] newInstance:registrar flutterViewController:controller];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (instancetype)newInstance:(NSObject<FlutterPluginRegistrar>*)registrar flutterViewController:(FlutterViewController*) controller{
_registrar = registrar;
_controller = controller;
iOSOrientation = [UIDevice currentDevice].orientation;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(rotateDevice:) name:UIDeviceOrientationDidChangeNotification object:nil];
return self;
}
- (void)rotateDevice:(NSObject *)sender {
UIDevice *device = [sender valueForKey:@"object"];
iOSOrientation = device.orientation;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([METHOD_CHANGE_ORIENTATION isEqualToString:call.method]) {
NSNumber *index = [NSNumber numberWithInt: [call.arguments[0] intValue]];
if ([index isEqualToNumber:@0]) {
if (iOSOrientation == UIDeviceOrientationLandscapeLeft || iOSOrientation == UIDeviceOrientationLandscapeRight){
lastLandOrientation = iOSOrientation;
}
iOSOrientation = UIDeviceOrientationPortrait;
} else if([index isEqualToNumber:@1]){
iOSOrientation = UIDeviceOrientationLandscapeLeft;
} else if([index isEqualToNumber:@2]){
iOSOrientation = UIDeviceOrientationLandscapeRight;
} else if([index isEqualToNumber:@3]){
if (iOSOrientation != UIDeviceOrientationPortrait && iOSOrientation != UIDeviceOrientationPortraitUpsideDown){
iOSOrientation = UIDeviceOrientationPortrait;
}
} else if([index isEqualToNumber:@4]){
if (iOSOrientation != UIDeviceOrientationLandscapeLeft && iOSOrientation != UIDeviceOrientationLandscapeRight){
if (lastLandOrientation) {
iOSOrientation = lastLandOrientation;
} else {
iOSOrientation = UIDeviceOrientationLandscapeLeft;
}
}
lastLandOrientation = iOSOrientation;
} else if([index isEqualToNumber:@6]){
if (iOSOrientation == UIDeviceOrientationPortraitUpsideDown) {
iOSOrientation = UIDeviceOrientationPortrait;
}
}
[[UIDevice currentDevice] setValue:@(iOSOrientation) forKey:@"orientation"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"FlutterIOSDevicePlugin" object:nil userInfo:@{@"orientationMask": index}];
[UIViewController attemptRotationToDeviceOrientation];
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
}
@end
这里我们在handleMethodCall里面设置了设备的方向,并通过[UIViewController attemptRotationToDeviceOrientation];来刷新设备的展示,事件里面看到我们发出了一个通知:[[NSNotificationCenter defaultCenter] postNotificationName:@"FlutterIOSDevicePlugin" object:nil userInfo:@{@"orientationMask": index}];
是通过通知将收到的参数传递到AppDelegate里面,进而控制设备旋转刷新的时候的可支持方向(下面我会贴上AppDelegate.swift的代码)
这里介绍设置设备方向代码,下面我列出了两种都可以,一个是kvc形式,一个是runtime形式(也可以说是kvc的内部实现方式)
//1.设置屏幕的转向为竖屏
[[UIDevice currentDevice] setValue:@(UIDeviceOrientationPortrait) forKey:@"orientation"];
//2.设置屏幕的转向为竖屏
- (void)interfaceOrientation:(UIInterfaceOrientation)orientation{
if([[UIDevicecurrentDevice] respondsToSelector:@selector(setOrientation:)]) {
SEL selector =NSSelectorFromString(@"setOrientation:");
NSInvocation*invocation = [NSInvocationinvocationWithMethodSignature:[UIDeviceinstanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevicecurrentDevice]];
intval = orientation;
// 从2开始是因为0 1 两个参数已经被selector和target占用
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}}
下面我们来看AppDelegate.swift里面的代码(这里有一部分是flutter app已经加入的,比如didFinishLaunchingWithOptions里面的GeneratedPluginRegistrant注册)
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var orientationMask: UIInterfaceOrientationMask = .all;
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
NotificationCenter.default.addObserver(self, selector: #selector(changeLandscape(center:)), name:NSNotification.Name(rawValue: "FlutterIOSDevicePlugin"), object: nil)
GeneratedPluginRegistrant.register(with: self)
FlutterIOSDevicePlugin.register(with: self.registrar(forPlugin: "FlutterIOSDevicePlugin"), flutterViewController: nil)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return orientationMask;
}
@objc func changeLandscape(center: Notification){
let index: NSNumber = (center.userInfo?["orientationMask"] ?? 5) as! NSNumber
orientationMask = OrientationUtil.getOrientationMaskWithIndex(number: index)
_ = application(UIApplication.shared, supportedInterfaceOrientationsFor: UIApplication.shared.keyWindow)
}
}
这里我们在设备支持方向的代码中添加了自定义的方向设定,在接收到通知的方法中处理好支持的方向类型,然后我们可以手动调用一下supportedInterfaceOrientationsFor来实现一下触发。
另外这里用到swift调用oc的实现。所以我们需要在已存在的桥接文件Runner-Bridging-Header.h里面加入我们的oc头文件(如果你想了解怎么创建桥接文件可以自行百度,这里不再做赘述),结果如下:
#import "GeneratedPluginRegistrant.h"
#import "FlutterIOSDevicePlugin.h"
可以看到AppDelgate.swift里面有一个changeLandscape来处理通知接收到的信息的OrientationUtil用来处理(这里的对应关系要和flutter端的定义一一对应,这里两端都是我写的,哈哈,就我自己和自己协商了,就都按照iOS官方的命名来近似定义了):
//
// OrientationUtil.swift
// Runner
//
// Created by 曹世鑫 on 2020/7/10.
//
import UIKit
class OrientationUtil: NSObject {
//转化工具
static func getOrientationMaskWithIndex(number: NSNumber) -> UIInterfaceOrientationMask {
var mask : UIInterfaceOrientationMask = .all
switch number {
case 0:
mask = .portrait
break
case 1:
mask = .landscapeLeft
break
case 2:
mask = .landscapeRight
break
case 3:
mask = .portraitUpsideDown
break
case 4:
mask = .landscape
break
case 5:
mask = .all
break
case 6:
mask = .allButUpsideDown
break
default:
mask = .all
break
}
return mask
}
}
接下来。我们就转向flutter代码:
首先我们创建一个屏幕旋转的桥接类,由于安卓得不需要,这里我就单设置iOS的了:ScreenRotationiOS
class ScreenRotationiOS {
static const MethodChannel _channel = MethodChannel('flutter_ios_device_rotation');
static Future changeScreenOrientation(DeviceOrientationMask orientationMask) {
return _channel.invokeMethod('change_screen_orientation', [orientationMask.index]);
}
}
enum DeviceOrientationMask {
//竖屏
Portrait,
//左旋转
LandscapeLeft,
//右旋转
LandscapeRight,
//竖直方向向上向下可旋转
PortraitUpsideDown,
//横屏
Landscape,
//全部四个方位
All,
//左右旋转
AllButUpsideDown,
}
可以看到这里的注释枚举是和iOS方面的switch的接收是一一对应的。
接下来就是使用:
在flutter代码中需要设置的地方:
if (Platform.isIOS) {
await ScreenRotationiOS.changeScreenOrientation(
DeviceOrientationMask.Landscape);
}
内容枚举自定义选择。
这里我们也可以不用加上判断是iOS平台才执行,但是本着之后代码的清晰,防止新来小伙伴再以为安卓里面也有定义的桥接造成不必要的误会,建议还是使用的地方加上iOS平台的判断。
然后就写一个demo来看一下效果吧:
至此,全部结束!
偶然遇到的小问题:
1.运行在iphone上面有效果,在ipad上面触发没反应。还是会上下左右任性的旋转。
解决办法:在iOS项目中打开info.plist打开全屏显示:
喜欢的小朋友们可以关注我一下吧。我会及时更新有意义的技术文章和攻坚点的。
已发布到pub.dev,项目中使用到的同学,可以直接yaml里面添加依赖:limiting_direction_csx 喜欢的可以点一个like,鼓励一下。谢谢了。