新浪微博个人主页效果实现
效果图
注意细节:
- 三个tableview的滚动都可以推动顶部图片和切换栏的滚动。
- 只要切换栏没有贴住导航栏,3个tableview都是从第一个cell开始显示。
- 切换栏贴住导航栏时,每个tableview的显示位置会被保持。
- 切换栏贴住导航栏,手指在当前页面拖动使切换栏脱离导航栏然后再贴住导航栏,另外2个tableview的显示状态被保持。
实现:
结构
- 研究微博个人主页容易发现这里使用了3个tableview。我在主控制器上添加3个子控制器,每一个子控制器管理一个tableview,然后将3个tableview添加到主控制器view上,所有的子控制器都继承自BaseTableViewController,方便后面的滚动监听。
- 每个tableview右边的指示条滚动范围都包含了顶部视图(图片)。在之前的版本中,我没有让tableview们共用一个顶部视图,而是给它们设置了同样的tableHeaderView(包含同样的imageView和同样的将会被切换栏覆盖的空白区域)。虽然这种结构能够实现微博个人主页的效果,但是不方便扩展,比如给tableview增加滑动手势,来左右切换要显示的界面。
- 在最新版本中,我给每个tableview都设置了240高度(图片高度200+切换栏高度40)的空白UIView作为tableHeaerView,并让3个tableview共用的一个headerView(包括要显示的图片和切换栏)。
首先将headerView加入到当前显示的tableview 上,跟着tableview滚动。当偏移量能够达到切换栏贴着导航栏时,将headerView加入到主控制器view中,固定位置;当偏移量要使切换栏脱离导航栏时,又将headerView加入到当前显示的tableview中;要切换当前显示的tableview,也需要根据偏移量来判断加入到哪个view上。
监听
我在这里采用代理方式来监听每个tableview在Y轴上的偏移量,主控制器作为BaseTableViewController的代理。 协议方法:
@protocol TableViewScrollingProtocol <NSObject>
- (void)tableViewScroll:(UITableView *)tableView offsetY:(CGFloat)offsetY;
- (void)tableViewWillBeginDecelerating:(UITableView *)tableView offsetY:(CGFloat)offsetY;
- (void)tableViewDidEndDecelerating:(UITableView *)tableView offsetY:(CGFloat)offsetY;
@end
计算
- (void)tableViewScroll:(UITableView *)tableView offsetY:(CGFloat)offsetY{
if (offsetY > headerImgHeight - topBarHeight) {
if (![_headerView.superview isEqual:self.view]) {
[self.view insertSubview:_headerView belowSubview:_navView];
}
CGRect rect = self.headerView.frame;
rect.origin.y = topBarHeight - headerImgHeight;
self.headerView.frame = rect;
} else {
if (![_headerView.superview isEqual:tableView]) {
for (UIView *view in tableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
[tableView insertSubview:_headerView belowSubview:view];
break;
}
}
}
CGRect rect = self.headerView.frame;
rect.origin.y = 0;
self.headerView.frame = rect;
}
}
这段代码主要是实时监听tableview的Y轴偏移量,如果当前offsetY大于136(图片高度200减去导航栏状态栏高度64))的话,就将headerView添加到主控器view上,此时切换栏紧贴导航栏。如果小于的话,就将headerView添加到当前显示的tableView上,跟随tableview滑动。
切换栏在没有贴住导航栏时,三个tableview的Y轴偏移量是同样的,贴住时就不再一样。 我在主控制器里设置了一个字典来存储每个tableview的偏移量,key为tableview所在控制器的地址(之前我的使用的是tableview的地址,造成的问题是在tableview上拖拽,会造成其他两个控制器view被提前加载一次)。这样我就可以随时记录和改变每个偏移量,然后在切换到要显示的tableview时,直接设置给它的contentoffset.y就可以了。
- (void)tableViewDidEndDragging:(UITableView *)tableView offsetY:(CGFloat)offsetY {
_segCtrl.userInteractionEnabled = YES;
NSString *addressStr = [NSString stringWithFormat:@"%p", _showingVC];
if (offsetY > headerImgHeight - topBarHeight) {
[self.offsetYDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([key isEqualToString:addressStr]) {
_offsetYDict[key] = @(offsetY);
} else if ([_offsetYDict[key] floatValue] <= headerImgHeight - topBarHeight) {
_offsetYDict[key] = @(headerImgHeight - topBarHeight);
}
}];
} else {
if (offsetY <= headerImgHeight - topBarHeight) {
[self.offsetYDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id _Nonnull obj, BOOL * _Nonnull stop) {
_offsetYDict[key] = @(offsetY);
}];
}
}
}
当手指离开屏幕时,如果当前tableview没有速度了,就要在tableViewDidEndDragging:offsetY:方法里面获取tableview的偏移量。如果offsetY大于136的话,先把自己的存起来。而另外两个tableview,如果它们之前的偏移量小于等于136,现在就要改成136了(切换过去的话,让它们刚好偏移到切换栏贴住导航栏);如果offsetY小于等于136,就将三个tableview的存储偏移量设为一样。
如果当前tableview还有速度,就要在tableViewDidEndDecelerating: offsetY:方法里面设置,处理方式一样。
- (void)segmentedControlChangedValue:(HMSegmentedControl*)sender {
[_showingVC.view removeFromSuperview];
BaseTableViewController *newVC = self.childViewControllers[sender.selectedSegmentIndex];
if (!newVC.view.superview) {
[self.view addSubview:newVC.view];
newVC.view.frame = self.view.bounds;
}
NSString *nextAddressStr = [NSString stringWithFormat:@"%p", newVC];
CGFloat offsetY = [_offsetYDict[nextAddressStr] floatValue];
newVC.tableView.contentOffset = CGPointMake(0, offsetY);
[self.view insertSubview:newVC.view belowSubview:self.navView];
if (offsetY <= headerImgHeight - topBarHeight) {
[newVC.view addSubview:_headerView];
for (UIView *view in newVC.view.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
[newVC.view insertSubview:_headerView belowSubview:view];
break;
}
}
CGRect rect = self.headerView.frame;
rect.origin.y = 0;
self.headerView.frame = rect;
} else {
[self.view insertSubview:_headerView belowSubview:_navView];
CGRect rect = self.headerView.frame;
rect.origin.y = topBarHeight - headerImgHeight;
self.headerView.frame = rect;
}
_showingVC = newVC;
}
这段代码就是移除之前的tableview,添加将要显示的。 唯一注意点就是要根据偏移量来判断将headerView加到主控制器view上还是tableview上。
其它
1.关于导航栏颜色渐变的处理,我这里采用的是将系统导航栏设为无色。
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController.navigationBar setBackgroundImage:[UIImage new]forBarMetrics:UIBarMetricsDefault];
self.navigationController.navigationBar.shadowImage = [UIImage new];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.navigationController.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
self.navigationController.navigationBar.shadowImage = nil;
}
然后添加一个navView(UIView)盖在导航栏上,然后滚动监听改变其alpha就好了。
2.为了记录准确的偏移量,跟微博一样,采取了在滚动过程中第一次单击切换栏使tableView停止滚动,第二次单击切换栏才切换tableView的做法。因为tableView自带了单击停止滚动的效果,所以如果切换栏没有贴住导航栏时,它的父控件headerView作为tableView的一部分,单击切换栏就可以让tableView停止。如果切换栏贴住导航栏时,它的父控件headerView就是控制器view的一部分了,我的做法是此时屏蔽掉headerView对触摸事件的响应,这样子切换栏后面的tableView部分就可以响应该单击事件了。详情请看源码,分析过程请看http://www.jianshu.com/p/2f664e71c527
3.状态栏颜色变化已经添加,gif图没有体现出来。详情请看源码,分析过程请看http://www.jianshu.com/p/ee1c9c91a477
4.研究发现新浪微博的顶部视图并没有下拉放大的效果,而是图片隐藏了一部分,下拉会显示出来。下拉放大实现起来比较容易,并且我觉得意义不大,在此就不在添加了。