iOS多显示器编程指导
使用窗口(window)在多个显示设备上显示内容
每一个iOS应用都会有一个窗口来处理用户界面的显示问题。窗口提供了很关键的功能,但是大多数应用都不会直接访问它。典型情况下,只有那些支持外接显示设备的应用才需要与窗口打交道。
概要
在iOS中,一个窗口对象包含应用的视图,并管理视图在显示设备上的显示。一个窗口会与一个屏幕(screen)对象关联,这个屏幕对象代表了当前正在使用的显示设备。如果你的应用支持在外接显示设备显示内容,那么你就需要创建另外一个窗口对象,并使用这个窗口对象来管理内容如何在外接设备上显示。
窗口在应用中提供了重要的功能
除了包含应用的可见内容,窗口也起着向视图传递触摸事件以及响应方向变化等作用。与窗口关联的屏幕对象当前正在使用的显示设备的有关信息。当你使用storyboards来处理应用的显示界面时,主storyboard会自动的创建一个窗口来代表默认显示设备。
一个额外的窗口来支持外接显示设备
如果你的应用支持外接设备显示,你就需要创建一个单独的窗口对象来代表它。你可以在两个显示设备上显示同样的内容(这个镜像特性,事实上也是默认发生的),你也可以在每个设备上显示不用的内容。
理解窗口和屏幕
一个窗口处理应用UI的所有外观。窗口与视图一起来管理可见视图结构的相互作用和变化。
每一个应用都有一个窗口在iOS系统默认的显示设备上显示应用的用户界面。当一个外接显示设备连接到iOS设备,应用也可以创建另外一个窗口,并在此外接显示设备上显示内容。
窗口在iOS应用中充当的角色
在iOS应用和Mac应用中,窗口扮演着不相同的角色。在iOS中,窗口没有标题栏,关闭按钮以及一些其他的可视装饰元素。用户不会看到,关闭,或者移动iOS应用中的窗口。在Mac应用中经常会打开一个新窗口以显示新的内容,而在iOS应用中通常是改变窗口中的视图。
窗口对象-也就是UIWindow实例-有许多重要的职责
· 它包含应用的可见内容。
· 它在向视图及其他应用对象传递触摸事件中起关键作用。
· 它和视图控制器一起处理方向转动。
在大多数情况下,你不需要做任何事情,应用的窗口就已经能正常工作。如果你使用storyboard创建应用的用户界面,那么显示创建一个窗口对象的唯一原因便是支持外接设备显示。
窗口的根视图包含你的内容
一个窗口通常会有一个根视图(在一个相应的视图控制器中管理)。并在根视图包含所有其他显示内容的视图。一个根视图简化了界面改变的流程,当你需要显示新内容时,你只需要替换根视图。
你可以使用任意你想要的视图作为根视图。根据你界面设计的需要,根视图可以是一个包含其他子视图的UIView对象,也可以是一个标准的UIKit视图,还可以是你自定制的视图。经常被作用根视图的标准UIKit视图包含滚动视图(scroll view),表格视图(table view),和图像视图(p_w_picpath view)。
要点:如果窗口的根视图由容器视图控制器中提供,例如标签栏控制器(tab bar controller),导航控制器(navigation controller),或者分割视图控制器(split-view controller),你无需设置视图的初始大小。容器视图控制器会根据状态栏是否可见来自动调整视图大小。
主窗口(key window)接受特定事件
一个窗口当前能接受键盘和非触摸事件时,便被认为是主窗口。而触摸事件则被投递到触摸发生的窗口,没有相应坐标值的事件被投递到主窗口。同一时刻只有一个窗口是主窗口。
大部分时间内,应用窗口是主窗口。由于iOS使用单独的窗口来显示警告视图(alert view)和输入附件视图(input accessory view),这些窗口也可以成为主窗口。例如,当一个讲稿视图或者输入附件视图有一个文本框,而用户当前正在这个文本框中输入,那么包含此输入视图的窗口便是主窗口。
绝大多数应用窗口处在同一等级
当你为应用创建一个新窗口时,UIVindow类自动赋给它一个等级,称为正常窗口等级(normal window level),这个等级对显示应用相关内容的窗口是合适的。这个等级,可以通过windowLevel属性来设置,它代表窗口相对其他窗口在z轴所处的位置。你可以使与应用相关的窗口在其他等级显示,但这不是必须的。
除了用于显示应用相关内容的窗口外,还有一些高等级的窗口,他们显示一些需要悬浮在应用内容之上的信息。例如系统状态栏和警告等。
每个窗口与一个显示设备关联
UIWindow类的screen属相代表窗口当前用于显示的显示设备。这个属性包含一个屏幕对象,也就是UIScreen的一个实例,这个对象包含显示设备的信息,例如它的边框,模式,和亮度。
屏幕对象也包括一些通知用来监听显示设备的变动。例如你可以注册显示设备的连接或断开,或者显示设备的模式或者亮度改变的通知。
窗口通知帮助你监听变化
iOS定义了许多表明窗口或者屏幕对象变化的通知。一般而言,这些通知对于支持外部显示器的应用是有帮助的。
除了表明何时键盘是可见的通知(例如UIKeyboardDidShowNotification),UIWindow还定义了以下通知:
· UIWindowDidBecomeVisibleNotification
· UIWindowDidBecomeHiddenNotification
· UIWindowDidBecomeKeyNotification
· UIWindowDidResignKeyNotification
每当应用窗口发生编程变化时,UIWindow通知就会被投递。例如,当你的应用显示或者隐藏一个窗口时,UIWindowDidBecomeVisibleNotification和UIWindowDidBecomeHiddenNotification通知相应地就会被投递。值得注意的是,当应用转移到后台时,这些通知不会被投递:即便应用转到后台时,窗口不会显示,窗口在应用的上下文中仍然被认为是可见的。
大多数应用不需要处理UIWindowDidBecomeVisibleNotification和UIWindowDidBecomeHiddenNotification通知,很少有应用拥有一个以上窗口。
UIWindowDidBecomeKeyNotification 和 UIWindowDidResignKeyNotification能帮助你跟踪应用窗口何时是主窗口,何时不是。当你通过显示一个输入配件视图来或者用户输入时,你也许需要知道一个窗口是不是主窗口。
窗口和屏幕支持少许其他任务
一般来说,除了需要支持外接显示器这种情况外,你很少需要访问应用的窗口和屏幕对象。当应用启动后,也就是窗口被创建,加载,以及设置后,只有少许事情需要由窗口处理。
· 使用窗口对象来转换点和矩形的坐标。例如,你有一个窗口坐标系中的值,,你可能想把它转换成一个特定视图坐标系中的值,然后使用。
· 使用窗口的通知来跟踪窗口相关的变化。窗口在显示和隐藏以及成为或放弃主窗口状态时,都会生成通知。你可以借助这些通知在应用的其他地方执行一些行动。
类似地,只有个别原因才需要访问屏幕对象。其中一个原因是你需要调节屏幕的亮度。例如,你可以使用brightness属性让用户可以调节iOS设备的屏幕的亮度。你还可以通过wantsSoftwareDimming属性来表明应用需要将屏幕亮度调整到比中等亮度偏暗的级别。(需要注意的是,打开wantsSoftwareDimming可能会对性能有影响,因为这种昏暗是通过软件来实现的。)
Xcode和Storyboards能创建,设置和加载窗口
当你在Xcode模板基础上创建一个新应用,然后使用storyboard来设计用户界面,你不需要手工创建,设置和加载应用的窗口。
当你创建一个storyboard文件,然后在资料属性列表文件(information property list file)中将它设置为主storyboard,iOS将为你执行一些启动任务。在启动的时候iOS:
· 实例化一个窗口
· 装载主storyboard,然后实例化它的起始视图控制器。
· 将这个视图控制器作为窗口的rootViewController,然后让窗口显示。
在起始视图控制器显示之前,应用委托(app delegate)有机会改变此视图控制器的设置。
你应该尽可能的使用storyboard来设定应用的用户界面(iOS 5.0之前版本不支持storyboard)。如果你选择nib文件而不是storyboard来创建应用的界面,你可能仍然不需要创建一个窗口对象,多数Xcode模板已经为你创建了一个。(如果你拖拽一个窗口对象到Interface Builder文件中,需要设置它的Full Screen at Launch属性,已确保窗口在对应设备上大小合适。)
如果你使用nib文件而不是storyboard来创建应用界面,你需要确保在启动的时候主nib文件中的内容被安装到窗口中。你可以在application:willFinishLaunchingWithOptions:方法中通过以下代码来完成:
- window.rootViewController = myViewController;
在极少数情况中,你想以编码的方式创建应用的窗口。您将使用类似的代码以编程方式创建一个窗口,安装根视图控制器,然后使该窗口可见:
- - (BOOL)application:(UIApplication *)application
- willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
- UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
- myViewController = [[MyViewController alloc] init];
- window.rootViewController = myViewController;
- [window makeKeyAndVisible];
- return YES;
- }
在外接显示器中显示内容
如果你通过AirPlay允许用户将内容重定向到外接显示器中,在第二个显示器中,你可以显示当前应用的内容,也可以显示不同的内容。
你可以通过在每个显示器上显示不同内容来提高用户体验。例如,你可以在主设备的显示器上显示应用的界面,同时在外接显示设备上显示高清媒体内容。
在外接显示器上显示不同内容,你可以按照以下基本步骤:
1. 在应用启动时,检查是否存在外接显示器,并注册屏幕连接和断开通知。
2. 当外接显示器可用时(在应用启动时或者应用运行过程中),为外接显示器创建一个窗口并设置它。
3. 显示此窗口,并正常更新。
重点:确保窗口正确地设置了状态栏的方向。外接显示器不能通过主设备加速器来响应方向变化,而是依赖应用设置的状态栏方向来响应方向的变化。
外接显示器已经存在时,创建一个窗口
在应用启动时,你可以通过测试UIScreen对象中screens数组来检查当前是否存在外接显示器。通常情况下,screen数组中只有一个screen对象,它代表主iOS设备中的显示器。如果用户连接到了一个外接显示器,这个数组就包含一个额外的屏幕对象。iOS设备中支持外接显示器的包括带有视网膜屏的iPhone和iPod touch以及iPad。旧版本的设备例如iPhone 3GS不支持外接显示器。
Listing 2-1 显示在应用启动时检查是否存在一个外接显示器。如果存在,对应地为它创建一个窗口。
Listing 2-1 检测是否存在外接显示器
- - (void)checkForExistingScreenAndInitializeIfPresent {
- if ([[UIScreen screens] count] > 1) {
- UIScreen *secondScreen = [[UIScreen screens] objectAtIndex:1];
- CGRect screenBounds = secondScreen.bounds;
- self.secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
- self.secondWindow.screen = secondScreen;
- self.secondWindow.hidden = NO;
- }
- }
要点:你应该先将窗口与屏幕对象关联,然后显示窗口。虽然你可以修改当前可见窗口对应的屏幕对象,但是此操作代价很大,应该尽量避免。
注册连接和断开通知
当用户连上或者断开一个显示器时,系统向应用发送对应的通知。这些类型的通知对处理外接显示设备变动至关重要。你应该使用这些通知更新应用状态,创建或者销毁与外接显示器对应的窗口。
值得注意的是,这些连接和断开通知随时可能出现,甚至当应用在后台暂停的时候也可能出现。由于不能预测这些通知何时出现,最后的做法是在一个应用生命周期内一直存在的对象中观察这些通知,例如在应用委托中。如果应用存于暂停状态,这些消息进入队列中保存,直到应用退出暂停状态并开始在前台或者后台运行。
Listing 2-2 提供了一种注册这些通知的方法
Listing 2-2 注册屏幕连接和断开通知
- - (void)setupScreenConnectionNotificationHandlers {
- NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
- [center addObserver:self selector:@selector(handleScreenDidConnectNotification:)
- name:UIScreenDidConnectNotification object:nil];
- [center addObserver:self selector:@selector(handleScreenDidDisconnectNotification:)
- name:UIScreenDidDisconnectNotification object:nil];
- }
处理连接和断开通知
在你定制的通知处理方法中你接受到的通知对象是对应的屏幕对象。和应用启动时一样,你需要获取新屏幕的边框,创建一个窗口,并用相应尺寸大小初始化它。然后设置窗口的屏幕属性为新的屏幕对象,并且设置窗口的隐藏属性为NO,如Listing 2-3所示。
Listing 2-3 处理屏幕连接和断开通知
- - (void)handleScreenDidConnectionNotification:(NSNotification *)aNotification {
- UIScreen *newScreen = [aNotification object];
- CGRect screenBounds = newScreen.bounds;
- if (!self.secondWindow) {
- self.secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
- self.secondWindow.screen = newScreen;
- [viewController displaySelectionInSecondaryWindow:self.secondWindow];
- }
- }
- - (void)handleScreenDidDisconnectNotification:(NSNotification *)aNotification {
- if (self.secondWindow) {
- self.secondWindow.hidden = YES;
- self.secondWindow = nil;
- [viewController displaySelectionOnMainScreen];
- }
- }
不管是通知处理体中还是在应用启动时创建的另一个窗口,你接下来的所有绘制操作都发生在这个窗口中。要返回到主设备的显示器上绘制,可以使用window setScreen: 方法设置窗口到内部显示器上。你可以在两个显示屏上自由切换。所有绘制操作,包括OpenGL操作,都被定向到当前显示屏上。
必要时调整外接显示屏的属性
许多显示屏支持多个分辨率,其中一些使用不同的像素宽高比。一个UIScreen对象默认使用显示屏的最佳屏幕模式,最可能的使用这种模式当然也是最好的。你可以通过UIScreen对象的preferredMode属性来获取显示屏的最佳模式。
在一些情况下,在将屏幕关联到窗口之前,你也许需要调整显示屏模式,使其更适合你的内容。例如,你通过OpenGL ES实现了一个游戏,其中的纹理是按照640x480像素分辨率屏幕来设计的,在一些默认是更高分辨率的显示器上,你就可以改变屏幕的模式。在改变屏幕的模式前,请确保内容被放大时,用户的体验依然是可以接受的。
如果你打算使用一个其他屏幕模式,而不是默认模式,那么你应该在将屏幕关联到窗口之前就对此屏幕应用这种模式。UIScreenMode类定义了屏幕模式的属性。你可以通过availableModes属性来获取屏幕对象支持的模式列表,然后遍历列表找到一个符合你需要的模式。
注意:不保证所有模式都会被一个外接显示器支持,所以你不应该依赖一个特定的模式。
作为对屏幕模式的补充,一个UIScreen对象包含overscanCompensation属性,必要时,你可以通过它调整外接显示器的过扫描补偿。过扫描指的是一种起源与阴极射线管显示器的行为。由于技术上的限制,旧的CRT扫描输入的图片时可能会越过显示管的边界,造成显示的图像不完整。虽然这个技术限制应该解决,许多广播和显示器厂家仍然期望过扫描。使用overscanCompensation属性的默认值,也就是UIScreenOverscanCompensationScale,当iOS检测到外接显示器存在过扫描时,它会适当的缩放你的内容。
在极少数情况下,你需要使用其他值来设置overscanCompensation属性,这样做总会导致你不得不做更多的工作。例如,当你使用UIScreenOverscanCompensationInsetBounds,你必须准备处理非标准显示器的尺寸大小。
最后,你也许想使用displayLinkWithTarget:selector:方法创建一个Core Animation display link对象,使用这个对象根据外接显示设备的刷新率来同步绘制。
转载于:https://blog.51cto.com/iosfocus/1059843