现在上市场上很多主流的App模块都是很庞大,往往一个页面会有很复杂的功能,但是再大的模块都是由一个个简单的模块堆叠起来的。模块多了之后,有时要处理好模块之间的联系不是那种想都不用想就可以开干的。还涉及到不同模块之间的手势和动画处理。
为了举个比较好的栗子,就拿微博个人信息界面动动手。通过观察微博个人信息界面,总结出需要注意的三个地方:
- 头部背景图滚动处理
- 两层滚动视图嵌套手势处理
- 按钮底部毛毛虫动画效果
头部背景
先看下效果图
首先第一步分析 顶部的ImageView,如果手势向上滚动我们会发现它就和 TableHeaderView差不多,当向下拉伸直到TableView出现弹性效果的时候,头部的ImageView变高了,但是不是放大。而头像是保持不动的。因此,顶部ImageView不是TableHeaderView,它的坐标会随着TableView滚动作出不同的改变。它的位置是在TableView下面,设置
tableHeaderView
为透明即可。在
scrollViewDidScroll:
代理函数中实现(-60是它的初始y坐标)
if(offsetY <= 0){
self.coverView.y = -offsetY/2 + (- 60);
}else{
self.coverView.y = -offsetY + (- 60);
}
多重嵌套
实现这个布局可以是实现一个TabView
,把四个子视图作为TabView的子控件, TabView
作为TableView
的cell显示出来。但是当我们照做的时候发现,当滚动TableView
到底部的时候,TabView
的子视图不会滚动,而滚动TabView
的子视图到顶部,TableView
也不会滚动。之所以会出现这样的情况是因为子控件和父控件手势是单独响应不会传递。解决的方法是自定义一个UITableView
重写以下方法
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
这个方法返回YES可以让子控件接收到父控件的手势事件,子控件的手势也会传给父控件,在这里可以让UITableView的滚动事件传递。
但是实现后发现 滚动四个子控件 父控件也跟着滚动。我们要实现的是:当父控件滚动到底部时子控件才能滚动,子控件滚动到头部时,子控件停止滚动父控件开始滚动。逻辑代码如下:
父控件:
if(offsetY >= HeaderHeight - 64){
[[NSNotificationCenter defaultCenter] postNotificationName:TabViewScrollToTopNotification object:@(YES)];
self.shouldScroll = NO;
}else{
[[NSNotificationCenter defaultCenter] postNotificationName:TabViewScrollToTopNotification object:@(NO)];
}
if(self.shouldScroll == NO){
[scrollView setContentOffset:CGPointMake(0, HeaderHeight - 64)];
}
子控件:
CGFloat offsetY = scrollView.contentOffset.y;
if(offsetY <= 0){
[[NSNotificationCenter defaultCenter] postNotificationName:ItemScrollToTopNotification object:@(YES)];
}
if(self.shouldScroll == NO){
[scrollView setContentOffset:CGPointZero];
}
毛毛虫效果
实现毛毛虫动画效果可有多种方式,在这里我用的是CASharpLayer
结合UIBezierPath
来实现。
仔细观察动画可发现,动画可以切分为两部完成,如下图所示:
通过改变
CASharpLayer
的strokeStart
和strokeEnd
属性来控制长度即可。
首先, 在初始化布局每个标题按钮的时候,用一个字典把所有按钮下面的坐标记录下来
- (void)setupBtn:(UIButton *)btn index:(NSInteger)index{
if(index == 0){
btn.selected = YES;
self.selectBtn = btn;
}
self.btnDict[@(index)] = btn;
self.pointsDict[@(index)] = @{@"start":@(btn.x + 2),@"end":@(CGRectGetMaxX(btn.frame)-2)};
btn.tag = index;
/**
. 其他设置
......
*/
}
然后通过监控scrollView的偏移量,结合保存的坐标计算出在不同位置的下标线的strokeStart和strokeEnd
- (void)LHTabViewDidScroll:(LHTabView *)tabView{
CGFloat offsetX = tabView.offset.x;
NSInteger index = offsetX/WIDTH;
CGFloat zero = [self.pointsDict[@(0)][@"start"] floatValue];
CGFloat currentStart = [self.pointsDict[@(index)][@"start"] floatValue];
CGFloat currentEnd = [self.pointsDict[@(index)][@"end"] floatValue];
CGFloat nextStart = [self.pointsDict[@(index+1)][@"start"] floatValue];
CGFloat nextEnd = [self.pointsDict[@(index+1)][@"end"] floatValue];
CGFloat PhysicsDelta = offsetX - index * WIDTH;
CGFloat end,start;
CGFloat delta = nextEnd - currentEnd;
if(PhysicsDelta <= WIDTH/2){ //对应图片 step one
delta = (PhysicsDelta/(WIDTH/2)) * delta;
end = currentEnd + delta;
self.lineLayer.strokeStart = (currentStart - zero)/self.lineTotalWidth;
self.lineLayer.strokeEnd = (end - zero)/self.lineTotalWidth;
}else{ //对应图片 step two
delta = nextStart - currentStart;
PhysicsDelta = PhysicsDelta - WIDTH/2;
delta = (PhysicsDelta/(WIDTH/2)) * delta;
start = currentStart + delta;
self.lineLayer.strokeStart = (start - zero)/self.lineTotalWidth;
self.lineLayer.strokeEnd = (nextEnd - zero)/self.lineTotalWidth;
}
}
刚开始的时候发现CAShaplayer滑动的效果和scrollView的滑动有延迟,可以通过CAShaplayer的speed
属性来调整动画速率即可 。
实现效果图如下:
以上就是所有的步骤。具体代码可以点击demo
最后吐槽下,因为最近在使用react-native做一个项目,需要实现类似功能,但是因为滑动的效果没有原生的流畅,出现很奇怪的现象,各种不顺,没办法只能放弃使用这种UI结构。看来react-native还有很长的路要走,原生还是第一生产力。