UIScrollView 实践经验.docx
- 文档编号:13332430
- 上传时间:2023-06-13
- 格式:DOCX
- 页数:6
- 大小:21.25KB
UIScrollView 实践经验.docx
《UIScrollView 实践经验.docx》由会员分享,可在线阅读,更多相关《UIScrollView 实践经验.docx(6页珍藏版)》请在冰点文库上搜索。
UIScrollView实践经验
UIScrollView(包括它的子类UITableView和UICollectionView)是iOS开发中最常用也是最有意思的UI组件,大部分App的核心界面都是基于三者之一或三者的组合实现。
UIScrollView是UIKit中为数不多能响应滑动手势的view,相比自己用UIPanGestureRecognizer实现一些基于滑动手势的效果,用UIScrollView的优势在于bounce和decelerate等特性可以让App的用户体验与iOS系统的用户体验保持一致。
本文通过一些实例讲解UIScrollView的特性和实际使用中的经验。
UIScrollView和AutoLayoutiPhone5刚出来的时候,大部分不支持横屏的App都不需要做太多的适配工作,因为屏幕宽度没有变,tableview多个cell也不需要加code。
但是在iPhone6和iPhone6Plus发布以后,多分辨率适配终于不再是Android开发的专利了。
于是,从iOS6起就存在的AutoLayout终于有了用武之地。
关于AutoLayout的基本用法不再赘述,可以参考RayWenderlich上的教程(Part2)。
但UIScrollView在AutoLayout是一个很特殊的view,对于UIScrollView的subview来说,它的leading/trailing/top/bottomspace是相对于UIScrollView的contentSize而不是bounds来确定的,所以当你尝试用UIScrollView和它subview的leading/trailing/top/bottom来互相决定大小的时候,就会出现「Hasambiguousscrollablecontentwidth/height」的warning。
正确的姿势是用UIScrollView外部的view或UIScrollView本身的width/height确定subview的尺寸,进而确定contentSize。
因为UIScrollView本身的leading/trailing/top/bottom变得不好用,所以我习惯的做法是在UIScrollView和它原来的subviews之间增加一个contentview,这样做的好处有:
不会在storyboard里留下error/warning为subview提供leading/trailing/top/bottom,方便subview的布局通过调整contentview的size(可以是constraint的IBOutlet)来调整contentSize不需要hardcode与屏幕尺寸相关的代码更好地支持rotationSample中的AutoLayout演示了UIScrollView+AutoLayout的例子。
UIScrollViewDelegateUIScrollViewDelegate是UIScrollView的delegateprotocol,UIScrollView有意思的功能都是通过它的delegate方法实现的。
了解这些方法被触发的条件及调用的顺序对于使用UIScrollView是很有必要的,本文主要讲拖动相关的效果,所以zoom相关的方法跳过不提,拖动相关的delegate方法按调用顺序分别是:
-(void)scrollViewDidScroll:
(UIScrollView*)scrollView这个方法在任何方式触发contentOffset变化的时候都会被调用(包括用户拖动,减速过程,直接通过代码设置等),可以用于监控contentOffset的变化,并根据当前的contentOffset对其他view做出随动调整。
-(void)scrollViewWillBeginDragging:
(UIScrollView*)scrollView用户开始拖动scrollview的时候被调用。
-(void)scrollViewWillEndDragging:
(UIScrollView*)scrollViewwithVelocity:
(CGPoint)velocitytargetContentOffset:
(inoutCGPoint*)targetContentOffset该方法从iOS5引入,在didEndDragging前被调用,当willEndDragging方法中velocity为CGPointZero(结束拖动时两个方向都没有速度)时,didEndDragging中的decelerate为NO,即没有减速过程,willBeginDecelerating和didEndDecelerating也就不会被调用。
反之,当velocity不为CGPointZero时,scrollview会以velocity为初速度,减速直到targetContentOffset。
值得注意的是,这里的targetContentOffset是个指针,没错,你可以改变减速运动的目的地,这在一些效果的实现时十分有用,实例章节中会具体提到它的用法,并和其他实现方式作比较。
-(void)scrollViewDidEndDragging:
(UIScrollView*)scrollViewwillDecelerate:
(BOOL)decelerate在用户结束拖动后被调用,decelerate为YES时,结束拖动后会有减速过程。
注,在didEndDragging之后,如果有减速过程,scrollview的dragging并不会立即置为NO,而是要等到减速结束之后,所以这个dragging属性的实际语义更接近scrolling。
-(void)scrollViewWillBeginDecelerating:
(UIScrollView*)scrollView减速动画开始前被调用。
-(void)scrollViewDidEndDecelerating:
(UIScrollView*)scrollView减速动画结束时被调用,这里有一种特殊情况:
当一次减速动画尚未结束的时候再次dragscrollview,didEndDecelerating不会被调用,并且这时scrollview的dragging和decelerating属性都是YES。
新的dragging如果有加速度,那么willBeginDecelerating会再一次被调用,然后才是didEndDecelerating;如果没有加速度,虽然willBeginDecelerating不会被调用,但前一次留下的didEndDecelerating会被调用,所以连续快速滚动一个scrollview时,delegate方法被调用的顺序(不含didScroll)可能是这样的:
scrollViewWillBeginDragging:
scrollViewWillEndDragging:
withVelocity:
targetContentOffset:
scrollViewDidEndDragging:
willDecelerate:
scrollViewWillBeginDecelerating:
scrollViewWillBeginDragging:
scrollViewWillEndDragging:
withVelocity:
targetContentOffset:
scrollViewDidEndDragging:
willDecelerate:
scrollViewWillBeginDecelerating:
...scrollViewWillBeginDragging:
scrollViewWillEndDragging:
withVelocity:
targetContentOffset:
scrollViewDidEndDragging:
willDecelerate:
scrollViewWillBeginDecelerating:
scrollViewDidEndDecelerating:
虽然很少有因为这个导致的bug,但是你需要知道这种很常见的用户操作会导致的中间状态。
例如你尝试在UITableViewDataSource的tableView:
cellForRowAtIndexPath:
方法中基于tableView的dragging和decelerating属性判断是在用户拖拽还是减速过程中的话可能会误判(见例1)。
Sample中的Delegate简单输出了一些Log,你可以快速了解这些方法的调用顺序。
实例下面通过一些实例,更详细地演示和描述以上各delegate方法的用途。
1.TableView中图片加载逻辑的优化虽然这种优化方式在现在的机能和网络环境下可能看似不那么必要,但在我最初看到这个方法是的09年(印象中是Tweetie作者在08年写的Blog,可能有误),遥想iPhone3G/3GS的机能,这个方法为多图的tableview的性能带来很大的提升,也成了我的秘密武器。
而现在,在移动网络环境下,你依然值得这么做来为用户节省流量。
先说一下原文的思路:
当用户手动dragtableview的时候,会加载cell中的图片;在用户快速滑动的减速过程中,不加载过程中cell中的图片(但文字信息还是会被加载,只是减少减速过程中的网络开销和图片加载的开销);在减速结束后,加载所有可见cell的图片(如果需要的话);问题1:
前面提到,刚开始拖动的时候,dragging为YES,decelerating为NO;decelerate过程中,dragging和decelerating都为YES;decelerate未结束时开始下一次拖动,dragging和decelerating依然都为YES。
所以无法简单通过tableview的dragging和decelerating判断是在用户拖动还是减速过程。
解决这个问题很简单,添加一个变量如userDragging,在willBeginDragging中设为YES,didEndDragging中设为NO。
那么tableView:
cellForRowAtIndexPath:
方法中,是否load图片的逻辑就是:
if(!
self.userDragging&&tableView.decelerating){cell.imageView.image=nil;}else{//codeforloadingimagefromnetworkordisk}问题2:
这么做的话,decelerate结束后,屏幕上的cell都是不带图片的,解决这个问题也不难,你需要一个形如loadImageForVisibleCells的方法,加载可见cell的图片:
-(void)loadImageForVisibleCells{NSArray*cells=[self.tableViewvisibleCells];for(GLImageCell*cellincells){NSIndexPath*indexPath=[self.tableViewindexPathForCell:
cell];[selfsetupCell:
cellwithIndexPath:
indexPath];}}问题3:
这个问题可能不容易被发现,在减速过程中如果用户开始新的拖动,当前屏幕的cell并不会被加载(前文提到的调用顺序问题导致),而且问题1的方案并不能解决问题3,因为这些cell已经在屏上,不会再次经过cellForRowAtIndexPath方法。
虽然不容易发现,但解决很简单,只需要在scrollViewWillBeginDragging:
方法里也调用一次loadImageForVisibleCells即可。
再优化上述方法在那个年代的确提升了tableview的performance,但是你会发现在减速过程最后最慢的那零点几秒时间,其实还是会让人等得有些心急,尤其如果你的App只有图片没有文字。
在iOS5引入了scrollViewWillEndDragging:
withVelocity:
targetContentOffset:
方法后,配合SDWebImage,我尝试再优化了一下这个方法以提升用户体验:
如果内存中有图片的缓存,减速过程中也会加载该图片如果图片属于targetContentOffset能看到的cell,正常加载,这样一来,快速滚动的最后一屏出来的的过程中,用户就能看到目标区域的图片逐渐加载你可以尝试用类似fadein或者flip的效果缓解生硬的突然出现(尤其是像本例这样只有图片的App)核心代码:
-(void)scrollViewWillBeginDragging:
(UIScrollView*)scrollView{self.targetRect=nil;[selfloadImageForVisibleCells];}-(void)scrollViewWillEndDragging:
(UIScrollView*)scrollViewwithVelocity:
(CGPoint)velocitytargetContentOffset:
(inoutCGPoint*)targetContentOffset{CGRecttargetRect=CGRectMake(targetContentOffset->x,targetContentOffset->y,scrollView.frame.size.width,scrollView.frame.size.height);self.targetRect=[NSValuevalueWithCGRect:
targetRect];}-(void)scrollViewDidEndDecelerating:
(UIScrollView*)scrollView{self.targetRect=nil;[selfloadImageForVisibleCells];}是否需要加载图片的逻辑:
BOOLshouldLoadImage=YES;if(self.targetRect&&!
CGRectIntersectsRect([self.targetRectCGRectValue],cellFrame)){SDImageCache*cache=[managerimageCache];NSString*key=[managercacheKeyForURL:
targetURL];if(!
[cacheimageFromMemoryCacheForKey:
key]){shouldLoadImage=NO;}}if(shouldLoadImage){//loadimage}更值得高兴的是,通过判断是否nil,targetRect同时起到了原来userDragging的作用。
本例完整的代码见Sample中的LazyLoad2.分页的几种实现方式利用UIScrollView有多种方法实现分页,但是各自的效果和用途不尽相同,其中方法2和方法3的区别也正是一些同类App在模仿Glow的首页Bubble翻转效果时跟Glow体验上的的差距所在(但愿他们不会看到本文并且调整他们的实现方式)。
本例通过三种方法实现相似的一个场景,你可以通过安装到手机上来感受三种实现方式的不同用户体验。
为了区分每个例子的重点,本例没有重用机制,重用相关内容见例3。
2.1pagingEnabled这是系统提供的分页方式,最简单,但是有一些局限性:
只能以framesize为单位翻页,减速动画阻尼大,减速过程不超过一页需要一些hacking实现bleeding和padding(即页与页之间有padding,在当前页可以看到前后页的部分内容)Sample中Pagination有简单实现bleeding和padding效果的代码,主要的思路是:
让scrollview的宽度为page宽度+padding,并且设置clipsToBounds为NO这样虽然能看到前后页的内容,但是无法响应touch,所以需要另一个覆盖期望的可触摸区域的view来实现类似touchbridging的功能适用场景:
上述局限性同时也是这种实现方式的优点,比如一般App的引导页(教程),Calendar里的月视图,都可以用这种方法实现。
2.2Snap这种方法就是在didEndDragging且无减速动画,或在减速动画完成时,snap到一个整数页。
核心算法是通过当前contentOffset计算最近的整数页及其对应的contentOffset,通过动画snap到该页。
这个方法实现的效果都有个通病,就是最后的snap会在decelerate结束以后才发生,总感觉很突兀。
2.3修改targetContentOffset通过修改scrollViewWillEndDragging:
withVelocity:
targetContentOffset:
方法中的targetContentOffset直接修改目标offset为整数页位置。
其中核心代码:
-(CGPoint)nearestTargetOffsetForOffset:
(CGPoint)offset{CGFloatpageSize=BUBBLE_DIAMETER+BUBBLE_PADDING;NSIntegerpage=roundf(offset.x/pageSize);CGFloattargetX=pageSize*page;returnCGPointMake(targetX,offset.y);}-(void)scrollViewWillEndDragging:
(UIScrollView*)scrollViewwithVelocity:
(CGPoint)velocitytargetContentOffset:
(inoutCGPoint*)targetContentOffset{CGPointtargetOffset=[selfnearestTargetOffsetForOffset:
*targetContentOffset];targetContentOffset->x=targetOffset.x;targetContentOffset->y=targetOffset.y;}适用场景:
方法2和方法3的原理近似,效果也相近,适用场景也基本相同,但方法3的体验会好很多,snap到整数页的过程很自然,或者说用户完全感知不到snap过程的存在。
这两种方法的减速过程流畅,适用于一屏有多页,但需要按整数页滑动的场景;也适用于如图表中自动snap到整数天的场景;还适用于每页大小不同的情况下snap到整数页的场景(不做举例,自行发挥,其实只需要修改计算目标offset的方法)。
完整代码参见Pagination3.重用大部分的iOS开发应该都清楚UITableView的cell重用机制,这种重用机制减少了内存开销也提高了performance,UIScrollView作为UITableView的父类,在很多场景中也很适合应用重用机制(其实不只是UIScrollView,任何场景中会反复出现的元素都应该适当地引入重用机制)。
你可以参照UITableView的cell重用机制,总结重用机制如下:
维护一个重用队列当元素离开可见范围时,removeFromSuperview并加入重用队列(enqueue)当需要加入新的元素时,先尝试从重用队列获取可重用元素(dequeue)并且从重用队列移除如果队列为空,新建元素这些一般都在scrollViewDidScroll:
方法中完成实际使用中,需要注意的点是:
当重用对象为viewcontroller时,记得addChildeViewController当view或viewcontroller被重用但其对应model发生变化的时候,需要及时清理重用前留下的内容数据可以适当做缓存,在重用的时候尝试从缓存中读取数据甚至之前的状态(如tableview的contentOffset),以得到更好的用户体验当onscreen的元素数量可确定的时候,有时候可以提前init这些元素,不会在scroll过程中遇到因为init开销带来的卡顿(尤其是以viewcontroller为重用对象的时候)例2中的场景很适合以view为重用单位,本例新增一个以viewcontroller为重用对象的例子,该例子同时演示了联动效果,具体见下个例子。
完整代码参见Reuse4.联动/视差滚动上一个例子里mainscrollview和titleview里的scrollview就是一个联动的例子,所谓联动,就是当A滚动的时候,在scrollViewDidScroll:
里根据A的contentOffset动态计算B的contentOffset并设给B。
同样对于非scrollview的C,也可以动态计算C的frame或是transform(Glow的气泡为例)实现视差滚动或者其他高级动画,这在现在许多应用的引导页面里会被用到。
联动/视差滚动部分原理上其实比较简单,不再赘述,写了个简单的例子Parallax。
写在最后不知不觉就写了很多关于UIScrollView的内容,其实还有很多可写,由于时间关系只好停笔。
在我看来,UIScrollView就好像提供了一个跳脱二维空间束缚的途径,如果你有足够的想象力,它能帮你实现更丰富的跳出平面束缚的用户体验。
本来还准备写一个综合性的例子,但是由于时间关系还没完成,后面有时间会继续更新。
此外,例子中可能会有错误或可以改进的地方,欢迎在GitHub直接提Issue或PR。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- UIScrollView 实践经验