iOS中实现下拉刷新,这是我在github上看到的一个比较优秀的源码,现在解析下该功能的实现流程。
示例图,这次录制的视频转不成gif - 。 - 我先加张图,下次加上
工程流程示意图
UI层次设计解剖
现在我说下 实现的思路
/** 设置控制视图的基本属性,并赋予空间
* horizontalRandomness : 动画手势拖拽释放后,每个线条移动的加速度
internalAnimationFactor : 内部动画实现的时长
reverseLoadingAnimation : 动画正向执行还是逆向执行
*/
self.storeHouseRefreshControl = [CBStoreHouseRefreshControl attachToScrollView:self.tableView target:self refreshAction:@selector(refreshTriggered:) plist:@"storehouse" color:[UIColor whiteColor] lineWidth:1.5 dropHeight:80 scale:1.0 horizontalRandomness:150 reverseLoadingAnimation:YES internalAnimationFactor:0.5];
这是对应的类方法中创建BarItem
视图的方法,为了更好的实现动画效果BarItem通过initWithFrame:
创建了空间以及frame,通过setHorizontalRandomness:
去打乱位置,制造形变(x,y上)。为的是刚进入界面时候的动画子视图不可见,遮挡在我们的navigation bar 下面(可以看UI 上面截的UI图层解剖)。setupWithFrame:
去修改子视图的锚点,将动画中心安置在画出的线条中点上,修改锚点后,子视图的frame要重新设置,通过前后锚点的偏移,将增减的frame(x,y)重新设置。
NSMutableArray *mutableBarItems = [[NSMutableArray alloc] init];
for (int i=0; i<startPoints.count; i++) {
CGPoint startPoint = CGPointFromString(startPoints[i]);
CGPoint endPoint = CGPointFromString(endPoints[i]);
BarItem *barItem = [[BarItem alloc] initWithFrame:refreshControl.frame startPoint:startPoint endPoint:endPoint color:color lineWidth:lineWidth];
barItem.tag = i;
barItem.backgroundColor=[UIColor clearColor];
barItem.alpha = 0;
[mutableBarItems addObject:barItem];
[refreshControl addSubview:barItem];
[barItem setHorizontalRandomness:refreshControl.horizontalRandomness dropHeight:refreshControl.dropHeight];
}
refreshControl.barItems = [NSArray arrayWithArray:mutableBarItems];
refreshControl.frame = CGRectMake(0, 0, width, height);
refreshControl.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2, 0);
for (BarItem *barItem in refreshControl.barItems) {
[barItem setupWithFrame:refreshControl.frame];
}
实现UIScrollview中的偏移量监听以及手势释放
的方法。将方法在下拉动画父视图CBStoreHouseRefreshControl
中去实现
#pragma mark - Notifying refresh control of scrolling
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self.storeHouseRefreshControl scrollViewDidScroll];
}
/** 拖拽松手后执行 */
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
[self.storeHouseRefreshControl scrollViewDidEndDragging];
}
视图在手势作用下,偏移量发生改变执行的动画效果animationProgress
是获取当前偏移量所处的最大下滑度的比例,从而 根据它去设置当前动画的进度。在这里特别说明下形变中的旋转是有累加量的,所以每次旋转要取递增量。而且当前视图的线条是画出来的,角度只要∏就够了。progress==0的处理是将动画组件视图的基本属性进行设置:例如frame 为的是解决第一次展示的时候动画错位
,UITableview 在第一次显示的时候会默认调用滚动监听,最后滚动回去,呈现的时候偏移量就变成0了。由于这个原因导致在类方法创建CBStoreHouseRefreshControl
时对BarItem
的frame隐藏操作失效。这里就是处理这一问题。
- (void)scrollViewDidScroll
{
if (self.originalTopContentInset == 0) self.originalTopContentInset = self.scrollView.contentInset.top;
self.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2, self.realContentOffsetY*krelativeHeightFactor);
if (self.state == CBStoreHouseRefreshControlStateIdle)
[self updateBarItemsWithProgress:self.animationProgress];
}
- (CGFloat)animationProgress
{
return MIN(1.f, MAX(0, fabsf(self.realContentOffsetY)/self.dropHeight));
}
- (void)updateBarItemsWithProgress:(CGFloat)progress
{
for (BarItem *barItem in self.barItems) {
NSInteger index = [self.barItems indexOfObject:barItem];
CGFloat startPadding = (1 - self.internalAnimationFactor) / self.barItems.count * index;
CGFloat endPadding = 1 - self.internalAnimationFactor - startPadding;
if (progress == 1 || progress >= 1 - endPadding) {
barItem.transform = CGAffineTransformIdentity;
barItem.alpha = kbarDarkAlpha;
}
else if (progress == 0) {
[barItem setHorizontalRandomness:self.horizontalRandomness dropHeight:self.dropHeight];
}
else {
CGFloat realProgress;
if (progress <= startPadding)
realProgress = 0;
else
realProgress = MIN(1, (progress - startPadding)/self.internalAnimationFactor);
barItem.transform = CGAffineTransformMakeTranslation(barItem.translationX*(1-realProgress), -self.dropHeight*(1-realProgress));
barItem.transform = CGAffineTransformRotate(barItem.transform, M_PI*(realProgress));
barItem.transform = CGAffineTransformScale(barItem.transform, realProgress, realProgress);
barItem.alpha = realProgress * kbarDarkAlpha;
}
}
}
在手势拖拽消失后触发动画,而在refreshTriggered:
中声明3秒动画执行后,将固定的下拉视图复位,并执行下拉刷新的逆动画。
- (void)refreshTriggered:(id)sender
{
/** afterDelay 设置刷新的时间 */
//通过self 的方法选择器 ,我们可以选择模式 NSRunLoopCommonModes ,这个模式不论在手势是否触碰滑动 都能执行在子线程执行动画,涉及到了runLoop。
/**
* NSDefaultRunLoopMode 默认模式 在手势触碰的时候 ,子线程动画暂停
* UITrackingRunLoopMode 在手势触碰时候,子线程动画执行
*/
[self performSelector:@selector(finishRefreshControl) withObject:nil afterDelay:3 inModes:@[NSRunLoopCommonModes]];
}
- (void)finishRefreshControl
{
[self.storeHouseRefreshControl finishingLoading];
}
注意设置scrollView.contentInset
后 scrollView.contentOffset
也要重新设置。
- (void)scrollViewDidEndDragging
{
if (self.state == CBStoreHouseRefreshControlStateIdle && self.realContentOffsetY < -self.dropHeight) {
if (self.animationProgress == 1) self.state = CBStoreHouseRefreshControlStateRefreshing;
if (self.state == CBStoreHouseRefreshControlStateRefreshing) {
UIEdgeInsets newInsets = self.scrollView.contentInset;
newInsets.top = self.originalTopContentInset + self.dropHeight;
CGPoint contentOffset = self.scrollView.contentOffset;
[UIView animateWithDuration:0 animations:^(void) {
self.scrollView.contentInset = newInsets;
self.scrollView.contentOffset = contentOffset;
}];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self.target respondsToSelector:self.action])
[self.target performSelector:self.action withObject:self];
#pragma clang diagnostic pop
[self startLoadingAnimation];
}
}
}
拖拽滚动视图结束后,动画的持续时间是3秒,如果是网络请求层
,我们可以将 回复 动画加在JSON字符串正确返回的blcok返回块
中
CBStoreHouseRefreshControl类 代码
根据realContentOffsetY
重写get方法
获取当前的ScrollView偏移量+self.scrollView.contentInset.top(不操作,默认是0)
//根据子视图的的位置。获取动画显示父视图的最小满足区域
CGPoint startPoint = CGPointFromString(startPoints[i]);
CGPoint endPoint = CGPointFromString(endPoints[i]);
if (startPoint.x > width) width = startPoint.x;
if (endPoint.x > width) width = endPoint.x;
if (startPoint.y > height) height = startPoint.y;
if (endPoint.y > height) height = endPoint.y;
设置手势拖拽结束后,动画执行3秒完毕后的 回复 动画,为什么我们去使用了displayLink
而不去使用NSTimer
,前者对时间的把握更加精确,在制作动画上不建议使用NSTimer。设置好模式后再子线程设置时间执行 回复 动画完毕后销毁 计时器,主线程中 执行计时器的操作。
- (void)finishingLoading
{
self.state = CBStoreHouseRefreshControlStateDisappearing;
UIEdgeInsets newInsets = self.scrollView.contentInset;
newInsets.top = self.originalTopContentInset;
[UIView animateWithDuration:kdisappearDuration animations:^(void) {
self.scrollView.contentInset = newInsets;
} completion:^(BOOL finished) {
self.state = CBStoreHouseRefreshControlStateIdle;
[self.displayLink invalidate];
self.disappearProgress = 1;
}];
for (BarItem *barItem in self.barItems) {
[barItem.layer removeAllAnimations];
barItem.alpha = kbarDarkAlpha;
}
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateDisappearAnimation)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];//主线程 子线程 分开
self.disappearProgress = 1;
}