iOS實戰:動畫實戰-自定義下拉刷新控件

前言

本文是上一篇文章上手CAShapeLayer,動畫其實并不難 的實戰,用到的知識有CAShapeLayer、UIBezierPath和CABasicAnimation。如果對這些類不大了解,可先去基礎篇看看。

正文

這次要做的是一個簡單的下拉刷新控件,主要還是練習剛上手的動畫。

一、效果預覽

效果預覽.gif

二、動畫分析

隨著下拉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;該方法開始繪制的點是該圓最右邊的那個點。所以如果只是單純地用這個方法繪制圓的話,在下拉的時候會出現如下問題:

不旋轉會出現的問題.gif

所以需要旋轉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
    }
}

解釋:下拉一定像素后,設置topLayerstrokeEnd屬性,這樣就會有上面的效果啦。同時拉到一定程度后,開始旋轉的動畫并告訴控制器該刷新了。

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,動畫的KeyPathtransform.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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容