自定义下拉刷新控件
来源:互联网 发布:centos和linux的区别 编辑:程序博客网 时间:2024/05/22 11:53
一:前言
记得工作中第一次用的刷新控件是svpulltorefresh,用法稍微有点麻烦,而且bug颇多,后来果断放弃,现在用的是MJRefresh,不管是用法还是bug,都比前一个好多了,但是不久前也遇到了一个致命的bug,有好些情况下会导致MJRefresh陷入一个死循环,导致不断的刷新,只能重启软件才行。MJRefresh工程比较庞大,找到了bug也很难修改,然后还是决定自己写一个,系统提供的UIRefreshControl我认为是最好的,缺点是不提供自定义UI的方法,那么我就自己基于它来自定义UI。我不是一开始就决定继承于UIRefreshControl,我同时也写了一个继承与UIView的control,两个进行对比,发现使用UIview会有很多弊端,这种弊端在一些复杂特殊的情况下一下子就暴露出来了,而且很难解决,当然,正常状态下是没什么问题的,有兴趣的同学倒是可以去试一试。本demo供大家学习和参考,如有发现bug,还请issues 我。
二: 了解 UIRefreshControl
基本使用方法
//初始化一个control
UIRefreshControl*control = [[UIRefreshControlalloc] init];
//给control 添加一个刷新方法
[control addTarget:self action:@selector(refreshAction) forControlEvents:UIControlEventValueChanged];
//把control 添加到 tableView
[self.tableView addSubview:control];
存在的问题
刷新时的动画是一个灰色小菊花,很多情况下不符合app的刷新动画效果
经过多次反复测试,下拉的偏移量达到130以上才会触发刷新方法,很显然这个也不符合,一般的刷新控件的高度60左右,所以下拉的偏移量达到60就可以触发刷新的方法了。
自定义控件的思路
去掉默认的动画效果
自定义自己的动画效果
改变满足刷新时的条件
三:FMRefreshControl
先看一下我写完的这个控件的使用方法
FMRefreshControl*control = [[FMRefreshControlalloc] initWithTargrt:self refreshAction:@selector(refreshAction)];
[self.tableView addSubview:control];
两行代码,用法比系统的还要稍微简单一点。
再看一下效果
四:思路与代码
1. 关于 UIRefreshControl 的几个注意点,通过frame无法修改它的高度,修改高度目前只找到一种方法,先添加到 superViwe,再执行
[[_control.subviews objectAtIndex:0] setFrame:CGRectMake(0,0,_control.bounds.size.width,30)];
一开始我是想改变它的高度是否就能改变它的触发刷新的偏移量,然后我找到了这个方法可以修改它的高度,但实际上改变了高度还是无法改变触发下拉刷新的偏移量,所以我们需要自定义去触发刷新这个动作的时机。
2.手动去触发刷新动作也有几个注意点,我们是根据偏移量去触发刷新,但是仅仅靠这一个动作是不够的,还需要一个条件,那就是用户手指响应过屏幕,简单地说,先定义一个变量,如果用户触摸过屏幕,就把变量置为YES,然后再判断用户手指离开时是否达到了触发刷新的偏移量,如果两个条件都满足,就触发刷新,刷新完把变量置为NO,如果不满足,就不触发,也把变量置为NO。这样就避免了UIScrollow 因偏移量变动而导致非人为的刷新。
3. 进入代码阶段
FMRefreshControl *control = [[FMRefreshControlalloc]initWithTargrt:selfrefreshAction:@selector(refreshAction)];
[self.tableViewaddSubview:control];
初始化的时候赋一个 target 和 一个 action,当满足条件的时候,我们需要知道让谁去执行刷新方法,有这两个参数足够,当执行到第二行 addSubView的时候,我们需要在control内部实现这个方法:
-(void)willMoveToSuperview:(UIView *)newSuperview{
[superwillMoveToSuperview:newSuperview];
if([newSuperviewisKindOfClass:[UIScrollViewclass]]){
self.superScrollView = (UIScrollView *)newSuperview;
[self.superScrollViewaddObserver:selfforKeyPath:@"contentOffset"options:NSKeyValueObservingOptionNewcontext:nil];
}
}
这样,我们就知道当前这个control被添加到哪个父视图上了,为了安全及代码的严谨,先判断父视图是否属于
UIScrollView,如果是,就用KVO监听contentOffset属性,这样便能知道用户滑动的偏移量。
这里我定义了3种状态:
typedefNS_ENUM(NSInteger,FMRefreshState){
FMRefreshStateNormal = 0, /** 普通状态 */
FMRefreshStatePulling, /** 释放刷新状态 */
FMRefreshStateRefreshing, /** 正在刷新 */
};
以及切换状态后UI的切换和方法的触发:
-(void)setCurrentStatus:(FMRefreshState)currentStatus{
_currentStatus = currentStatus;
switch(_currentStatus){
caseFMRefreshStateNormal:
NSLog(@"切换到Normal");
[self.imageViewstopAnimating];
self.label.text = FM_Refresh_normal_title;
[self.labelsizeToFit];
self.imageView.image = [UIImageimageNamed:@"refresh_1"];
break;
caseFMRefreshStatePulling:
NSLog(@"切换到Pulling");
self.label.text = FM_Refresh_pulling_title;
[self.labelsizeToFit];
self.imageView.animationImages = self.refreshingImages;
self.imageView.animationDuration = 1.5;
[self.imageViewstartAnimating];
break;
caseFMRefreshStateRefreshing:
NSLog(@"切换到Refreshing");
self.label.text = FM_Refresh_Refreshing_title;
[self.labelsizeToFit];
[selfbeginRefreshing];
self.imageView.animationImages = self.refreshingImages;
self.imageView.animationDuration = 1.5;
[self.imageViewstartAnimating];
[selfdoRefreshAction];
break;
}
}
切换到FMRefreshStateNormal 停止动画,切换到FMRefreshStatePulling 开始动画,达到这个状态,说明用户已经达到了刷新的偏移量,此时松手便可刷新,切换到FMRefreshStateRefreshing,如果此时往回滑动,小于临界值,那么状态重新切回FMRefreshStateNormal。
满足刷新条件,则便可执行以下方法:
-(void)doRefreshAction
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if(self.refreshTarget && [self.refreshTargetrespondsToSelector:self.refreshAction])
[self.refreshTargetperformSelector:self.refreshAction];
#pragma clang diagnostic pop
}
下面看最关键的KVO方法,也是这里面最复杂的逻辑处理代码:
-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id>*)change context:(void*)context{
//isDragging 属性是指用户手指是否在拖动
if(self.superScrollView.isDragging && !self.isRefreshing){
if(!self.originalOffsetY){
self.originalOffsetY = -self.superScrollView.contentInset.top;
}
CGFloatnormalPullingOffset = self.originalOffsetY - k_FMRefresh_Height;
if(self.currentStatus == FMRefreshStatePulling && self.superScrollView.contentOffset.y > normalPullingOffset){
self.currentStatus = FMRefreshStateNormal;
}elseif(self.currentStatus == FMRefreshStateNormal && self.superScrollView.contentOffset.y < normalPullingOffset){
self.currentStatus = FMRefreshStatePulling;
}
}elseif(!self.superScrollView.isDragging){
if(self.currentStatus == FMRefreshStatePulling){
self.currentStatus = FMRefreshStateRefreshing;
}
}
//拖动的偏移量,转换成正数
CGFloatpullDistance = -self.frame.origin.y;
self.backgroundView.frame = CGRectMake(0,0,k_FMRefresh_Width,pullDistance);
CGFloattotalWidth = 35 + 20 + self.label.bounds.size.width;
CGFloatimageViewX = (k_FMRefresh_Width - totalWidth)/2;
self.imageView.frame = CGRectMake(imageViewX, -k_FMRefresh_Height+pullDistance+(k_FMRefresh_Height - self.imageView.bounds.size.height)/2,self.imageView.frame.size.width,self.imageView.frame.size.height);
self.label.frame = CGRectMake(imageViewX + 35 + 20, -k_FMRefresh_Height + pullDistance + (k_FMRefresh_Height - self.label.bounds.size.height)/2,self.label.frame.size.width,self.label.frame.size.height);
}
这里最重要的就是处理两点:1. 根据偏移量和用户手指的拖动来切换状态,2. control上面的子视图需要我们根据偏移量来实时更新。
还有一种情况,上面也提到过,用户先滑动到FMRefreshStatePulling状态,然后又往回滑动,此时的偏移量在0-FMRefreshStatePulling状态的偏移量之间,此时调用自身的 endRefreshing偏移量不会复原,还需要我们自己处理,看了几个老外写的自定义刷新控件,他们都没修复这个bug。他们也没封装,全部代码写在了控制器里,什么都没有改变,只是实现了一个动画效果,还多了个bug,动画效果倒是不错的。有兴趣的可以参考一番:
https://www.jackrabbitmobile.com/app-development/ios-custom-pull-to-refresh-contro/
https://possiblemobile.com/2014/05/ios-custom-pull-to-refresh/
-(void)endRefreshing{
if(self.currentStatus != FMRefreshStateRefreshing){
return;
}
self.currentStatus = FMRefreshStateNormal;
[superendRefreshing];
//在执行刷新的状态中,用户手动拖动到 nornal 状态的 offset,[super endRefreshing] 无法回到初始位置,所以手动设置
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(0.5 * NSEC_PER_SEC)),dispatch_get_main_queue(), ^{
if(self.superScrollView.contentOffset.y >= self.originalOffsetY - k_FMRefresh_Height && self.superScrollView.contentOffset.y <= self.originalOffsetY){
CGPointoffset = self.superScrollView.contentOffset;
offset.y = self.originalOffsetY;
[self.superScrollView setContentOffset:offset animated:YES];
}
});
}
最后还有一点不要忘记 dealloc移除监听:
-(void)dealloc{
[self.superScrollViewremoveObserver:selfforKeyPath:@"contentOffset"];
}
整篇文章从上至下是按照整个完整的思路写下来的,先是提出遇到的问题以及难点,然后最后的代码和思路也是由外至内一路写下来,希望方便大家阅读。这是上篇,下拉刷新的,还有下篇,上拉加载,过两天写,demo中已经有了,不过就是还没优化。
domo地址:https://github.com/suifengqjn/FMRefreshControl
- 自定义控件--下拉刷新
- 自定义控件-下拉刷新
- 自定义控件--下拉刷新
- 自定义下拉刷新控件
- 自定义控件:下拉刷新
- 自定义下拉刷新控件-CBStoreHouseRefreshControl
- 自定义下拉刷新控件-CBStoreHouseRefreshControl
- 自定义下拉刷新控件-CBStoreHouseRefreshControl
- 自定义下拉刷新控件-CBStoreHouseRefreshControl
- 自定义下拉刷新控件-CBStoreHouseRefreshControl
- Android自定义控件下拉刷新
- 关于下拉刷新自定义控件
- 自定义下拉刷新控件-仿美团刷新效果
- Android PullToRefreshView自定义下拉刷新控件
- Android自定义控件之仿美团下拉刷新
- Android自定义控件之仿美团下拉刷新
- Android自定义控件--下拉刷新的实现
- 自定义控件 CustomListView (下拉刷新)
- 洛谷 P1330 封锁阳光大学 二分图染色模板
- jdbc调用oracle存储过程
- Ubuntu下安装MySQL
- html布局:定位position使用技巧
- linux静态链接库与动态链接库详解
- 自定义下拉刷新控件
- 深入理解Angular中的$apply()以及$digest()
- IIS的基本概念以及如何启用IIS服务
- popupWindos使用说明
- 字典数据
- Linux错误集锦
- js入门
- gitHub使用简单介绍
- redis中 hash类型的操作命令