寫在前面
“什么值得買”是我這種剁手族常用的軟件,最近發(fā)現它的下拉刷新做得挺好的,而且也算是一種經常見到的樣式,正好前幾天剛好分析完了MJRefresh,趁熱打鐵,這次就來嘗試實現一下它的下拉刷新吧。
一、總體構成

從上圖可以看出,RefreshView主要是兩部分組成:
- 位于上方的Label
- 位于下方的ImageView
Label就不多介紹了,我們來重點看一下Image部分。
- 下拉過程中,Circle可以隨著我們下拉的位移量改變
- 刷新過程中,缺了一角的Circle會圍繞“值”旋轉
- 刷新完畢后,動畫結束
實現難度不大,下面我們就開始動手吧。
二、刷新部分

最后的效果大概就是這個樣子的,還算合格,我們來詳細分析下。
(一)資源
提取ipa包內圖片資源的方法有很多,而我這人比較懶,所以喜歡直接用工具,這里推薦給大家一款我一直用的:iOS-Images-Extractor,國內的某Coder寫的,很好用,分享給大家,好用的話別忘了點個星,是給作者最大的鼓舞。

使用方法很簡單,把ipa包拖進去,點擊start等待分析完成,之后點擊Output Dir就會自動跳轉到輸出目錄。
OK,工具介紹完,我已經把圖片找出來了,一共倆:

看到這倆角色,就明了了,一開始我以為缺了一塊的Circle是用ShapeLayer畫的,原來是美工做的,那就直接用吧,省事。
(二)動畫實現
圖片"zhi"在下,"circle"在上,然后對circle做旋轉動畫就OK了。
- (void)viewDidLoad {
[super viewDidLoad];
[self.logoView addSubview:self.circleView];
[self.view addSubview:self.logoView];
}
- (void)viewDidLayoutSubviews{
self.logoView.center = self.view.center;
self.logoView.bounds = CGRectMake(0, 0, 30, 30);
self.circleView.frame = self.logoView.bounds;
}
- (void)viewDidAppear:(BOOL)animated{
[self.circleView.layer addAnimation:[self getTransformAnimation] forKey:nil];
}
-(CABasicAnimation *)getTransformAnimation{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; //指定對transform.rotation屬性做動畫
animation.duration = 2.0f; //設定動畫持續(xù)時間
animation.byValue = @(M_PI*2); //設定旋轉角度,單位是弧度
animation.fillMode = kCAFillModeForwards;//設定動畫結束后,不恢復初始狀態(tài)之設置一
animation.repeatCount = 1000;//設定動畫執(zhí)行次數
animation.removedOnCompletion = NO;//設定動畫結束后,不恢復初始狀態(tài)之設置二
return animation;
}
- (UIImageView *)logoView{
if (!_logoView) {
_logoView = [[UIImageView alloc] init];
_logoView.image = [UIImage imageNamed:@"zhi"];
}
return _logoView;
}
- (UIImageView *)circleView{
if (!_circleView) {
_circleView = [[UIImageView alloc] init];
_circleView.image = [UIImage imageNamed:@"circle"];
}
return _circleView;
}
很簡單,主要就是動畫部分,如果對動畫不熟悉的童鞋,推薦ios核心動畫高級技巧。
三、下拉部分
這部分,主要是要實現Circle隨我們手勢改變自身完成度,先上效果圖:

(一) 用ShapeLayer畫個圓:
這里的Circle部分,我們用CAShapeLayer來做:
CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪制的圖形,最后CAShapeLayer就自動渲染出來了。
形象點來說,就是你給CAShapeLayer指定腳本(Path),并設定好各屬性(Color,Width)之后,CAShapeLayer就自動完成了。
-(CAShapeLayer *)getShape{
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:self.logoView.bounds];//先寫劇本
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = path.CGPath;//安排劇本
shapeLayer.fillColor = [UIColor clearColor].CGColor;//填充色要為透明,不然會遮擋下面的圖層
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.lineWidth = 1.0;
shapeLayer.frame = self.logoView.bounds;
return shapeLayer;
}
- (void)viewDidAppear:(BOOL)animated{
[self.logoView.layer addSublayer:[self getShape]]; //將ShapeLayer圖層增加到logoView上
}

