《iOS 6高级开发手册(第4版)》——1.13节使用外部屏幕-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

《iOS 6高级开发手册(第4版)》——1.13节使用外部屏幕

简介:

本节书摘来自异步社区《iOS 6高级开发手册(第4版)》一书中的第1章,第1.13节使用外部屏幕,作者 【美】Erica Sadun,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.13 使用外部屏幕
iOS 6高级开发手册(第4版)
可以用许多方式使用外部屏幕。例如,采取最新款的iPad。第二代和第三代型号提供了内置的屏幕监测。连接VGA或HDMI电缆,就可以把内容显示在外部显示器和内置屏幕上。某些设备允许使用AirPlay(Apple的专有无线缆空中下载视频解决方案)把屏幕以无线方式镜像到Apple TV。这些镜像特性极其方便,但是并不仅限于在iOS中简单地把一个屏幕上的内容复制到另一个屏幕上。

UIScreen类允许独立地检测并写到外部屏幕上。可以把任何连接的显示器视作一个新窗口,并为该显示器创建内容,使之独立于主设备屏幕上显示的任何视图。可以为任何有线屏幕执行该操作,并且从iPad 2(及更高型号)和iPhone 4S(及更高型号)开始,可以使用AirPlay to Apple TV 2(及更高型号)以无线方式执行该操作。名为Reflector的第三方应用程序允许使用AirPlay把显示器镜像到Mac或Windows计算机。

几何学很重要。为什么呢?iOS设备目前包括320像素×480像素的老式iPhone显示器、640像素×960像素的Retina显示器单元和1024像素×768像素的iPad。典型的复合/分量输出是在720像素×480像素(480i和480p)、1024像素×768像素和1280像素×720像素(720p)下的VGA上产生的,然后还有更高质量的HDMI输出可用。

除此之外,还有过扫描的问题及其他目标显示器的局限性,并且Video Out会迅速变成一个几何挑战。幸运的是,Apple利用一些方便的现实修改来响应这种挑战。无需尝试在输出屏幕与内置的设备屏幕之间创建一种一对一的关系,而可以基于输出显示器的可用属性构建内容。只需创建一个窗口,填充它,并显示它。 如果打算开发Video Out应用程序,不要想当然地认为用户会严格地使用AirPlay。许多用户仍然使用老式的电缆接头连接到显示器和投影仪。确保每种电缆至少具有一根(复合、分量、VGA和HDMI),还要具有准备好使用AirPlay的iPhone和iPad,以便可以彻底地测试每种输出配置。第三方电缆(通常是从远东进口的,没有打上Made for iPhone/iPad的标签)将不会工作,因此要确保购买具有Apple品牌的物品。

1.13.1 检测屏幕
UIScreen类可以报告连接了多少个屏幕。你知道无论何时这个计数大于1,都会连接有外部屏幕。screens数组中的第一项总是主设备屏幕:

#define SCREEN_CONNECTED ([UIScreen screens].count > 1)

每个屏幕都可以报告它的边界(即它的物理尺寸,以磅为单位)以及它的屏幕比例(将磅与像素关联起来)。两个标准的通知使你能够观察屏幕何时连接到设备以及与设备断开。

// Register for connect/disconnect notifications
[[NSNotificationCenter defaultCenter]
    addObserver:self selector:@selector(screenDidConnect:)
    name:UIScreenDidConnectNotification object:nil];
[[NSNotificationCenter defaultCenter]
    addObserver:self selector:@selector(screenDidDisconnect:)
    name:UIScreenDidDisconnectNotification object:nil];

连接意指任意类型的连接,无论是通过电缆还是通过AirPlay。无论何时接收到这种类型的更新,都一定要统计屏幕数量,并调整用户界面,以匹配新的情况。

你的职责是:无论何时加入新屏幕,都要建立一些窗口,一旦发生分离事件,就要清除它们。每个屏幕都应该具有它自己的窗口,为输出显示器管理内容。对于分离的屏幕,不要抓住它们的窗口不放。释放它们,然后当新屏幕出现时重新创建它们。

注意:

在screens数组中不会表示出镜像的屏幕,而代之以将镜像存储在主屏幕的mirroredScreen属性中。当禁用、未连接或者设备的能力简直不支持镜像时,这个属性为空。

