注:本节有点长,并且有些难度,希望大家有毅力看下去。
一个适配器允许接口不兼容的类在一起工作。它把它自己包裹成一个对象,公开一个与这个对象相互作用的标准接口。
如果你熟习 适配器模式 ,你会注意到苹果实施它的时候有一点不同的习惯─苹果使用协议 (protocols)。你可能熟习像 UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying 这样的协议。例子,NSCopying 的协议 (protocol),任何类都可以提供这样一个标准的复制方法。
我们提到的滚动区域是这样的:
现在开始,在项目导航的 View 文件夹上右击鼠标,选择 New File…,用 iOS/Cocoa Touch/Object-C class 模板创建一个新类。新类的名字叫 HorizontalScroller,选择它的子类为 UIView。
打开 HorizontalScroller.h 文件在 @end 后面插入如下代码:
@protocol HorizontalScrollerDelegate <NSObject> // methods declaration goes in here @end
这里定义一个 HorizontalScrollerDelegate 名字的协议,它继承于 NSObject 协议,同样的这是继承它父类的一个 Objective-C 类。符合 NSObject 协议,这是一个很好的做法─或者遵照 NSObject 协议。这能使你从定义的 NSObject 发送消息到 HorizontalScroller 的代理。你将会看到为什么这很重要。
定义个代理执行的方法,要在 @protocol 和 @end 之间,它们分为必要方法和可选方法。添加下面协议方法:
@required // 询问 delegate 在滚动区域里有多少个视图要被显示 - (NSInteger)numberOfViewsForHorizontalScroller: (HorizontalScroller*)scroller; // 返回索引是 index 的视图 - (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index; // 当索引是 index 的视图被点击了,通知 delegate - (void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index; @optional // 通知 delegate,显示初始化时索引是 Index 的视图。这个方法是可选的 // ask the delegate for the index of the initial view to display. this method is optional // 如果没有被 delegate 执行,默认值是 0 - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;
这里我们必选的和可选的方法我们都定义了。必选方法一定要被代理执行,它通常包含一些类必须要执行的数据。这里,必选方法是获取视图的数量,当前显示视图的索引和当视图被点击的时候执行的操作。可选方法这里是初始化视图;如果没有执行 HorizontalScroller 将会显示第一个索引的视图。
接下来,你需要在 HorizontalScroller 内部定义你的新代理。但是协议的定义在类的定义下面,因此在这点上它是不可见的。你该怎么办?
解决办法就是在前面声明协议以便于编译器(和Xcode)知道这个协议是可用的。好了,在 @interface 上面加入下面代码:
@protocol HorizontalScrollerDelegate;
还是 HorizontalScroller.h,在 @interface 和 @end 之间加入下面代码:
@property (weak) id<HorizontalScrollerDelegate> delegate; - (void)reload;
这个属性被定义成为一个 weak。这是为了防止循环 retain。如果一个类保持一个强指针(strong pointer)指向它的委托(delegate),同时委托也保持一个强指针指向这个类,在释放类所占用的内存时会造成 app 内存泄漏。
id 的意思是把这个代理指定给一个类,它遵照 HorizontalScrollerDelegate,给你一些类型安全。
reload 方法是模仿 UITableView 类的 relaodData;它重新加载所有数据用来创建一个水平移动视图。
用下面代码替换 HorizontalScroller.m 的内容:
#import “HorizontalScroller.m” #define VIEW_PADDING 10 #define VIEW_DIMENSIONS 100 #define VIEW_OFFSET 100 @interface HorizontalScroller () <UIScrollViewDelegate> @end @implementation HorizontalScroller { UIScrollView *scroller; } @end
来解释下每块代码:
接下来你需要执行初始化。添加下面的方法:
- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { scroller = [[UIScrollerView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; scroller.delegate = self; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarger:self action:@select(scrollerTapped:)]; [scroller addGestureRecognizer:tapRecognizer]; } return self; }
HorizontalScroller 将被滚动视图整个填充。如果一个专辑封面被点击,UITapGestureRecognizer 将会监听它上面的事件。如果有,它会通知 HorizontalScroller 的代理。
现在添加下面方法:
- (void)scrollerTapped:(UITapGestureRecognizer*)gesture { CGPoint location = [gesture locationInView:gesture.view]; // we can’t use an enumerator here, because we don’t want to enumerate over ALL of the UIScrollView subviews. // we want to enumerate only the subview that we added for (int index=0; index<[self.delegate numberOfViewForHorizontalScroller:self]; index++) { UIView *view = scroller.subviews[index]; if (CGRectContainsPoint(view.frame, location)) { [self.delegate horizontalScroller:self clickedViewAtIndex:index]; [scroller setContentOffset:CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0) animated:YES]; break; } } }
手势操作就如同传入的一个参数,可以从 locationInView: 获取定位信息。
接下来,调用委托的 numberOfViewForHorizontalScroller: 方法。它必须遵照 HorizontalScrollerDelegate 的协议安全发送消息,否则 HorizontalScroller 实例的代理是没法使用这些信息。
滚动视图里的每个视图,用 CGRectContainsPoint 执行一个点击测试,找到那个被点击的视图。当视图被找到,发送给委托一个消息 horizontalScroller:clickedViewAtIndex:。当你跳出这个循环后,设置被点击的视图滚动到视图中间。
现在添加下面的代码,用来刷新滚动视图(scroller):
- (void)reload { // 1 - nothing to load if there’s no delegate if (self.delegate == nil) return; // 2 - remover all subviews [scroller.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [obj removeFromSuperview]; } // 3 - xValue is the starting point of the views inside the scroller CGFloat xValue = VIEWS_OFFSET; for (int i=0; i<[self.delegate numberOfViewsForHorizontalScroller:self]; i++) { // 4 - add a view at the right position xValue += VIEW_PADDING; UIView *view = [self.delegate horizontalScroller:self viewAtIndex:i] view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSIONS, VIEW_DIMENSIONS); xValue += VIEW_DIMENSIONS + VIEW_PADDING; } // 5 [scroller setContentSize:CGSizeMake(xValue+VIEWS_OFFSET, self.frame.size.height)]; // 6 - if an initial view is defined, center the scroller on it if (self.delegate respondsToSelector:@select(initialViewIndexForHorizontalScroller:)]) { int initialView = [self.delegate initialViewIndexForHorizontalScroller:self]; [scroller setContentOffset:CGPointMake(initialView*(VIEW_DIMENSIONS+(2*VIEW_PADDING)), 0) animated:YES]; } }
能过代码一步步来讨论:
当数据发生改变的时候执行 reload 方法。当添加 HorizontalScroller 到别个一个视图时,你同样可以执行这个方法。在 HorizontalScroller.m 添加下面的代码替换后面的方案:
- (void)didMoveToSuperview { [self reload]; }
当它要添加一个子视图的时候,didMoveToSuperview 会发送消息给视图。这时正好可以更新滚动视图的内容。
HorizontalScroller 的最后一个难题就是,如何设置你看到的专辑总是在滚动视图的中间。为了这些,当用户通过他们的手指拖动滚动视图的时候你就需要做一些计算了。
添加下面方法(同样在 HorizontalScroller.m):
- (void)centerCurrentView { int xFinal = scroller.contentOffset.x + (VIEWS_OFFSET/2) + VIEW_PADDING; int viewIndex = xFinal / (VIEW_DIMENSIONS + (2*VIEW_PADDING)); xFinal = viewIndex * (VIEW_DIMENSIONS+(2*VIEW_PADDING)); [scroller setContentOffset:CGPointMake(xFinal, 0) animated:YES]; [self.delegate horizontalScroller:self clickedViewAtIndex:viewIndex]; }
上面的代码通过滚动视图的当前偏移量,外观尺寸,内边距来计算当前视图离中心的距离。最后一行非常重要:当一个视图居中后,你需要通知委托你选择的视图改变了。
为了侦测用户在滚动视图内完成拖拽的动作,你需要添加 UIScrollViewDelegate 方法:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (!decelerate) { [self centerCurrentView]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self centerCurrentView]; }
当用户完成拖拽的时候 scrollViewDidEndDragging:willDecelerate: 通知委托。如果滚动视图没有停止滚动, decelerate 参数会返回 true。当滚动结束,系统将会调用 scrollViewDidEndDecelerating。当用户拖动滚动当前视图后,两种情况,我们都需要调用一个新方法来使当前视图居中。
HorizontalScroller 现在可以使用了。浏览你刚刚写的代码;这里没有一处提到 Album 和 AlbumView 类。这非常棒,说明这个新的滚动视图是真正的完全独立的和可重用的。
Build 项目,确保所有的代码编译正确。
现在 HorizontalScroller 完成了,是时候在你的 APP 中使用了。打开 ViewController.m 添加如下引用:
#import “HorizontalScroller.h” #import “AlbumView.h”
给 ViewController 添加 HorizontalScrollerDelegate:
@interface ViewController () <UITableViewDataSource, UITableViewDelegate, HorizontalScroller>
在类的扩展里为水平滚动视图添加如下实例变量:
HorizontalScroller *scroller;
现在你可以执行代理方法了;你会惊奇的发现只需要几行代码你就能实现很多功能。
在 ViewController.m 添加如下代码:
#pragma mark - HorizontalScrollerDelegate methods - (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index { currentAlbumIndex = index; [self showDataForAlbumAtIndex:index]; }
这里设置一个变量用来存储当前的专辑,然后调用 showDataForAlbumAtIndex: 显示一个新专辑的数据。
提示:一般在方法代码的前面放置 #pragma mark 指示符。编译器会忽略这一行,当你在使用 Xcode 的跳转工具栏(Xcode’s jump bar)查看你的方法列表时,你会看到一个分隔符和个加粗的指示标题。在 Xcode 里,这可以帮助你很容易的组织代码。
下面,添加如下代码:
- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller *)scroller { return allAlbums.count; }
这里,协议方法返回滚动视图里的视图数量。因为滚动视图需要显示所有的专辑封面,这个 count 是所有专辑的数目。
现在,添加这些代码:
- (UIView *)horizontalScroller:(HorizontalScroller *)scroller viewAtIndex:(ini)index { Album *album = allAlbums[index]; return [[Album alloc] initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl]; }
这里你创建了一个新 AlbumView,然后交给 HorizontalScroller 使用。
就是这样,通过三个这么短的方法就可以显示一个漂亮的滚动视图。
实际上,你仍需要创建一个真正的滚动视图,然后添加到你的主视图上,但是在这之前,先添加下面的方法:
- (void)reloadScroller { allAlbums = [[LibraryAPI sharedInstance] getAlbums]; if (currentAlbumIndex < 0) currentAlbumIndex = 0; else if (currentAlbumIndex >=allAlbum.count) currentAlbumIndex = allAlbum.count - 1; [scroller reload]; [self showDataFroAlbumAtIndex:currentAlbumIndex; }
这个方法从 LibraryAPI 加载专辑数据,然后以当前视图的索引值为基础设置显示当前的图片。 如果当前视图的索引小于零,意味着当前没有选择视图,显示列表里的第一张专辑。否则显示最后一张专辑。
现在,在 viewDidLoad 里 [self showDataForAlbumIndex:0] 前面添加下面代码来初始化滚动视图:
scroller = [[HorizontalScroller alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)]; scroller.backgroundColor = [UIColor colorWithRed:0.24f greed:0.35f blue:0.49f alpha:1]; scroller.delegate = self; [self.view addSubview:scroller]; [self reloadScroller];
上面的代码创建了一个 HorizontalScroller 的实例,设置了它的背景颜色和委托,添加滚动视图到主视图上,在滚动视图的子视图上加载专辑数据。
提示:如果一个协议变得很大,里面有很多方法,你应该考虑把它们分散到几个小的协议里去。UITableViewDelegate 和 UITableViewDataSource 就是一个很好的例子,因为它们都是 UITablveView 的协议。设计协议的时候,最好一个名称引导一个功能。
构建和运行你的项目,你会看到一个新的很了不起的水平滚动视图:
啊嗯,等等。水平滚动的视图已经有了,可是专辑封面在哪里?
对了,你还没有代码来执行下载图片的功能。你需要添加一个下载图片的方法。查检 LibraryAPI 服务的所有接口,这里需要添加一个新的方法。不管怎样,现在还有几件事情需要考虑:
听起来这是一个难题?不用害怕,你将要学习如何使用观察者模式 (Observer pattern)。