(二)控制ShapeLayer的繪制進度
圓畫完了,下面是和Slider.value關聯,讓我們能控制圓的繪制進度。
關鍵屬性:strokeStart,strokeEnd
- strokeStart:從哪開始繪制
- strokeEnd:在哪結束繪制
我們設定我們的圓起始點為:
shapeLayer.strokeStart = 0;
shapeLayer.strokeEnd = 0.9;

可以出,stroke屬性的特點:
- 單位是百分比
- 0點在Layer右側中心
- 順時針繪制
有了這個屬性,我們就可以很方便的實現我們的目標了。
我們把strokeEnd的初始值設為0,再與我們的Slider.value掛鉤就好了。
完整代碼:
- (void)viewDidAppear:(BOOL)animated{
[self.logoView.layer addSublayer:self.circleLayer];
}
- (CALayer *)circleLayer{
if (!_circleLayer) {
_circleLayer = [self getShape];
}
return _circleLayer;
}
- (CAShapeLayer *)getShape{
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:self.logoView.bounds];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.lineWidth = 1.0;
shapeLayer.path = path.CGPath;
shapeLayer.frame = self.logoView.bounds;
shapeLayer.strokeEnd = 0;
return shapeLayer;
}
- (IBAction)didSlide:(UISlider *)sender {
self.circleLayer.strokeEnd = sender.value;
}
四、自定義MJRefresh
經過上面兩個步驟,我們已經實現了下拉刷新的核心視圖動畫,接下來該自定義MJRefresh了。
老規(guī)矩,先上完成圖:

自定義MJRefreshHeader,需要繼承自MJRefreshHeader,看過我之前文章的小伙伴一定很熟悉了。
不熟悉也不要緊,不過就有點死記硬背的感覺了。
(一)布局
#pragma mark - Const
CGRect kZZZLogoViewBounds = {0,0,25,25};
#pragma mark 在這里做一些初始化配置(比如添加子控件)
- (void)prepare
{
[super prepare];
[self.logoView addSubview:self.circleView];
[self.logoView.layer addSublayer:self.circleLayer];
[self addSubview:self.logoView];
}
#pragma mark 在這里設置子控件的位置和尺寸
- (void)placeSubviews
{
[super placeSubviews];
self.logoView.center = CGPointMake(self.mj_w/2.0, self.mj_h/2.0 + 10.0);// +10是為了logoView在中心點往下一點的位置,方便觀看
self.logoView.bounds = kZZZLogoViewBounds;
self.circleView.frame = self.logoView.bounds;
}
#pragma mark - setter & getter
- (UIImageView *)logoView{
if (!_logoView) {
_logoView = [[UIImageView alloc] init];
_logoView.image = [UIImage imageNamed:@"zhi"];
}
return _logoView;
}
- (UIImageView *)circleView{
if (!_circleView) {
_circleView = [[UIImageView alloc] init];
_circleView.image = [UIImage imageNamed:@"circle"];
_circleLayer.hidden = YES; //刷新時候的圖片,開始的時候不需要顯示出來
}
return _circleView;
}
- (CAShapeLayer *)circleLayer{
if (!_circleLayer) {
_circleLayer = [self creatCircleShapeLayerWithBounds:kZZZLogoViewBounds];//跟上面的getShapeLayer方法一樣,不過這里我稍微改寫了原函數,減少依賴
}
return _circleLayer;
}
有幾點需要說明的:
- MJRefresh默認高度是54,如需修改,放在prepare文件中即可:self.mj_h = **
- prepare方法中,不能放布局相關的內容,因為調用prepare是在視圖初始化的時候,這時候MJRefresh還沒有加入到View Hierarchy
-
placeSubViews方法中,注意MJRefreshView的Frame.origin = (0, -self.mj_h),所以調整Y值的時候注意正負。
布局
自定義的時候,慢慢來,出了BUG一般是Frame沒設置好,多利用調試工具。
(二)設置動態(tài)響應
我們只需要做兩件事情:
(1)將下拉位移量與我們的strokeEnd屬性關聯
關聯這件事情,MJRefresh已經幫我們處理了前半部分,我們只需要在相應方法里寫個等式就可以了。
?
(2) 處理狀態(tài)
- Idle :我們要設置各個組件是否隱藏
- Pulling: 不需要處理
- Refreshing:把CircleLayer隱藏,把CircleView顯示并做旋轉動畫
注意的是,我們的需要在endRefreshing方法中,手動移除動畫(因為我們在動畫定義部分為了動畫的流暢性,設置了animation.removedOnCompletion = NO),不然CircleView上的動畫會一直運行。
#pragma mark 監(jiān)聽控件的刷新狀態(tài)
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState;
switch (state) {
case MJRefreshStateIdle:
self.circleView.hidden = YES;
self.circleLayer.hidden = NO;
break;
case MJRefreshStatePulling:
break;
case MJRefreshStateRefreshing:
self.circleView.hidden = NO;
self.circleLayer.hidden = YES;
[self.circleView.layer addAnimation:[self creatTransformAnimation] forKey:nil];
break;
default:
break;
}
}
- (void)setPullingPercent:(CGFloat)pullingPercent
{
self.circleLayer.strokeEnd = pullingPercent;
}
- (void)endRefreshing{
[self.circleView.layer removeAllAnimations];
[super endRefreshing];
}
來看一下運行結果:

