最近有时间自己实现了tableView中的上拉刷新和下拉加载更多的功能,同时发现了自己在基础知识上的盲点和不足,顺便补习一下,我把代码拿出来跟大家分享一下。
一、bounds、frame等重要概念
frame: 该view在父view坐标系统中的位置和大小。(参照点是,父亲的坐标系统)
bounds:该view在本地坐标系统中的位置和大小。(参照点是,本地坐标系统,就相当于View自己的坐标系统,以0,0点为起点)
contentSize:滚动视图的范围,也就是所有内容的大小
contentOffset:The point at which the origin of the content view is offset from the origin of the scroll view. (文档描述)意思就是内容区域和scrollview的frame区域的高度差,注意一定是frame,这点下面会解释。
contentInset:The distance that the content view is inset from the enclosing scroll view.(文档描述)意思是在contentView周围增加边距,当有contentInset时会让用户感觉内容距离边框有一定的距离,同时它并没有占用增加的边距内容,隐藏的视图会显现出来,一会在代码中会用到。(详见图二)
关于bounds需要好好理解一下,它相当于在frame上层浮动的一层区域,而且是负责显示内容,只不过它是边界,contentView是中间的内容。一般一个View的bounds的原点都是(0,0),因为是以自己为坐标系,但是如果你改变了它的原点,那么该View的子View都会以改变后的原点来进行布局,总结为bounds的原点会影响其子视图的显示位置。
这些概念非常重要,建议大家写写demo具体观察一下。
推荐一个关于这方面介绍的好文章:iOS View的Frame和bounds之区别,setbounds使用(深入探究) - 郭晓东的专栏 - CSDN博客
二、tableView的重要概念
tableview的内容包括:1.cell,2.tableHeaderView / tableFooterView,3.sectionHeader / sectionFooter
contentOffset.y == frame顶部 和 contentSize顶部 的差值
inset是紧紧粘着内容,如果有tableHeaderView / tableFooterView,会在其上 / 下紧贴显示(详见图一)
如果我说的太啰嗦可以直接看图,把我说的当成加强理解就行了。
三、代码分析
接下来进行代码分析,在文章末尾有百度云的传送门。
先看一下效果图(iPhone X系列手机和iPhone8之前的都适配)
这个demo是普通的UITabBarController加UINavigationController,主页控制器是UITableViewController。
以下是用到的属性
首先初始化头部和底部的刷新控件,由于考虑到一般应用的头部都有广告或者搜索框之类的东西,所以header直接采用add subview的方式,直接紧贴着内容添加,也就是在广告条的上方。
footer由于一般底部没什么需要特别添加的,所以直接复制给tableFooterView。
接下来就要监听header的位置变化来实现其中文字的变化,当然图片变化也可以,添加图片后在合适的时候直接旋转。我在scrollViewDidScroll方法中调用dealHeader和dealFooter,重点分析header的处理。
图八中offsetY是一个static变量,因为它是一个固定的临界值,getRectNavAndStatusHight是一个获取顶部导航最大高度的宏,在iPhone X之前和之后的机型中都可以用。offsetY的意思是导航栏的最大高度加上header的高度,之所以加符号是由于下拉时contentOffsetY时负值,当下拉的Y值的绝对值超过offsetY,也就是self.tableView.contentOffset.y < offsetY时,header可以变换样式。这里在解释一下contentOffset,它是图二中你见到的那种样子,手指下滑时也就是下拉时越来越小直到变成负值,手指上滑时值越来越大。而这里有个很有意思的注意点,初始位置contentOffset.y是-88,也就是导航栏的高度,这是因为contentView是我们可视的区域,而bounds的原点是(0,-88),也就印证了我一开始说的bounds负责显示内容,只不过它是边界,contentView是中间的内容。我将打印结果截图了,大家可以分析一下。
bounds也影响了子视图显示位置,包括tableHeaderView的frame的原点是紧贴导航栏的左下角,header的frame的原点是(0,-50),也是以导航栏的左下角为参照。而tableView是占满整个屏幕的,这点通过查看视图可以印证(见图十二,那个紫色的区域就是我选中tableView后显示的)。
回到处理header上,之前能够通过偏移量处理header的变化,接下来就是当用户下拉松手后的处理,当偏移量没有超过临界值时不做操作,直接由scrollViewDidScroll中的dealHeader使其恢复原样,如果超过临界值则调用headerBeginRefreshing方法,监听用户的手势拖拽是scrollViewDidEndDragging:(UIScrollView*)scrollView willDecelerate:(BOOL)decelerate方法。在开始刷新的方法中先判断是不是正在处于刷新,如果是就直接返回,主要是防止在真正的应用中多次向服务器发出请求。在demo中没有接口请求所有我都是通过延迟来模拟的,contentInset的作用就是使得“正在请求数据”的header样式可以停留在用户的视野中。我加了一个动画让其看起来自然些。然后就是调用loadNewData方法请求数据,大家用的时候直接在这里面放入真正的接口请求然后reloadData就可以。最后结束刷新,没什么好说的直接看代码就能看懂,注释写的也很详细。
以上就是整个下拉刷新的实现,主要是为了加深对tableView的一些属性的理解。上拉刷新的过程跟这个差不多,我就不细说了,强调一下临界值的计算: CGFloat footerOffset = self.tableView.contentSize.height - self.tableView.frame.size.height + kTabBarHeight_X; kTabBarHeight_X是底部导航栏的高度,也是一个宏,在iPhone X之前和之后的机型中都可以用。由于是为了让footer的显示完全超过TabBar的上沿后才刷新,所以最后要加上TabBar的高度,前面两个值相减是tableView的contentOffset的Y值计算方法,但显然不是超过tableView的底部而是超过TabBar的上沿,还记不记得tableView的视图查看,这块需要好好理解一下才能想明白。footer没有想header那样处理的那么复杂有三段变化,所以没有在监听结束拖拽的方法中去处理刷新,想加的童鞋可以照着header来处理。
demo地址:链接:https://pan.baidu.com/s/1Ph-i3KFJpCmKQNapWNUDrA 密码:uk8p