iOS旋转视图实践

http://rdc.taobao.org/?p=408

在天气,股票,数据分析等app中,我们经常看到这样一个有趣的功能:在设备垂直方向使用的时候,可以看到一个数据图表的缩略图,在设备旋转到水平方向的时候,缩略图会放大至全屏,显示更加详细的数据图表。利用手持设备的旋转感应来切换缩略图和详细图,这样的交互显得轻巧和优雅。下面我们就来实践一下,如何在iOS中实现这个功能:我们首先介绍一下设备旋转的响应,之后我们用微淘中的一个例子来说明如何在旋转的过程中切换不同的视图,最后我们引入一个人为旋转的引导性功能并使它与设备旋转相兼容。

controller对旋转的响应

首先,在iOS5(及之前)和iOS6中对设备旋转的回调略有不同: 
在iOS6中,使用以下代码来控制是否响应设备旋转

- (BOOL)shouldAutorotate
{
return YES;
}

- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationIsPortrait(UIInterfaceOrientationMaskPortrait|| UIInterfaceOrientationMaskPortraitUpsideDown);

}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationIsPortrait(UIInterfaceOrientationPortrait|| UIInterfaceOrientationPortraitUpsideDown);

}

在iOS5及之前的SDK中,我们使用以下代码

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
{
if ((orientation == UIInterfaceOrientationPortrait) || (orientation == UIInterfaceOrientationLandscapeLeft))
return YES;

return NO;
}

实现旋转前后的视图切换

接下来,我们以微淘这个应用中的价格曲线横屏全屏这个功能来说明,如何在设备旋转前后显示不同的视图。 
首先我们看一对图片: 
iOS旋转视图实践_第1张图片iOS旋转视图实践_第2张图片
从以上两张图片中可以看到旋转后,横纵坐标轴的间隔,标题显示的内容和字体大小都发生了改变,状态栏和工具栏在旋转后也被隐藏了。下面我们就来讨论下实现的方法。

设备旋转的回调

首先在设备旋转的回调中通知视图,当前的设备方向发生了改变,并进一步通知价格曲线,使之重绘,代码如下:

//设备旋转前
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if (toInterfaceOrientation == UIInterfaceOrientationPortrait || toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
//如果竖屏显示status bar
[[UIApplication sharedApplication] setStatusBarHidden:NO];
} else {
//如果横屏隐藏status bar
[[UIApplication sharedApplication] setStatusBarHidden:YES];
}
}

//设备旋转前
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//对视图进行旋转处理
if (toInterfaceOrientation == UIInterfaceOrientationPortrait || toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
//1. 改变价格曲线的frame,使得用于重绘的rect发生改变
chartView.frame = PortraitFrame;
//2. 强制重绘
[chartView setNeedsDisplay];
} else {
//1. 改变价格曲线的frame,使得用于重绘的rect发生改变
chartView.frame = LandscapeFrame;
//2. 强制重绘
[chartView setNeedsDisplay];
}
}

//设备旋转完
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
//设备旋转完的处理
}

willRotateToInterfaceOrientation:duration: 和 willAnimateRotationToInterfaceOrientation:duration: 都是在设备旋转前的回调函数,它们的区别在于调用的顺序。willRotateToInterfaceOrientation在旋转前调用,willAnimateRotationToInterfaceOrientation在旋转的animation block中调用。在willRotateToInterfaceOrientation的时候设备的interfaceOrientation变量还未改变,同样设备的原点和view的尺寸都未改变,因此在这个函数中,适合做一些变量的设置;在willAnimateRotationToInterfaceOrientation中,设备的interfaceOrientation变量已经被系统设置为toInterfaceOrientation,view的尺寸也发生相应更改,这个时候可以对需要旋转的视图进行重绘。

当然,我们可以在willRotateToInterfaceOrientation改变当前的view,而不依赖于系统重绘视图的过程。但是这样在旋转前,我们可以很明显看到视图发生改变然后再旋转,而非在旋转的过程中视图进行重绘。在实践过程中,我们发现前者的动画效果更加柔和,不会在视觉上产生明显的界面跳变和迟钝。

视图内容的改变

上图中的价格曲线图,在drawRect中实现了曲线和坐标轴等的绘制,因此我们也在drawRect中处理不同设备方向时候的图形绘制,如下:

- (void)drawRect:(CGRect)rect
{
if (_orientation == UIInterfaceOrientationPortrait) {
//绘制竖屏时候的价格曲线
} else {
//绘制横屏时候的价格曲线,改变字体大小,位置等
}
}

我们使用一个传递的参数来通知曲线图当前的设备方向,以针对不同的设备方向,绘制不同的视图。然而这个方法使得这个价格曲线的view需要增加一个记录设备方向的变量,降低了这个view的通用性。如上一节所说,我们在willAnimateRotationToInterfaceOrientation的函数中调用setNeedsDisplay方法强制重绘曲线图。因此,在这里,我们完全可以使用rect里的bounds来进行重绘。改变代码如下:

- (void)drawRect:(CGRect)rect
{
if (rect.size.width < rect.size.height) {
//绘制竖屏时候的价格曲线
} else {
//绘制横屏时候的价格曲线,改变字体大小,位置等
}
}

