属于自己的刷新控件
记得在我这篇文章中我说过自己要做一个属于自己的刷新控件,为了生动,自己开发中快速方便使用,而且还不依赖人家三方库。之前也说过由于公司环境原因,可能并不会有那么多时间去做所谓与公司无关的事,不过幸运的是在上周四的时候就完成了,只是还有一些细节需要去修改而已。在这里就分享一下心得吧。
在分享之前先看看效果,说起来惭愧,现目前还是只有弹性波浪动画下拉刷新,之前说的目的是要集成多款各式各样的酷炫动画的下拉刷新(由于时间原因,之后还是会渐渐加入其它效果)。上拉加载更多 的话就是直接使用的指示器,因为笔者喜欢简洁风格。效果大概就是下面这样的��:
关于弹性波浪动画,实际就是shaperLayer根据贝塞尔曲线绘制出来的,关于弹性波浪动画可以看我这篇文章,不过有一点瑕疵,我之前认为贝塞尔曲线的控制点就是点,直接用Point表示就OK,然而在最后想要在结束的时候用上弹性阻尼动画的时候却是出了问题,我写了如下的代码测试:
let shapeLayer = CAShapeLayer()
let bezierPath = UIBezierPath()
let bezierPath1 = UIBezierPath()
shapeLayer.fillColor = UIColor.cyan.cgColor
bezierPath.move(to: CGPoint.zero)
bezierPath.addLine(to: CGPoint(x: 0, y: 50))
bezierPath.addCurve(to: CGPoint(x: 140.935, y: 50), controlPoint1: CGPoint(x: 0, y: 50), controlPoint2: CGPoint(x: 87.340, y: 50))
bezierPath.addCurve(to: CGPoint(x: 250.578, y: 50), controlPoint1: CGPoint(x: 198.5, y: 50), controlPoint2: CGPoint(x: 250.578, y: 50))
bezierPath.addCurve(to: CGPoint(x: 375, y: 50), controlPoint1: CGPoint(x: 250.57, y: 50), controlPoint2: CGPoint(x: 299.064, y: 50))
bezierPath.addLine(to: CGPoint(x: 375, y: 0))
bezierPath.close()
bezierPath1.move(to: CGPoint.zero)
bezierPath1.addLine(to: CGPoint(x: 0, y: 81.5))
bezierPath1.addCurve(to: CGPoint(x: 140.935, y: 139.099), controlPoint1: CGPoint(x: 0, y: 81.5), controlPoint2: CGPoint(x: 87.340, y: 81.5))
bezierPath1.addCurve(to: CGPoint(x: 250.578, y: 139.09), controlPoint1: CGPoint(x: 198.5, y: 203.90), controlPoint2: CGPoint(x: 250.578, y: 139.09))
bezierPath1.addCurve(to: CGPoint(x: 375, y: 81.5), controlPoint1: CGPoint(x: 250.57, y: 139.09), controlPoint2: CGPoint(x: 299.064, y: 81.5))
bezierPath1.addLine(to: CGPoint(x: 375, y: 0))
bezierPath1.close()
shapeLayer.path = bezierPath1.cgPath
self.view.layer.addSublayer(shapeLayer)
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: [], animations: {
shapeLayer.path = bezierPath.cgPath
}) { (finish) in
}
}
结果根本就没有酷炫的反弹效果,一时也是内心沮丧,原来是我天真的以为spring动画会自动为我分解出阻尼运动的动画帧,其实CALayer的path 这个value是不会支持被spring动画创建阻尼运动帧的,想要实现弹跳的效果,就只能CoreAnimation(keyFrameAnimation)去计算创建出阻尼运动的关键帧,但是这样的难度太大,也很复杂啊。最后才想到之前PR的库里面人家用的就是View的center来做贝塞尔曲线的,先前还觉得平白无故增加这么多空闲view到界面上不太友好,现在才知道其中的妙用,因为view的center是支持spring动画的呀。(这个issue先记这里,随后修改),笔者认为这个刷新库最大的问题就是动画啦,在解决动画之后的话,其他实现就比较方便了。首先刷新控件的关键就是对滚动视图的扩展,利用KVO监测滚动视图的frame,contentInset,contentSize等。最核心的代码也就是KVO了:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if([keyPath isEqualToString:@"contentOffset"]){
if (self.scrollerView.isDragging && self.refreshState == HCDRefreshStateNone) {
self->_refreshState = HCDRefreshStatePulling;
}
if (self.refreshState == HCDRefreshStateLoading) {
return;
}else if (self.refreshState == HCDRefreshStatePulling ){
if ([self realOffsetY] < 0) {
return;
}else{
[self.activityIndicatorView startAnimating];
}
self.frame = CGRectMake(0, (self.scrollerView.contentSize.height < self.scrollerView.bounds.size.height)? self.scrollerView.bounds.size.height : self.scrollerView.contentSize.height, self.scrollerView.bounds.size.width, kLoadingInset);
[self layoutSubviews];
}
}else if([keyPath isEqualToString:@"contentSize"]) {
[self layoutSubviews];
}else if([keyPath isEqualToString:@"frame"]){
[self layoutSubviews];
}else if([keyPath isEqualToString:@"panGestureRecognizer.state"]){
if (self.scrollerView.panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
[self.scrollerView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}else if (self.scrollerView.panGestureRecognizer.state == UIGestureRecognizerStateChanged){
}else if(self.scrollerView.panGestureRecognizer.state == UIGestureRecognizerStateCancelled || self.scrollerView.panGestureRecognizer.state == UIGestureRecognizerStateEnded){
if (self.refreshState == HCDRefreshStatePulling) { //从拖拽中结束
//向下拉
if([self realOffsetY] < 0){
self->_refreshState = HCDRefreshStateNone;
return;
}else{
//向上拉
[self.activityIndicatorView startAnimating];
if ([self realOffsetY] - (self.scrollerView.contentSize.height - self.scrollerView.bounds.size.height) > kMineDistanceToRefresh) {
//进入刷新状态 执行相应动画
self->_refreshState = HCDRefreshStateLoading;
[UIView animateWithDuration:0.25 animations:^{
self.scrollerView.contentInset = UIEdgeInsetsMake(self.originInsets.top, self.originInsets.left, self.originInsets.bottom + kLoadingInset, self.originInsets.right);
self.frame = CGRectMake(0, (self.scrollerView.contentSize.height < self.scrollerView.bounds.size.height)? self.scrollerView.bounds.size.height : self.scrollerView.contentSize.height, self.scrollerView.bounds.size.width, kLoadingInset);
} completion:^(BOOL finished) {
[self.scrollerView removeObserver:self forKeyPath:@"contentOffset"];
self->_refreshState = HCDRefreshStateLoading;
if (self.actionHandler) {
self.actionHandler();
}
}];
}else{
//未到下拉成功的幅度 执行动画还原
[UIView animateWithDuration:0.25 animations:^{
self.scrollerView.contentInset = self.originInsets;
self.frame = CGRectMake(0, (self.scrollerView.contentSize.height < self.scrollerView.bounds.size.height)? self.scrollerView.bounds.size.height : self.scrollerView.contentSize.height, self.scrollerView.bounds.size.width, kLoadingInset);
} completion:^(BOOL finished) {
[self.activityIndicatorView stopAnimating];
}];
}
}
}
}
}else if ([keyPath isEqualToString:@"contentInset"]){
}
}
另外还有一个关键点就是KVO什么时候添加,什么时候移除的问题,在这个问题上参考了MJRefresh。上拉加载更多的话,高度什么的都是固定的,写一个刷新控件主要问题在于滚动试图滚动时很多细节的处理。更多源码请看这里
如您发现任何错误,欢迎留言指正~