动手设计一个加载组件

好的动画效果会让用户将时间的概念忽略掉,会让人愿意去看动画的执行。观赏动画一方面能够抵消掉由于网络延迟或者视图处理缓慢照成的用户焦虑感,另一方面可以通过精美的动画效果吸引用户的眼球,提高用户留存。那么着手实现一个动画控件可以说是对基础数学计算的一个检验,从开发角度来说就是对动效的理解以及掌握程度的考验。

这篇文章主要是解析通过使用基础的Core Animation知识实现如上图所示的动画效果,并且将这个动画效果封装成为一个可复用的组件,最后在这个组件的基础上为其添加交互特性以及实现更加全面的加载效果。

解析动效原理

文章第一部分是要解析如何实现这样的一个动效。从一个动画映入眼帘就应该思考它是如何运作的,需要的知识有哪些,涉及的细节部分等等,在这之后通过自己对动效的想法去尝试着进行实现,对不足之处进行修改。

首先分析动画实现效果

通过观察可以发现,有三个不同颜色的点,四个位置,并且三个点在这四个位置间进行有规律的移动。仅此而已!

然后分析实现动画所需要得东西

在通过分析之后可以进行一个思考层面的演练,把需要的东西罗列出来。

  • 三个点可以使用CALayer来实现
  • 四个位置需要通过每一个点的大小以及加载控件的尺寸进行确定
  • 另外如果要是三个点有规律的移动,需要使用一个计时器进行控制位移的动画
  • 还有要对每一个位置进行一下标记,方便点的位移,这里使用枚举
接下来进行具体的实现

由上面可以知道,四个位置的确定离不开三个点的大小,而三个点的大小如果写死的话也是可以的,但是这里通过与组件的尺寸进行一个耦合使用等比变化大小,从而四个点也可以跟随着组件的尺寸进行合理的改变,使动画实现起来能够更加的均匀。

先确立点的大小,点是一个CALayer进行了切割之后形成的一个圆点,所以这里这么设置点

1
2
3
4
5
6
CGFloat dotRadius = width > height ? width / 13 : height / 13;
...
dot.masksToBounds = YES;
dot.cornerRadius = dotRadius;
dot.bounds = CGRectMake(0, 0, 2 * dotRadius, 2 * dotRadius);
...

对于四个位置的确定需要进行简单不绕弯的数学计算

1
2
3
4
_topPoint 	= CGRectMake(width/2 - dotRadius, 	height/4 - dotRadius, 	2*dotRadius, 2*dotRadius);
_rightPoint = CGRectMake(3*width/4 - dotRadius, height/2 - dotRadius, 2*dotRadius, 2*dotRadius);
_bottomPoint = CGRectMake(width/2 - dotRadius, 3*height/4 - dotRadius, 2*dotRadius, 2*dotRadius);
_leftPoint = CGRectMake(width/4 - dotRadius, height/2 - dotRadius, 2*dotRadius, 2*dotRadius);

然后将三个点选择四个位置中的三个进行frame初始化,挑一个其中的点为例

1
2
3
4
5
6
7
8
_firstDot = ({
_firstDot = [CALayer layer];
_firstDot.frame = _topPoint;
_firstDot.masksToBounds = YES;
_firstDot.cornerRadius = dotRadius;
_firstDot.bounds = CGRectMake(0, 0, 2 * dotRadius, 2 * dotRadius);
_firstDot;
});

对于点和位置的确定到这里就算实现了,下面要做的就是使用计时器让点在四个位置间进行规律动画。通过观察可以发现三个点在同一个时间,只有两个点动,另外的一个点不发生位移,那么这里就可以随便挑两个点先开始位移,需要注意的是,其中必须有一个点要穿过组件的中心点,然后另一个点不穿过中心点并且会成为下一个不位移的点,具体的一个周期效果图如下,

先确定计时器,通过计时器的循环方法进行位移动画的实现,对外提供一个API,被动的调用计时器

1
2
3
4
5
6
7
8
9
10
11
12
- (void)startLoading{

if (!_loading) {

_loading = YES;

_timer = [NSTimer scheduledTimerWithTimeInterval:ANIMATION_DURATION_SECS
target:self
selector:@selector(animationNextStep)
userInfo:nil repeats:YES];
}
}

通过效果图可以分析出来,所有的点状态一共有六种,但是使用一个五个选项的枚举就可以完成一个循环,枚举如下,

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSInteger ,HLLActivityIndicatorStep) {

HLLActivityIndicatorStepZero = 0,
HLLActivityIndicatorStepOne,
HLLActivityIndicatorStepTwo,
HLLActivityIndicatorStepThree,
HLLActivityIndicatorStepFour
};

在计时器的- animationNextStep方法中要做的就是在不同的枚举类型下进行点的位移动画,然后在移动完成之后枚举类型递进一位,大致如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void) animationNextStep{

switch (_stepNumber) {
case HLLActivityIndicatorStepZero:
{
[CATransaction begin];
[CATransaction setAnimationDuration:ANIMATION_DURATION_SECS];
...
[CATransaction commit];
break;
}
case HLLActivityIndicatorStepOne:
{
...
}
...
case HLLActivityIndicatorStepFour:
{
...
_stepNumber = HLLActivityIndicatorStepZero;
}
break;
default:
break;
}
_stepNumber ++;
}

