前言
本文是上一篇文章上手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