iOS 7 開(kāi)發(fā)中彈簧式列表的制作
UIScrollView可以說(shuō)是UIKit中最重要的類(lèi)之一了,包括UITableView和UICollectionView等重要的數(shù)據(jù)容器類(lèi)都是UIScrollView的子類(lèi)。在歷年的WWDC上,UIScrollView和相關(guān)的API都有專(zhuān)門(mén)的主題進(jìn)行介紹,也可以看出這個(gè)類(lèi)的使用和變化之快。今年也不例外,因?yàn)閕OS7完全重新定義了UI,這使得UIScrollView里原來(lái)不太會(huì)使用的一些用法和實(shí)現(xiàn)的效果在新的系統(tǒng)中得到了很好的表現(xiàn)。另外,由于引入了UIKit Dynamics,我們還可以結(jié)合ScrollView做出一些以前不太可能或者需要花費(fèi)很大力氣來(lái)實(shí)現(xiàn)的效果,包括帶有重力的swipe或者是類(lèi)似新的信息app中的帶有彈簧效果聊天泡泡等。如果您還不太了解iOS7中信息app的效果,這里有一張gif圖可以幫您大概了解一下:
iOS7中信息app的彈簧效果
這次筆記的內(nèi)容主要就是實(shí)現(xiàn)一個(gè)這樣的效果。為了避免重復(fù)造輪子,我對(duì)這個(gè)效果進(jìn)行了一些簡(jiǎn)單的封裝,并連同這篇筆記的demo一起扔在了Github上,有需要的童鞋可以到這里自取。
iOS7的SDK中Apple最大的野心其實(shí)是想用SpriteKit來(lái)結(jié)束iOS平臺(tái)游戲開(kāi)發(fā)(至少是2D游戲開(kāi)發(fā))的亂戰(zhàn),統(tǒng)一游戲開(kāi)發(fā)的方式并建立 良性社區(qū)。而UIKit Dynamics,個(gè)人猜測(cè)Apple在花費(fèi)力氣為SpriteKit開(kāi)發(fā)了物理引擎的同時(shí),發(fā)現(xiàn)在UIKit中也可以使用,并能得到不錯(cuò)的效果,于是順便革新了一下設(shè)計(jì)理念,在UI設(shè)計(jì)中引入了不少物理的概念。在iOS系統(tǒng)中,最為典型的應(yīng)用是鎖屏界面打開(kāi)相機(jī)時(shí)中途放棄后的重力下墜+反彈的效果,另一 個(gè)就是信息應(yīng)用中的加入彈性的消息列表了。彈性列表在我自己上手試過(guò)以后覺(jué)得表現(xiàn)形式確實(shí)很生動(dòng),可以消除原來(lái)列表那種冷冰冰的感覺(jué),是有可能在今后的設(shè) 計(jì)中被大量使用的,因此決定學(xué)上一學(xué)。
首先我們需要知道要如何實(shí)現(xiàn)這樣一種效果,我們會(huì)用到哪些東西。毋庸置疑,如果不使用UIKit Dynamics的話,自己從頭開(kāi)始來(lái)完成會(huì)是一件非常費(fèi)力的事情,你可能需要實(shí)現(xiàn)一套位置計(jì)算和物理模擬來(lái)使效果看起來(lái)真實(shí)滑潤(rùn)。而UIKit Dynamics中已經(jīng)給我們提供了現(xiàn)成的彈簧效果,可以用UIAttachmentBehavior進(jìn)行實(shí)現(xiàn)。另外,在說(shuō)到彈性效果的時(shí)候,我們其實(shí)是 在描述一個(gè)列表中的各個(gè)cell之間的關(guān)系,對(duì)于傳統(tǒng)的UITableView來(lái)說(shuō),描述UITableViewCell之間的關(guān)系是比較復(fù)雜的(因?yàn)?Apple已經(jīng)把絕大多數(shù)工作做了,包括計(jì)算cell位置和位移等。使用越簡(jiǎn)單,定制就會(huì)越麻煩在絕大多數(shù)情況下都是真理)。而 UICollectionView則通過(guò)layout來(lái)完成cell之間位置關(guān)系的描述,給了開(kāi)發(fā)者較大的空間來(lái)實(shí)現(xiàn)布局。另外,UIKit Dynamics為UICollectionView做了很多方便的Catagory,可以很容易地“指導(dǎo)”UICollectionView利用加入物 理特性計(jì)算后的結(jié)果,在實(shí)現(xiàn)彈性效果的時(shí)候,UICollectionView是我們不二的選擇。
如果您在閱讀這篇筆記的時(shí)候遇到困難的話,建議您可以看看我之前的一些筆記,包括今年的UIKit Dynamics的介紹和去年的UICollectionView介紹。
話不多說(shuō),我們開(kāi)工。首先準(zhǔn)備一個(gè)UICollectionViewFlowLayout的子類(lèi)(在這里叫做 VVSpringCollectionViewFlowLayout),然后在ViewController中用這個(gè)layout實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 collectionView:
- //ViewController.m
- 02
- 03 @interface ViewController ()<UICollectionViewDataSource, UICollectionViewDelegate>
- 04 @property (nonatomic, strong) VVSpringCollectionViewFlowLayout *layout;
- 05 @end
- 06
- 07 static NSString *reuseId = @"collectionViewCellReuseId";
- 08
- 09 @implementation ViewController
- 10 - (void)viewDidLoad
- 11 {
- 12 [super viewDidLoad];
- 13 // Do any additional setup after loading the view, typically from a nib.
- 14
- 15 self.layout = [[VVSpringCollectionViewFlowLayout alloc] init];
- 16 self.layout.itemSize = CGSizeMake(self.view.frame.size.width, 44);
- 17 UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:self.layout];
- 18
- 19 collectionView.backgroundColor = [UIColor clearColor];
- 20
- 21 [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseId];
- 22
- 23 collectionView.dataSource = self;
- 24 [self.view insertSubview:collectionView atIndex:0];
- 25 }
- 26
- 27 #pragma mark - UICollectionViewDataSource
- 28 - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
- 29 {
- 30 return 50;
- 31 }
- 32
- 33 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
- 34 {
- 35 UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseId forIndexPath:indexPath];
- 36
- 37 //Just give a random color to the cell. See <a href="\"https://gist.github.com/kylefox/1689973\"" target="\"_blank\"">https://gist.github.com/kylefox/1689973</a>
- 38 cell.contentView.backgroundColor = [UIColor randomColor];
- 39 return cell;
- 40 }
- 41 @end
這部分沒(méi)什么可以多說(shuō)的,現(xiàn)在我們有一個(gè)標(biāo)準(zhǔn)的FlowLayout的UICollectionView了。通過(guò)使用 UICollectionViewFlowLayout的子類(lèi)來(lái)作為開(kāi)始的layout,我們可以節(jié)省下所有的初始cell位置計(jì)算的代碼,在上面代碼的情況下,這個(gè)collectionView的表現(xiàn)和一個(gè)普通的tableView并沒(méi)有太大不同。接下來(lái)我們著重來(lái)看看要如何實(shí)現(xiàn)彈性的layout。對(duì)于彈性效果,我們需要的是連接一個(gè)item和一個(gè)錨點(diǎn)間彈性連接的UIAttachmentBehavior,并能在滾動(dòng)時(shí)設(shè)置新的錨點(diǎn)位置。我們?cè)趕croll的時(shí)候,只要使用UIKit Dynamics的計(jì)算結(jié)果,替代掉原來(lái)的位置更新計(jì)算(其實(shí)就是簡(jiǎn)單的scrollView的contentOffset的改變),就可以模擬出彈性的效果了。
首先在-prepareLayout中為cell添加UIAttachmentBehavior。
- //VVSpringCollectionViewFlowLayout.m
- 02 @interface VVSpringCollectionViewFlowLayout()
- 03 @property (nonatomic, strong) UIDynamicAnimator *animator;
- 04 @end
- 05
- 06 @implementation VVSpringCollectionViewFlowLayout
- 07 //...
- 08
- 09 -(void)prepareLayout {
- 10 [super prepareLayout];
- 11
- 12 if (!_animator) {
- 13 _animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
- 14 CGSize contentSize = [self collectionViewContentSize];
- 15 NSArray *items = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, contentSize.width, contentSize.height)];
- 16
- 17 for (UICollectionViewLayoutAttributes *item in items) {
- 18 UIAttachmentBehavior *spring = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
- 19
- 20 spring.length = 0;
- 21 spring.damping = 0.5;
- 22 spring.frequency = 0.8;
- 23
- 24 [_animator addBehavior:spring];
- 25 }
- 26 }
- 27 }
- 28 @end
prepareLayout將在CollectionView進(jìn)行排版的時(shí)候被調(diào)用。首先當(dāng)然是call一下super的prepareLayout,你 肯定不會(huì)想要全都要自己進(jìn)行設(shè)置的。接下來(lái),如果是第一次調(diào)用這個(gè)方法的話,先初始化一個(gè)UIDynamicAnimator實(shí)例,來(lái)負(fù)責(zé)之后的動(dòng)畫(huà)效 果。iOS7 SDK中,UIDynamicAnimator類(lèi)專(zhuān)門(mén)有一個(gè)針對(duì)UICollectionView的Category,以使 UICollectionView能夠輕易地利用UIKit Dynamics的結(jié)果。在UIDynamicAnimator.h中能夠找到這個(gè)Category:
- @interface UIDynamicAnimator (UICollectionViewAdditions)
- 02
- 03 // When you initialize a dynamic animator with this method, you should only associate collection view layout attributes with your behaviors.
- 04 // The animator will employ thecollection view layout’s content size coordinate system.
- 05 - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout*)layout;
- 06
- 07 // The three convenience methods returning layout attributes (if associated to behaviors in the animator) if the animator was configured with collection view layout
- 08 - (UICollectionViewLayoutAttributes*)layoutAttributesForCellAtIndexPath:(NSIndexPath*)indexPath;
- 09 - (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
- 10 - (UICollectionViewLayoutAttributes*)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKind atIndexPath:(NSIndexPath *)indexPath;
- 11
- 12 @end
于是通過(guò)-initWithCollectionViewLayout:進(jìn)行初始化后,這個(gè)UIDynamicAnimator實(shí)例便和我們的 layout進(jìn)行了綁定,之后這個(gè)layout對(duì)應(yīng)的attributes都應(yīng)該由綁定的UIDynamicAnimator的實(shí)例給出。就像下面這樣:
- //VVSpringCollectionViewFlowLayout.m
- 02 @implementation VVSpringCollectionViewFlowLayout
- 03
- 04 //...
- 05
- 06 -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
- 07 return [_animator itemsInRect:rect];
- 08 }
- 09
- 10 -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
- 11 return [_animator layoutAttributesForCellAtIndexPath:indexPath];
- 12 }
- 13 @end
向上拖動(dòng)時(shí)的錨點(diǎn)變化示意
現(xiàn)在我們來(lái)實(shí)現(xiàn)這個(gè)錨點(diǎn)的變化。既然都是滑動(dòng),我們是不是可以考慮在UIScrollView的–scrollViewDidScroll:委托方法中來(lái) 設(shè)定新的Behavior錨點(diǎn)值呢?理論上來(lái)說(shuō)當(dāng)然是可以的,但是如果這樣的話我們大概就不得不面臨著將剛才的layout實(shí)例設(shè)置為 collectionView的delegate這樣一個(gè)事實(shí)。但是我們都知道layout應(yīng)該做的事情是給collectionView提供必要的布局 信息,而不應(yīng)該負(fù)責(zé)去處理它的委托事件。處理collectionView的回調(diào)更恰當(dāng)?shù)貞?yīng)該由處于collectionView的controller 層級(jí)的類(lèi)來(lái)完成,而不應(yīng)該由一個(gè)給collectionView提供數(shù)據(jù)和信息的類(lèi)來(lái)響應(yīng)。在UICollectionViewLayout中,我們有一個(gè)叫做-shouldInvalidateLayoutForBoundsChange:的方法,每次layout的bounds發(fā)生變化的時(shí) 候,collectionView都會(huì)詢問(wèn)這個(gè)方法是否需要為這個(gè)新的邊界和更新layout。一般情況下只要layout沒(méi)有根據(jù)邊界不同而發(fā)生變化的 話,這個(gè)方法直接不做處理地返回NO,表示保持現(xiàn)在的layout即可,而每次bounds改變時(shí)這個(gè)方法都會(huì)被調(diào)用的特點(diǎn)正好可以滿足我們更新錨點(diǎn)的需 求,因此我們可以在這里面完成錨點(diǎn)的更新。
- //VVSpringCollectionViewFlowLayout.m
- @implementation VVSpringCollectionViewFlowLayout
- //...
- -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
- UIScrollView *scrollView = self.collectionView;
- CGFloat scrollDelta = newBounds.origin.y - scrollView.bounds.origin.y;
- //Get the touch point
- CGPoint touchLocation = [scrollView.panGestureRecognizer locationInView:scrollView];
- for (UIAttachmentBehavior *spring in _animator.behaviors) {
- CGPoint anchorPoint = spring.anchorPoint;
- CGFloat distanceFromTouch = fabsf(touchLocation.y - anchorPoint.y);
- CGFloat scrollResistance = distanceFromTouch / 500;
- UICollectionViewLayoutAttributes *item = [spring.items firstObject];
- CGPoint center = item.center;
- //In case the added value bigger than the scrollDelta, which leads an unreasonable effect
- center.y += (scrollDelta > 0) ? MIN(scrollDelta, scrollDelta * scrollResistance)
- : MAX(scrollDelta, scrollDelta * scrollResistance);
- item.center = center;
- [_animator updateItemUsingCurrentState:item];
- }
- return NO;
- }
- @end
首先我們計(jì)算了這次scroll的距離scrollDelta,為了得到每個(gè)item與觸摸點(diǎn)的之間的距離,我們當(dāng)然還需要知道觸摸點(diǎn)的坐標(biāo) touchLocation。接下來(lái),可以根據(jù)距離對(duì)每個(gè)錨點(diǎn)進(jìn)行設(shè)置了:簡(jiǎn)單地計(jì)算了原來(lái)錨點(diǎn)與觸摸點(diǎn)之間的距離distanceFromTouch, 并由此計(jì)算一個(gè)系數(shù)。接下來(lái),對(duì)于當(dāng)前的item,我們獲取其當(dāng)前錨點(diǎn)位置,然后將其根據(jù)scrollDelta的數(shù)值和剛才計(jì)算的系數(shù),重新設(shè)定錨點(diǎn)的 位置。最后我們需要告訴UIDynamicAnimator我們已經(jīng)完成了對(duì)冒點(diǎn)的更新,現(xiàn)在可以開(kāi)始更新物理計(jì)算,并隨時(shí)準(zhǔn)備 collectionView來(lái)取LayoutAttributes的數(shù)據(jù)了。
也許你還沒(méi)有緩過(guò)神來(lái)?但是我們確實(shí)已經(jīng)做完了,讓我們來(lái)看看實(shí)際的效果吧:
帶有彈性效果的collecitonView
當(dāng)然,通過(guò)調(diào)節(jié)damping,frequency和scrollResistance的系數(shù)等參數(shù),可以得到彈性不同的效果,比如更多的震蕩或者更大的幅度等等。
源碼下載地址:http://down.51cto.com/data/1120468
原文鏈接:http://www.devdiv.com/iOS_iPhone-ios_-thread-208170-1-1.html
【移動(dòng)開(kāi)發(fā)視頻課程推薦】
- iOS培訓(xùn)之Objective-C基礎(chǔ)視頻教程(40集)
- Cocos2d-x從零開(kāi)始【5天掌握跨平臺(tái)游戲開(kāi)發(fā)利器】(12集)
- Objective C編程基礎(chǔ)(24集)
- Android技術(shù)輕松入門(mén)課程(12集)
- 微信開(kāi)放平臺(tái)-Android應(yīng)用接入(4集)
- Cocos2d-x跨平臺(tái)游戲開(kāi)發(fā)入門(mén)基礎(chǔ)(29集)
- iOS開(kāi)發(fā)視頻教程-iOS網(wǎng)絡(luò)編程【高級(jí)篇】(39集)
- 移動(dòng)應(yīng)用用戶體驗(yàn)設(shè)計(jì)高級(jí)課程(60集)
- 從零學(xué)習(xí)iOS開(kāi)發(fā)–UI多視圖(30集)
- iOS開(kāi)發(fā)視頻教程【基礎(chǔ)入門(mén)篇】