對比原版,貌似有幾點問題:
- Refreshing狀態(tài)的時候,CircleLayer的消失做了一個動畫
- Refreshing結束的時候,CirCleLayer因為和PullingPersent的關聯,strokeEnd直接設為了0
有問題,就解決問題唄。
第一個問題
self.circleLayer.hidden = YES;
問題出在這行代碼上。
這涉及到了CoreAnimation的隱式動畫部分,說白了,你對Layer做的屬性修改,會觸發(fā)系統(tǒng)的隱藏動畫,所以我們取消系統(tǒng)隱藏動畫就好了。取消方法如下:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.circleLayer.hidden = YES;
[CATransaction commit];

好的,這個問題已經不是問題了。
第二個問題
原廠的動畫是,刷新完成之后,CircleLayer要保持StrokeEnd = 1.0的狀態(tài)。
也就是說,需要個參數,能區(qū)分進入Idle狀態(tài)之前是否刷新過,那我們就加個參數唄。
改動部分代碼如下:
- (void)prepare
{
[super prepare];
[self.logoView addSubview:self.circleView];
[self.logoView.layer addSublayer:self.circleLayer];
[self addSubview:self.logoView];
self.hasRefreshed = NO;//初始化的時候,肯定是沒有刷新過的
}
#pragma mark 監(jiān)聽控件的刷新狀態(tài)
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState;
switch (state) {
case MJRefreshStateIdle:
self.circleView.hidden = YES;
self.circleLayer.hidden = NO;
break;
case MJRefreshStatePulling:
break;
case MJRefreshStateRefreshing:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.circleLayer.hidden = YES;
[CATransaction commit];
self.circleView.hidden = NO;
[self.circleView.layer addAnimation:[self creatTransformAnimation] forKey:nil];
self.hasRefreshed = YES;//刷新過了
break;
default:
break;
}
}
#pragma mark 監(jiān)聽拖拽比例(控件被拖出來的比例)
- (void)setPullingPercent:(CGFloat)pullingPercent
{
if (self.hasRefreshed) {//刷新返回的時候,strokeEnd = 1.0
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.circleLayer.strokeEnd = 1.0;
[CATransaction commit];
self.hasRefreshed = NO;//重置狀態(tài)為未刷新
}else{
self.circleLayer.strokeEnd = pullingPercent;
}
}
搞定。
總結
MJRefresh給我們提供了很好的底層實現,我們可以在它的基礎上,進行豐富的自定義,基本都能滿足自己的需求。
哪怕是實在滿足不了你了,也可以借鑒MJRefresh的整體思路,自己寫一個簡單的框架。
我在分析完MJRefresh的技術細節(jié)之后,不再感覺自己面對的是一個黑匣子,修改起來是相當地輕松。
所以,讀源碼果然是提高自己技術水平的有效手段(就是有點累)。
至此,MJRefresh的旅程就算結束了。
希望大家以后都能做出獨具個性的刷新控件。
