iOS菜单滚动定位以及滚动悬浮实现方案
前言
最近应项目需求,要实现一个类似于淘宝顶部菜单点击定位到相应的section位置的界面效果,如下:
淘宝效果.gif顶部的导航菜单负责导航并且定位对应的section的块的位置,点击对应的菜单滚动定位到对应的模块位置。
分析
大致理了一下,这样的效果可以分为三个关键点:
- 菜单悬浮
- 点击菜单定位跳转
- 滚动联动菜单项
这三个问题解决以后效果也就出来了,如下:
实现效果实现
按照分析,实际也就主要解决以上三个问题就实现了这种滚动效果。
首先布局Demo界面,一个分组的带section的UITableView,一个tableViewHeader完成简单的布局。
菜单悬浮的处理方案
首先菜单悬浮的实现并不是系统自带的section的悬浮,因为如果启用系统的滚动悬浮,其他的section都会跟着一起悬浮,会造成菜单栏的丢失,最终目的是保证菜单悬浮而其他的section跟着tableView滚动。所以菜单的悬浮需要我们自己去实现了。为了实现悬浮的效果,可以在tableView代理的滚动方法中根据偏移量来切换菜单的层级,即若滚动超出菜单显示区域,将菜单加载顶部悬浮位置,没有滚动超出的时候,再将菜单放到之前的位置。实现代码如下:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
CGFloat offsetY = scrollView.contentOffset.y;
//悬浮菜单
[self hangOnMenu:offsetY];
//菜单联动
[self updateMenuTitle:offsetY];
}
- (void)hangOnMenu:(CGFloat)offsetY{
if (offsetY > (176 - 44)) {
//防止多次更改页面层级
if ([self.selectMenu.superview isEqual:self.view]) {
return;
}
//加载到view上
self.selectMenu.frame = CGRectMake(0, 64, SCREEN_WIDTH, 44);
[self.view addSubview:self.selectMenu];
}
else{
//防止多次更改页面层级
if ([self.selectMenu.superview isEqual:self.tableView]) {
return;
}
//加载到view上
self.selectMenu.frame = CGRectMake(0, 196, SCREEN_WIDTH, 44);
[self.tableView addSubview:self.selectMenu];
}
}
点击菜单定位跳转的实现
点击菜单跳转到对应的section位置,如果单纯的用tableView scrollToIndex
方法会发现跳转位置不好调整,因为该方法只能调到对应的cell的位置,会将部分的sectionHeader遮挡一部分,导致滚动位置的不准确性。为了保证滚动位置的准确性并且能够做到手动控制,这里采用了 setContentOffset
方法进行内容的滚动,通过rectForSection
方法找到对应section
的位置,如下:
- (LCSelectMenuView *)selectMenu{
if (!_selectMenu) {
_selectMenu = [LCSelectMenuView new];
_selectMenu.frame = CGRectMake(0, 196, SCREEN_WIDTH, 44);
_selectMenu.titleArray = @[@"商品介绍",@"商品型号",@"商品参数",@"相关评论",@"相关推荐"];
__weak typeof(self) _ws = self;
[_selectMenu setPageSelectBlock:^(NSInteger index) {
CGRect rect = [_ws.tableView rectForSection:index];
CGFloat offsetY = rect.origin.y - 20 - 44 - 44;
[_ws.tableView setContentOffset:CGPointMake(0, offsetY) animated:YES];
_ws.scrollFlag = YES; //打开菜单点击标志,防止滚动代理didScrollView触发
}];
}
return _selectMenu;
}
滚动联动菜单项
这里最不好解决的就是滚动的时候菜单标题联动这个问题。滚动的时候因为要保证实时同步当前的菜单的位置,所以在滚动的时候我们需要实时判断是否滚动到对应的菜单。
因为要判断当前滚动的位置是否到达某个菜单,所以在tableView加载完数据之后,需要对tableView中sectionHeader中的位置进行一次计算并保存,这样才能在滚动的时候知道是否滚动到了对应的sectionHeader的位置,才能在滚动的时候改变菜单的位置,所以在布局完成之后进行了以下的计算:
- (void)markSectionHeaderLocation{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.sectionLocationArray = nil;
//计算对应每个分组头的位置
for (int i = 0; i < self.selectMenu.titleArray.count; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:i];
CGRect frame = [self.tableView rectForSection:indexPath.section];
//第一组的偏移量比其他组少10
CGFloat offsetY = (frame.origin.y-64-44);
NSLog(@"offsetY is %f",offsetY);
[self.sectionLocationArray addObject:[NSNumber numberWithFloat:offsetY]];
}
});
}
计算完成之后,我们只需要判断滚动的位置OffsetY的位置是否在各个section的区间段内既可完成菜单的位置的更改。如下:
/**
联动过程步骤title
*/
- (void)updateMenuTitle:(CGFloat)contentOffsetY{
if(!self.scrollFlag){
//遍历
for (int i = 0; i<self.sectionLocationArray.count; i++) {
//最后一个按钮
if (i == self.sectionLocationArray.count - 1) {
if (contentOffsetY >= [self.sectionLocationArray[i] floatValue]) {
[self.selectMenu setCurrentPage:i];
}
}else{
if (contentOffsetY >= [self.sectionLocationArray[i] floatValue] && contentOffsetY < [self.sectionLocationArray[i+1] floatValue]) {
[self.selectMenu setCurrentPage:i];
}
}
}
}
}
需要注意
这里有一点需要强调的是如果手动点击菜单会触发tableView的scrollDidScroll
方法,这里由于会根据滚动的偏移设置菜单的位置会造成滚动的过程中菜单位置的错乱,所以在此添加了一个点击菜单的标志位scrollFlag
来避免因为滚动造成的菜单切换的冲突。
除此之外,如果cell中加入了网页webView
的话,由于cell的没有滚动到cell位置不予显示的特点,会造成计算section
位置的偏差,导致点击菜单跳转到对应的section位置不准确问题,所以在cell中添加webView
的同学要注意了,解决方法可以先显示带有webView
的cell,然显示其他cell即可完成section
的计算的准确。
以下是Demo地址
https://github.com/cclbj/LCHangMenuDemo