创建一个新屏幕并把它用于独立的外部显示器总会撤销镜像。因此,即使用户启用了镜像,当应用程序开始写到并创建外部显示器,它将具有优先级。

1.13.2 获取屏幕分辨率
每个屏幕都提供了一个availableModes属性。这是一个分辨率对象的数组,其中的元素按从最低分辨率到最高分辨率排序。每种模式都有一个size属性,指示一个目标像素大小的分辨率。许多屏幕都支持多种模式。例如,VGA显示器可能具有多达6种模式,或者具有比它提供的更多不同的分辨率。支持的分辨率数量因硬件而异。总是至少有一种分辨率可用,但是当具有多种分辨率时,应该给用户提供选择的机会。

1.13.3 设置Video Out
在从[UIScreens screens]数组中获取了外部屏幕对象之后,可以查询可用的模式并选择要使用的尺寸。一般说来,可以放弃选择列表中的最后一种模式,总是使用尽可能高的分辨率,或者放弃使用最低分辨率的第一种模式。

要开始一个Video Out(视频输出)流,可以创建一个新的UIWindow,并将其尺寸调整为所选的模式。给那个窗口添加一个新视图,以便在其上绘图。然后把窗口分配给外部屏幕,并使之成为关键的和可见的窗口。这命令窗口显示出来,并准备好使用它。之后,再次使原始窗口成为关键窗口。这允许用户继续与主屏幕交互。不要跳过这一步。对于最终用户来说,什么也不会比发现他们昂贵的设备不再响应他们的触摸更令他们暴躁:

self.outputWindow = [[UIWindow alloc] initWithFrame:theFrame];
outputWindow.screen = secondaryScreen;
[outputWindow makeKeyAndVisible];
[delegate.view.window makeKeyAndVisible];

1.13.4 添加显示器链接
显示器链接是一种计时器,以便把绘图与显示器的刷新率进行同步。可以通过更改显示器链接的frameInterval属性,来调整这个画面刷新时间。该属性默认为1,更高的数字会降低刷新率。如果把它设置为2,则会把画面刷新率减半。当屏幕连接到设备时,就会创建显示器链接。UIScreen类实现了一个方法,用于返回它的屏幕的显示器链接对象。可以为显示器链接指定一个目标以及要调用的选择器。

显示器链接定期触发,以让你知道何时更新Video Out屏幕。如果CPU负载较小,可以把时间间隔调长一些,但是这将会获得较低的画面刷新率。这是一个重要的折衷,尤其是对于在设备端需要具有高级CPU响应的直接操作界面更是如此。 秘诀1-8中的代码为运行循环使用了常见的模式,提供最少的等待时间。在处理显示器链接时,如果使之无效(invalidate),就将它从运行循环中删除。

1.13.5 过扫描补偿
UIScreen类允许通过给overscanCompensation属性赋值,补偿显示器屏幕边缘的像素损失。在Apple的文档中描述了可以指派的技术,但是它们基本上相当于你是想剪辑内容还是想用空格填充它。

1.13.6 VIDEOkit
秘诀1-8介绍了VIDEOkit,它是一个基本的外部屏幕客户。该秘诀演示了配置和使用有线与无线外部屏幕所需的所有特性。通过调用startupWithDelegate:来建立屏幕监测。把主视图控制器传递给它,该控制器的职责将是创建外部内容。

内部的init方法开始侦听屏幕连接和分离事件,并根据需要构建和删除窗口。每次显示器链接触发时,都会调用一个非正式的委托方法(updateExternalView:)。它将传递外部窗口上的视图,委托可以根据需要在该窗口上绘图。

在这个秘诀的配套示例代码中,视图控制器委托存储一个本地颜色值,并使用它给外部显示器着色:

- (void) updateExternalView: (UIImageView *) aView
{
    aView.backgroundColor = color;
}

- (void) action: (id) sender
{
    color = [UIColor randomColor];
}

每次按下动作按钮时,视图控制器都会生成一种新颜色。当VIDEOkit查询控制器以更新外部视图时,将把该颜色设置为背景色。可以看到外部屏幕即时更新为新的随机颜色。