有开始就要有结束,也是一个对外的API,里面的操作是要停止计时器、将三个点归位为初始的三个位置,然后将标志位枚举置为0。

1
2
3
4
5
6
7
8
9
10
11
12
13

- (void)stopLoading{

_loading = NO;

[self.timer invalidate];

_stepNumber = HLLActivityIndicatorStepZero;
[CATransaction begin];
[CATransaction setAnimationDuration:ANIMATION_DURATION_SECS];
...
[CATransaction commit];
}

实现一个网络加载处理的控制器类

上一部分仅仅是实现了一个简单的动画加载控件,可以单独的把他应用到项目中,也可以将他结合控制器很好的用于解决由于网络加载缓慢以及加载错误等等的友好显示。

一般来说一个网络请求从发出去到服务器响应请求并返回数据给客户端这一段时间是难以估计的,并且这段时间基本上UI界面是没有什么变化的,但是在另一个线程的网络方面,是有很大的变化的。对于用户体验来说一个可以用于提示用户网络请求加载的UI是在网络请求发送之后不可缺少的一部分,有的时候还可能出现网络超时的情况,并且网络请求有时候会有各种的300、400、500错误导致返回的不是客户端需要的数据,因此还需要能有很好的对错误响应的处理功能,基本的错误原因展示功能是必须的,其他的情况先不考虑。

结合需要一个网络等待的UI和能对错误信息进行操作这两个需求,下面做一个不是很好移植,但是很简单的网络加载控制器,姑且叫它 — NetworkingLoadingController。这是一个控制器,对于一个控制器要显示在另外一个正在展示的控制器上,除了模态导航之外还可以使用Segue,其实是使用了UIKit中一个比较少用到的类 — UIContainerView

这个类可以在控制器中占位,并且通过Segue连接一个视图控制器,视图控制器可以显示在UIContainerView占的位置上。但是这个类只能够在StoryBoard中创建以及使用,这就照成了以后要移植这个加载控制器不会很方便,但是他的创建很简单。既然他是通过Segue来连接一个占位视图和控制器的,所以在主控制器中可以通过-prepareForSegue:sender:方法获得到占位的控制器。

我的做法是使用一个UIContainerView占位,连接这个NetworkingLoadingController,然后NetworkingLoadingController提供一套UI显示:网络加载、错误处理、响应结果显示等等,其中在主视图控制器中可以获取到NetworkingLoadingController,并且进行相应的UI展示,另外NetworkingLoadingController中的交互事件可以通过代理模式与主试图控制器之间传递。

视图控制器间通信

对于不同之间的UI切换可以通过获取的NetworkingLoadingController进行主动的方法调用,使用代理-协议使两个视图控制器之间进行通信,首先要获得NetworkingLoadingController并且设置代理,具体的代理协议可以视情况而定,这里简单的设置一个,就是当出现请求超时的时候,NetworkingLoadingController提供一个重新尝试的点击操作,将这个操作传递给主视图控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// set protocol
@protocol NetworkLoadingViewDelegate <NSObject>

@optional;
-(void) retryRequestButtonWasPressed:(NetworkLoadingViewController *)viewController;
@end;

// get NetworkingLoadingController and set delegate
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

if ([segue.identifier isEqualToString:@"networkLoadingSegue"])
{
self.networkLoadingController = segue.destinationViewController;
self.networkLoadingController.delegate = self;
}
}

当NetworkingLoadingController的UI要响应一些点击事件的时候使用常规的代理方法就可以将事件传递给代理

1
2
3
4
5
6
7
8
- (IBAction)retryRequest:(id)sender
{
if (self.delegate && [self.delegate respondsToSelector:@selector(retryRequestButtonWasPressed:)])
{
[self.delegate retryRequestButtonWasPressed:self];
}
[self showLoadingView];
}

对于主动的进行UI切换,比如,在主视图控制器中的某一个操作要进行网络请求的时候,可以通过调用NetworkingLoadingController的方法,其中的loadContainerView就是UIContainerView。

1
2
self.loadContainerView.hidden = NO;
[self.networkLoadingController showLoadingView];

具体的-showLoadingView方法中的操作就是显示加载UI,隐藏错误处理以及其他无关UI。

加载UI部分

加载部分的UI由上面的加载控件以及一个标签视图组成,在NetworkingLoadingController要显示加载UI的时候开始加载控件的动画,隐藏的时候停止动画,标签仅仅是一个显示“Loading…”。UI如下,

错误处理UI部分

对于错误处理部分,第一是要能够显示错误信息,第二是能够让用户面对错误进行下一步操作,比如超时请求的重试,

展示响应UI部分

有的时候不仅仅需要有加载的UI和处理错误的UI,还需要将响应的结果进行一个展示,这时候大可以再添加一个视图用于展示相应的数据。

NetworkingLoadingController的详细UI结构

结合以上的UI展示可以看出来,通过显示隐藏不同的UI来进行响应不同的网络情况,那么NetworkingLoadingController的具体的UI结构如下,

以上的这些UI都是在NetworkingLoadingController的View上的,而NetworkingLoadingController又是根据UIContainerView占位显示的,所以在主视图控制器中添加UIContainerView的时候要将它添加在最后一个,也就是最前面,这样才能够覆盖全屏显示网络加载信息,当然也可以不覆盖全屏,但是一定要在最前面!!