因为项目有个界面要模仿高德地图路径规划滑动效果,因此写了demo,并简单说下分析过程。
高德地图效果演示:
仿高德路线规划滑动.gif
demo效果演示:
高德地图规划滑动.gif
一. 分析
首先,我们可以看出这个滚动的视图应该是UIScrollView或者UIScrollView的子类(比如:UITableView);
其次,从高德地图里的视图一开始的滑动,可以看出这个滑动是平稳的滑动,没有加速和减速,因此这里不可能是UIScrollView的滚动效果,因为UIScrollView的滚动效果是由一个加减速的过程,因此一开始滑动,应该是通过滑动手势UIPanGestureRecognizer,来移动UIScrollView的y值来移动
接着滑动到指定位置之后,UIScrollView的y值固定不动,然后UIScrollView的内容进行滚动。这里就涉及到滑动手势UIPanGestureRecognizer的滑动,还有UIScrollView内部的滚动的处理。高德地图的演示效果里面,一开始滑动视图向上移动,移动到指定的点之后,立马就变成视图的滚动,这里可以分析,UIScrollView既支持手势的滑动又支持视图的滚动,只是通过条件来判断限制两者的执行逻辑。
同时我们可以看到,如果一开始向上拉动视图力度大一点,视图会直接滚动到指定位置,如果力度小,就恢复到原来位置,因此这里需要依据手势滑动的加速度来进行判断处理。
而当你滑动到中间位置的时候,也需要依据最后滑动的位置来判断应该动画滚动到上方还是下方。
最后滑动的时候上方的视图和滑动视图本身有背景颜色的渐变效果,这里需要依据滑动距离来判断。
二.代码分析:
首先由于滚动视图(demo里面是UITableView)需要支持手势滑动和内部滚动,因此需要写一个类FJBaseTableView继承自UITableView,然后在FJBaseTableView的实现里面重写如下方法:
// 当有 多个手势 都可以 响应
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
来支持响应多个手势。
然后给滚动视图tableView添加滑动手势,当tableView从底部滑动到顶部指定位置时,应该限制tableView内部的视图滚动。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (self.tableView.frame.origin.y > _scrollViewStartPositionY) {
[scrollView setContentOffset:CGPointMake(0, 0)];
}
}
这里的_scrollViewStartPositionY是顶部指定位置。
接着看下手势滑动的处理逻辑:
#pragma mark - 手势处理
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
_beganPoint = [sender locationInView:sender.view.superview];
_curPoint = sender.view.center;
_topTipContainerViewCurrentY = _topContainerView.frame.origin.y;
_previousOffsetY = self.tableView.contentOffset.y;
} else if(sender.state == UIGestureRecognizerStateChanged) {
CGPoint point = [sender locationInView:sender.view.superview];
CGFloat offsetY = _previousOffsetY - self.tableView.contentOffset.y;
NSInteger y_offset = point.y - _beganPoint.y - offsetY;
if (sender.view.frame.origin.y >= _scrollViewStartPositionY || (self.tableView.contentOffset.y == 0 && self.tableView.contentSize.height > self.tableView.frame.size.height)) {
sender.view.center = CGPointMake(_curPoint.x, _curPoint.y + y_offset);
[self updateViewControlsWithSlideOffset:y_offset];
}
if (sender.view.frame.origin.y > _scrollViewLimitMaxY) {
sender.view.y = _scrollViewLimitMaxY;
[self updateViewControlsWithSlideUp:NO];
}
else if(sender.view.frame.origin.y < _scrollViewStartPositionY) {
sender.view.y = _scrollViewStartPositionY;
[self updateViewControlsWithSlideUp:YES];
}
} else if(sender.state == UIGestureRecognizerStateEnded) {
if (sender.view.frame.origin.y <= _scrollViewStartPositionY || sender.view.frame.origin.y > _scrollViewLimitMaxY) {
if (sender.view.frame.origin.y <= _scrollViewStartPositionY) {
[self updateViewControlsWithSlideUp:YES];
}
if (sender.view.frame.origin.y > _scrollViewLimitMaxY) {
[self updateViewControlsWithSlideUp:NO];
}
return;
}
// 滑动速度处理
CGPoint velocity = [sender velocityInView:self.view];
CGFloat speed = 350;
if (velocity.y < - speed) {
// 快速向上
[self tableViewMoveToTop];
return;
} else if (velocity.y > speed) {
// 快速向下
[self tableViewMoveToBottom];
return;
}
// 滑动临界值
CGFloat criticalValue = _scrollViewLimitMaxY/2.0;
if (sender.view.frame.origin.y <= criticalValue) {
[self tableViewMoveToTop];
} else {
[self tableViewMoveToBottom];
}
}
}
这里几个点需要注意:
_beganPoint、_curPoint两个参数是用来计算手势滑动距离然后调整scrollView的滑动距离。而_previousOffsetY是用来记录滑动之前tableView的内部视图的偏移距离,因为当tableView滑动到顶部指定位置后,tableView开始滚动,这时候tableView向下滑动是先移动了tableView内部的滚动距离,然后才是滑动距离,因此需要将这部分值先记录,然后去除掉,才是tableView向下真正需要滑动的距离。
CGFloat offsetY = _previousOffsetY - self.tableView.contentOffset.y;
NSInteger y_offset = point.y - _beganPoint.y - offsetY;
2.滑动过程中,顶部视图的移动和渐变处理,这里先依据滑动的距离算出tableView滑动距离与tableView最大滑动距离的比值,然后再算出顶部视图需要移动的距离和背景的透明度。
- (void)updateViewControlsWhenSliding {
if (self.tableView.frame.origin.y > _scrollViewStartPositionY && self.tableView.frame.origin.y < _scrollViewLimitMaxY) {
CGFloat offsetLimitDistance = _scrollViewLimitMaxY - _scrollViewStartPositionY;
CGFloat offsetDistance = self.tableView.frame.origin.y - _scrollViewStartPositionY;
if (offsetDistance > 0 && offsetDistance < offsetLimitDistance) {
CGFloat topViewHeight = [FJFTopContainerView viewHeight];
CGFloat topViewHeightOffset = offsetDistance * (topViewHeight / offsetLimitDistance);
CGFloat viewAlpha = offsetDistance / offsetLimitDistance;
_topContainerView.y = topViewHeightOffset - topViewHeight;
_topContainerView.alpha = viewAlpha;
}
}
}
3.滑动速度处理,依据velocityInView函数获取速度值,然后依据当前速度值大小和正负和设定的速度值比较来判断是否需要向上或向下移动。
// 滑动速度处理
CGPoint velocity = [sender velocityInView:self.view];
CGFloat speed = 350;
if (velocity.y < - speed) {
// 快速向上
[self tableViewMoveToTop];
return;
} else if (velocity.y > speed) {
// 快速向下
[self tableViewMoveToBottom];
return;
}
4.滑动临界值处理,判断最后滑动位置与底部指定位置一半,两个值的大小来判断滑动的方向。
// 滑动临界值
CGFloat criticalValue = _scrollViewLimitMaxY/2.0;
if (sender.view.frame.origin.y <= criticalValue) {
[self tableViewMoveToTop];
} else {
[self tableViewMoveToBottom];
}
三.总结
这里最主要就是介绍了分析的思路,来找出可靠的实现方法,具体逻辑,详见demo