注意:

Reflector App(单一许可的费用是15美元,5台计算机的许可费用是50美元,reflectorapp.com)为AirPlay提供了一个优秀的调试伙伴,它提供了一种可以在Mac和Windows计算机上工作的无线/无Apple TV的解决方案。它模拟Apple TV AirPlay接收器,允许从iOS直接广播到桌面,并记录该输出。
秘诀1-8 VIDEOkit

@interface VIDEOkit : NSObject
{
    UIImageView *baseView;
}
@property (nonatomic, weak) UIViewController *delegate;
@property (nonatomic, strong) UIWindow *outputWindow;
@property (nonatomic, strong) CADisplayLink *displayLink;
+ (void) startupWithDelegate: (id) aDelegate;
@end

@implementation VIDEOkit
static VIDEOkit *sharedInstance = nil;

- (void) setupExternalScreen
{
    // Check for missing screen
    if (!SCREEN_CONNECTED) return;

    // Set up external screen
    UIScreen *secondaryScreen = [UIScreen screens][1];
    UIScreenMode *screenMode =
        [[secondaryScreen availableModes] lastObject];
    CGRect rect = (CGRect){.size = screenMode.size};
    NSLog(@"Extscreen size: %@", NSStringFromCGSize(rect.size));

    // Create new outputWindow
    self.outputWindow = [[UIWindow alloc] initWithFrame:CGRectZero];
    _outputWindow.screen = secondaryScreen;
    _outputWindow.screen.currentMode = screenMode;
    [_outputWindow makeKeyAndVisible];
    _outputWindow.frame = rect;

    // Add base video view to outputWindow
    baseView = [[UIImageView alloc] initWithFrame:rect];
    baseView.backgroundColor = [UIColor darkGrayColor];
    [_outputWindow addSubview:baseView];

    // Restore primacy of main window
    [_delegate.view.window makeKeyAndVisible];
}

- (void) updateScreen
{
    // Abort if the screen has been disconnected
    if (!SCREEN_CONNECTED && _outputWindow)
        self.outputWindow = nil;

    // (Re)initialize if there's no output window
    if (SCREEN_CONNECTED && !_outputWindow)
        [self setupExternalScreen];

    // Abort if encountered some weird error
    if (!self.outputWindow) return;

    // Go ahead and update
    SAFE_PERFORM_WITH_ARG(_delegate,
        @selector(updateExternalView:), baseView);
}

- (void) screenDidConnect: (NSNotification *) notification
{
    NSLog(@"Screen connected");
    UIScreen *screen = [[UIScreen screens] lastObject];

    if (_displayLink)
    {
        [_displayLink removeFromRunLoop:[NSRunLoop currentRunLoop]
            forMode:NSRunLoopCommonModes];
        [_displayLink invalidate];
            _displayLink = nil;
    }

    self.displayLink = [screen displayLinkWithTarget:self
        selector:@selector(updateScreen)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
        forMode:NSRunLoopCommonModes];
}

- (void) screenDidDisconnect: (NSNotification *) notification
{
    NSLog(@"Screen disconnected.");
    if (_displayLink)
    {
        [_displayLink removeFromRunLoop:[NSRunLoop currentRunLoop]
            forMode:NSRunLoopCommonModes];
        [_displayLink invalidate];
            self.displayLink = nil;
    }
}

- (id) init
{
    if (!(self = [super init])) return self;

    // Handle output window creation
    if (SCREEN_CONNECTED)
        [self screenDidConnect:nil];

    // Register for connect/disconnect notifications
    [[NSNotificationCenter defaultCenter]
        addObserver:self selector:@selector(screenDidConnect:)
        name:UIScreenDidConnectNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(screenDidDisconnect:)
        name:UIScreenDidDisconnectNotification object:nil];

    return self;
}

- (void) dealloc
{
    [self screenDidDisconnect:nil];
    self.outputWindow = nil;
}
+ (VIDEOkit *) sharedInstance
{
    if (!sharedInstance)
    sharedInstance = [[self alloc] init];
    return sharedInstance;
}

+ (void) startupWithDelegate: (id) aDelegate
{
    [[self sharedInstance] setDelegate:aDelegate];
}
@end

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: