1. 简介
“甩出”效果是当我们快速拖动container并松开后,container继续朝原方向运动,但是渐渐减速直到停止的效果。
ScrollView的onTouchEnded方法会设置Timer,间隔0、延迟0、无限次数,回调函数是deaccelerateScrolling方法。说明触摸结束时,当该方法不被unschedule时将每帧执行一次。
2. setContentSize
先看一个和“甩出”有关的方法setContentSize:
ScrollView的setContentSize重写了Node的方法,设置的是container的尺寸。
注意,还调用了updateInset方法,执行:
_maxInset = this->maxContainerOffset(); _maxInset.set(_maxInset.x + _viewSize.width * INSET_RATIO, _maxInset.y + _viewSize.height * INSET_RATIO); _minInset = this->minContainerOffset(); _minInset.set(_minInset.x - _viewSize.width * INSET_RATIO, _minInset.y - _viewSize.height * INSET_RATIO);
设置了_maxInset:maxContainerOffset的值加部分可视范围。_minInset:minContainerOffset减部分可视范围。
INSET_RATIO为0.2,可以自行修改。
_maxInset和_minInset相当于扩大了偏移范围,并且只在有回弹效果时才有用处。
3. deaccelerateScrolling
情况一:开启回弹,不执行setContentSize
deaccelerateScrolling方法中会判断ScrollView是否设置了回弹效果。
我们先看有回弹的情况,如果不对ScrollView执行setContentSize,这样的话maxInset和minInset均为0。
if (_bounceable) //有回弹则true { maxInset = _maxInset; minInset = _minInset; }
接下来设置container位置,代码不再粘贴。
然后,该if语句第二第三个条件永远满足,那么将会执行unschedule,说明了deaccelerateScrolling将会只执行这一次。之后,当container此时超出范围时,再通过relocateContainer方法设置回弹效果,不再赘述。
if ( (fabsf(_scrollDistance.x) <= SCROLL_DEACCEL_DIST && fabsf(_scrollDistance.y) <= SCROLL_DEACCEL_DIST) || ((_direction == Direction::BOTH || _direction == Direction::VERTICAL) && (newY >= maxInset.y || newY <= minInset.y)) || ((_direction == Direction::BOTH || _direction == Direction::HORIZONTAL) && (newX >= maxInset.x || newX <= minInset.x)) ) { this->unschedule(CC_SCHEDULE_SELECTOR(ScrollView::deaccelerateScrolling)); this->relocateContainer(true); }
因为deaccelerateScrolling仅执行一次,仅在设置一次container的位置,所以没有“甩出”效果。
情况二:没有回弹
当没有回弹时,maxInset和minInset被设置成container偏移范围的界限。
if (_bounceable) //false { //... } else { maxInset = this->maxContainerOffset(); minInset = this->minContainerOffset(); }
那么,如果拖动在范围之内,接下来if判断的3个条件中后两个将为false,我们看第一个条件:
(fabsf(_scrollDistance.x) <= SCROLL_DEACCEL_DIST && fabsf(_scrollDistance.y) <= SCROLL_DEACCEL_DIST)
_scrollDistance在if之前执行了:
_scrollDistance = _scrollDistance * SCROLL_DEACCEL_RATE;
SCROLL_DEACCEL_DIST是一个界限,当_scrollDistance绝对值小于它时,使得if第一个条件满足,执行unschedule,deaccelerateScrolling将不会在下一帧执行。
onTouchMoved结束时保存了_scrollDistance,该向量是两次Moved之差。在触摸结束后,每帧执行deaccelerateScrolling时,_scrollDistance都会乘以小于0的系数SCROLL_DEACCEL_RATE,使得_scrollDistance渐渐变小。
并且,每次deaccelerateScrolling方法开始会根据当前_scrollDistance设置container的新位置:
_container->setPosition(_container->getPosition() + _scrollDistance);
到这里,显而易见,虽然触摸结束了,但是deaccelerateScrolling将会在触摸结束后每帧执行,设置container的新位置,而每帧位置的增长都渐渐变小,实现了“甩动甩出”的效果。
当_scrollDistance小于界限值时,将会unschedule销毁Timer,deaccelerateScrolling不会在下一阵执行,我们看到的“甩出”效果就结束了。
还有一个问题,在无回弹情况下,如果“甩出”时container到了边界是如何处理的?
看deaccelerateScrolling部分代码:
_container->setPosition(_container->getPosition() + _scrollDistance); //假设此时设置位置后越界 if (_bounceable) //false { //... } else { maxInset = this->maxContainerOffset(); minInset = this->minContainerOffset(); } newX = _container->getPosition().x; newY = _container->getPosition().y; _scrollDistance = _scrollDistance * SCROLL_DEACCEL_RATE; this->setContentOffset(Vec2(newX,newY)); //看
container在setPosition后又用setContentOffset方法设置了一次位置。
看setContentOffset部分代码:
if (!_bounceable) { const Vec2 minOffset = this->minContainerOffset(); const Vec2 maxOffset = this->maxContainerOffset(); offset.x = MAX(minOffset.x, MIN(maxOffset.x, offset.x)); offset.y = MAX(minOffset.y, MIN(maxOffset.y, offset.y)); } _container->setPosition(offset); if (_delegate != nullptr) { _delegate->scrollViewDidScroll(this); }
当没有越界时,确实是执行了两次参数一样的setPosition。
而越界时,位置会被修正为边界位置setPosition。
同时,因为修正前的坐标已经越界,deaccelerateScrolling方法最后会触发unschedule,“甩出”效果终止。
情况三:开启回弹,执行setContentSize
因为执行了setContentSize,maxInset和minInset不是0,而是比maxContainerOffset()和minContainerOffset()计算的值扩大了部分可视范围。
本情况中偏移范围扩大了,是为了当container“甩出”时,允许在一定程度内超出maxContainerOffset()和minContainerOffset()范围。当“甩出”过了一定程度,会触发if三条件中的越界条件,从而执行unschedule,再执行relocateContainer设置回弹效果。
4. 总结
onTouchEnded设置了一个在触摸结束后每帧执行的Timer。
当有“甩出”效果时,对“甩出”时每帧之间container的距离间隔设为比上一帧缩小,实现了每帧移动的距离慢慢减小,直到到达临界点停止。
当没有“甩出”效果时,deaccelerateScrolling仅执行一次,设置一次container的位置。