当然,在更多的情况下,我们并不一定都重载drawRect来绘制自定义的视图,更多的使用场景,是使用系统的UILabel等控件作为child view来构成自定义的视图。在这种使用场景下,我们可以重载 viewWillLayoutSubviews 函数,该函数在视图在前端渲染时被调用。在视图的bounds被改变时,会被再次调用,示例代码如下:

- (void)viewWillLayoutSubviews
{
if ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationPortrait) {
//绘制竖屏时候的价格曲线
} else {
//绘制横屏时候的价格曲线,改变字体大小,位置等
}
}

此处,我们使用UIApplication中的statusBarOrientation来判断当前设备的方向,以决定如何绘制subview。

小结:通过以上代码,我们可以很轻松的实现设备旋转前后,视图的切换,系统状态栏等的改变。切换的动画柔和平滑。值得注意的是,在视图的绘制过程中,我们只要考虑视图尺寸的变化,而无需考虑设备视窗原点等的变化。

手动旋转控制

注意到在价格曲线图中,有一个这样的按钮 btn-rotate2landscape@2x ,这个按钮既可以提醒用户旋转设备的功能,也可以在点击的时候实现人为的旋转过程,而无需依赖设备的旋转。下面我们就来讨论下如何实现这样的引导旋转功能,并且这个功能如何与现有的设备旋转功能相兼容。可以想到的方案有以下两种:

  1. 手动控制设备的方向,模拟设备旋转。无需考虑与原有的代码逻辑的兼容问题
  2. 手动旋转视图。需要考虑兼容问题,对于复杂的图像绘制,会影响旋转动画的平滑

首先考虑第一种成本代价最小的方案,经过一番google。iOS确实提供了一个在代码中控制设备的方法:

[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationLandscapeLeft];

但是该方法在iOS6中已经被depreciated。当然我们依然可以利用objc的动态特性去调用这个函数:

#import 

objc_msgSend([UIDevice currentDevice], @selector(setOrientation:), UIDeviceOrientationLandscapeLeft);

这里用到了objective-c的动态特性,objc_msgSend提供了类似java中反射的方法,这些runtime的特性虽然在开发中并不常用,但是作为objc的特性之一,在一些场合下也可以发挥很强大很灵活的作用,例如objc_setAssociatedObject和objc_getAssociatedObject这对函数,可以绑定两个实例。在响应UIButton的control事件中,我们通常会使用tag来传递参数,在这里用这对方法,我们可以给UIButton的实例绑定更复杂的类型来传递消息。关于objc的动态特性和使用场景以后再详述,不在本文里讨论。

使用上述方法,虽然可以满足需求,但是存在一定的风险,在需要发布到app store中的应用里使用被depreciated有可能会在审核的时候遭到reject。因此我们还是继续考虑第二种方案。考虑到我们在旋转前后需要显示不同的视图,我们没法通过简单的旋转动画来实现。而该视图需要根据我们设定的尺寸进行重绘以得到旋转后的视图,因此我们使用两个animation block来实现重绘和旋转的过程:

[UIView animateWithDuration:0.1 animations:^{
//改变价格曲线的frame,改为旋转后的尺寸,并强制重绘
self.chartView.frame = rotateFrame;
[self.chartView setNeedsDisplay];

} completion:^(BOOL finished) {
[UIView beginAnimations:@"" context:nil];
[UIView setAnimationDuration:0.5];
//旋转动画
self.chartView.transform = CGAffineTransformMakeRotation(M_PI/2);
[UIView commitAnimations];
}];

同样还要实现一个相反的过程来旋转回初始的状态。需要注意的是,在人为旋转的过程中,因为设备的原点未发生改变,因此我们需要在原坐标轴的情况下首先放大价格曲线图,并在新的放大的尺寸中重绘价格曲线图的坐标轴和曲线,完成之后才能进行整个视图的旋转。

人为的旋转实现完之后,我们还需要考虑与设备旋转相兼容的情况。例如在人为触发了旋转到横屏显示之后,如果旋转设备,触发设备旋转到横屏,会将已经横屏显示的价格曲线图再次旋转显示,造成显示错乱。在这里我们引入一个标志位 isRotateManually 来帮助controller判断旋转的触发来源。在实践中,我们简化了这个过程,在设备旋转被触发的情况下,屏蔽了人为的旋转视图;如果人为旋转视图被触发,则屏蔽设备旋转的回调,所有的旋转效果都由我们自己实现和逼近设备旋转的效果,只有当人为的旋转回垂直方向时,才复位设备旋转的回调,由设备的方向来控制视图的显示。在旋转效果逼近设备旋转效果的时候,这样实现更为简洁。

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
...

if (chartView.isRotateManually) {
//如果价格曲线由人为触发了旋转,则取消设备旋转的回调,并在接收到设备转回垂直方向时,同样触发手动旋转复位。
if (toInterfaceOrientation == UIInterfaceOrientationPortrait) {
[chartView rotateToPortraitWithAnimation:YES];
}
return NO;
}

...
}

总结

本文中我们实践了如何在旋转的过程中切换视图,以达到不同设备方向显示不同视图的交互过程。并且我们加入了一个旋转按钮来作为这个交互的引导和提示,同样实现了人为旋转的交互,并且使得设备旋转和人为的旋转相兼容。


你可能感兴趣的:(iOS屏幕旋转专题)