前言
本文是上一篇文章上手CAShapeLayer,动画其实并不难 的实战,用到的知识有CAShapeLayer、UIBezierPath和CABasicAnimation。如果对这些类不大了解,可先去基础篇看看。
正文
这次要做的是一个简单的下拉刷新控件,主要还是练习刚上手的动画。
一、效果预览
二、动画分析
随着下拉tableView,蓝色的图层的填充比会越来越大,直到充满,开始刷新。蓝色的圆弧开始旋转。
三、开始代码
1.新建自定义刷新控件
首先新建自定义刷新控件,继承自UIView。然后给这个类添加属性和方法。这里思考传递事件的方法(也就是当下拉到一定程度告诉控制器该刷新了)。我这里选用的是
- (id)performSelector:(SEL)aSelector;
方法来传递事件的。当然也可以用block或者delegate。所以:
@interface YQRefreshHeadView : UIView
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL action;
@property (nonatomic, weak) UIScrollView *scrollView;
- (void)startAnimation;
- (void)endAnimation;
@end
2.新建UIScrollView的拓展类
为了能够更加方便,我新建了UIScrollView的拓展类。拓展类代码如下:
.h:
@interface UIScrollView (YQRefreshHeadView)
- (YQRefreshHeadView *)attachRefreshHeadViewWithTarget:(id)target action:(SEL)action;
@end
.m:
- (YQRefreshHeadView *)attachRefreshHeadViewWithTarget:(id)target action:(SEL)action
{
YQRefreshHeadView *headView = [[YQRefreshHeadView alloc] initWithFrame:CGRectMake(([UIScreen mainScreen].bounds.size.width - 50)/2, -50, 50, 50)];
headView.target = target;
headView.action = action;
headView.scrollView = self;
[self addSubview:headView];
return headView;
}
这样在使用刷新控件的时候,直接这样:
self.rhv = [tableView attachRefreshHeadViewWithTarget:self action:@selector(reloadData)];
这样就绑定上去了,当然需要一个返回值,毕竟刷新完成后要让它停止转动的,就像这样:[self.rhv endAnimation];
3.设置好控件里的layer
重写- (instancetype)initWithFrame:(CGRect)frame
方法,并在其中设置layer。
- (CAShapeLayer *)bottomLayer
{
if (_bottomLayer == nil) {
_bottomLayer = [CAShapeLayer layer];
_bottomLayer.fillColor = [UIColor clearColor].CGColor;
_bottomLayer.strokeColor = [UIColor lightGrayColor].CGColor;
_bottomLayer.lineCap = kCALineCapRound;
_bottomLayer.lineJoin = kCALineJoinRound;
_bottomLayer.lineWidth = 2;
_bottomLayer.frame = CGRectMake(self.bounds.size.height*0.2, self.bounds.size.height*0.2, self.bounds.size.height*0.6, self.bounds.size.height*0.6);
_bottomLayer.path = [UIBezierPath bezierPathWithOvalInRect:_bottomLayer.bounds].CGPath;
}
return _bottomLayer;
}
- (CAShapeLayer *)topLayer
{
if (!_topLayer) {
_topLayer = [CAShapeLayer layer];
_topLayer.fillColor = [UIColor clearColor].CGColor;
_topLayer.strokeColor = [UIColor colorWithRed:0.0431 green:0.7569 blue:0.9412 alpha:1.0].CGColor;
_topLayer.lineCap = kCALineCapRound;
_topLayer.lineJoin = kCALineJoinRound;
_topLayer.lineWidth = 2;
_topLayer.frame = self.bottomLayer.frame;
_topLayer.path = [UIBezierPath bezierPathWithOvalInRect:_topLayer.bounds].CGPath;
[_topLayer setValue:@(-M_PI_2) forKeyPath:@"transform.rotation.z"];
_topLayer.strokeEnd = 0;
}
return _topLayer;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
[self initViews];
}
return self;
}
- (void)initViews
{
[self.layer addSublayer:self.bottomLayer];
[self.layer addSublayer:self.topLayer];
}
注意!!!
这里有一个要注意的。+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
该方法开始绘制的点是该圆最右边的那个点。所以如果只是单纯地用这个方法绘制圆的话,在下拉的时候会出现如下问题:
所以需要旋转topLayer。
[_topLayer setValue:@(-M_PI_2) forKeyPath:@"transform.rotation.z"];
这样看上去,这个圆的开始绘制点就在最顶端那点了。
4.设置监听并写监听方法
在刚开始写时,我遇到了问题。原本我打算在- (instancetype)initWithFrame:(CGRect)frame
方法里添加监听的。就像这样:
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
[self initViews];
[self.scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionInitial context:nil];
}
return self;
}
- (void)initViews
{
[self.layer addSublayer:self.bottomLayer];
[self.layer addSublayer:self.topLayer];
}
但是没有效果。原因是设置监听时self.scrollView
还是空的。原因还得看UIScrollView
拓展类:
- (YQRefreshHeadView *)attachRefreshHeadViewWithTarget:(id)target action:(SEL)action
{
YQRefreshHeadView *headView = [[YQRefreshHeadView alloc] initWithFrame:CGRectMake(([UIScreen mainScreen].bounds.size.width - 50)/2, -50, 50, 50)];
headView.target = target;
headView.action = action;
headView.scrollView = self;
[self addSubview:headView];
return headView;
}
在执行initWithFrame
的时候,scrollView
还没有设置上去。
我的解决方法是,加一个- (void)addObserver;
公共方法,在初始化控件并设置完属性后,手动添加监听。
该方法实现:
- (void)addObserver
{
[self.scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionInitial context:nil];
}
这时的scrollView已经设置上去了。下面是监听对应的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
UIScrollView *scrollView = object;
CGFloat offsetY = scrollView.contentOffset.y;
if (offsetY > -30) {
return;
}
// 向下拖拽时计算圆弧的结束值
CGFloat dragProgress = MIN(fabs(offsetY+30)/50, 1);
NSLog(@"%f", dragProgress);
CGFloat strokeEnd = dragProgress;
self.topLayer.strokeEnd = strokeEnd;
// 满足刷新条件
if (!scrollView.isDragging && fabs(offsetY+30)/50>1) {
[scrollView setContentOffset:CGPointMake(0, -80) animated:NO];
[self startAnimation];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.action];
#pragma clang diagnostic pop
}
}
解释:下拉一定像素后,设置topLayer
的strokeEnd
属性,这样就会有上面的效果啦。同时拉到一定程度后,开始旋转的动画并告诉控制器该刷新了。
5.写开始动画和结束动画方法
- (void)startAnimation
{
self.topLayer.strokeEnd = 0.2;
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.toValue = @(M_PI * 2 *0.72);
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
rotationAnimation.duration = 2;
rotationAnimation.repeatCount = HUGE;
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.removedOnCompletion = NO;
[self.topLayer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}
这里用的是CABasicAnimation
,动画的KeyPath
为transform.rotation.z
。
结束动画方法:
- (void)endAnimation
{
[self.topLayer removeAllAnimations];
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
}
这样,最简单的刷新控件就完成了啦。看看如何调用:
- (void)viewDidLoad
{
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height - self.navigationController.navigationBar.bounds.size.height - 20) style:UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
tableView.backgroundColor = [UIColor clearColor];
[self.view addSubview:tableView];
self.tableView = tableView;
self.rhv = [tableView attachRefreshHeadViewWithTarget:self action:@selector(reloadData)];
}
#pragma mark - 内部方法
- (void)reloadData
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.rhv endAnimation];
});
}
最后
本文的github地址:https://github.com/JabberYQ/YQRefreshHeadViewDemo