新浪微博中的“加号按钮”点击后的弹出动画很有意思,每当一个人孤单寂寞冷的时候总会不停的点这个动画,终于忍不住自己撸了一个。废话不多说,直接上效果图:

首先说一下通过这个动画制作过程给大家分享的技术问题:
1.背景的毛玻璃效果
2.弹性动画
3.动画进行时对用户交互的处理
4.UIControl 的 event 类型
一、背景的毛玻璃效果
对新浪微博中弹出动画背景的思路分析:
新浪的背景动画效果是有一个透明渐变的过程,并且最终渐变停止之后显示的是一个带有毛玻璃(半透明模糊)效果的视图。我的模仿思路是当准备要弹出动画的时候对整个视图进行截屏操作,将截屏后的图片进行毛玻璃效果渲染,然后在视图上加一个背景 UIImageView 来显示这张图片,然后通过 alpha 动画来由透明逐渐渐变出来。
1.对视图进行截屏:
UIGraphics BeginImageContextWithOptions(size, NO, scale);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
2.截屏后加上毛玻璃效果:
制作毛玻璃效果有三种选择,一是 iOS8 后推出的 UIBlurEffect 和 UIVisualEffectView 来直接显示一个带有毛玻璃效果的 View 不过这种方式并不能直接生成一张带有毛玻璃效果的图片,而且它的模糊程度设置方法非常有限只有那几个枚举类型,无法满足需求。
第二种是通过 CoreImage 添加滤镜的方式来实现毛玻璃效果,不过这种方式有个缺点如果滤镜使用频繁会对主线程产生影响,如果我不断频繁的重复动画效果就必须要做判断看滤镜是否正在起作用,否则会经常出现崩溃和内存泄漏问题。
第三种我们使用苹果13年 WWDC 上发布的官方 sample 一个 UIImage 的分类 UIImage+ImageEffects.h ,它不但可以制作毛玻璃效果图片,而且可以调整模糊程度和颜色渲染。下面给出代码:
image = [image bhb_applyBlurWithRadius:15
tintColor:tintColor
saturationDeltaFactor:1
maskImage:nil];
最终的显示效果很不错,我将模糊程度尽量的调节到与新浪微博一致了,不过在这个过程中,我发现当我频繁的进行弹出操作时,内存会不断攀升如下图:

内存暴增的原因肯定是因为刚才的截图或者毛玻璃效果导致的,我们来用 Instruments 的 Allocations 来进行内存分析,找出元凶。
Xcode -> Product -> Profile -> Allocations, 开启之后我们来使用右下角的 Mark generation(内存快照功能,进一步了解请移步[这片文章][1])

在弹出 View 和移除 View 的两个时间点加 Mark generation。

可以观察到,两个时间点竟然相差了4.31M,这是一个相当恐怖的数字,怪不得我点击几次弹出功能之后内存会暴增,让我们继续跟踪:



跟踪到最后,我们发现大部分未释放的内存来自于绘图和位图的创建,回想我们当初做的截图操作,图片上下文开启后并没有进行关闭操作,所以在程序不断截图的过程中开启了无数的图片上下文而且不会被释放,添加下面这句关键的代码就可以解决问题(NC的我竟然连这个都忘了加-.-):
UIGraphicsEndImageContext()
二、弹性动画
新浪动画中,按钮弹出的动画为弹性效果,按钮到达最终位置后不会直接停止,而是做类似弹簧的一种阻尼运动,要实现这种动画也很简单 iOS7 后苹果非常给力的添加了 spring 弹性动画的快速创建方式:
[UIView animateWithDuration:(NSTimeInterval)
delay:(NSTimeInterval)
usingSpringWithDamping:(CGFloat)
initialSpringVelocity:(CGFloat)
options:(UIViewAnimationOptions)
animations:
completion:^(BOOL finished) {}]
通过对 Damping 阻力和 Velocity 初速度的设置可以实现弹性动画动画效果如下图:

当然实现弹性动画还可以使用[Jonathan Willing][2]大大的 JNWSpringAnimation ,你可以像使用 CABasicAnimation 一样轻松的使用它,通过改变关键的3个属性 Damping 阻力, stiffness 硬直, mass 质量来改变弹性动画的效果代码如下:
JNWSpringAnimation *scale = [JNWSpringAnimation
animationWithKeyPath:@"transform.translation.x"];
scale.damping = 7;
scale.stiffness = 7;
scale.mass = 1;
scale.fromValue = @(0);
scale.toValue = @(400);
[redBall.layer addAnimation:scale forKey:scale.keyPath];
redBall.transform = CGAffineTransformMakeTranslation(400, 0);
关键的三个属性对动画的影响如下图:

为了减轻项目对第三方框架的依赖,我使用了 iOS7 原生的 spring 动画,如果你想要兼容以前版本,替换成JNWSpringAnimation 即可。回到新浪动画的制作,动画是6个按钮按照顺序依次出现和消失,并且点击 more 按钮后可以向左平移到第二屏幕,并且在第二屏幕点击 叉号 按钮动画会加在第二屏幕的6个按钮上。
效果如图:

对此我使用了一个 UIScrollView 来承载这些按钮,并声明2个数组用来保存所有的按钮和正在显示的按钮(在屏幕上,并且需要做动画):
@property (nonatomic,strong) NSMutableArray * visableArray;
@property (nonatomic,strong) NSMutableArray * itemsArray;
这样我在给这些按钮加动画的时候就不会浪费性能,只把动画加在当前显示在屏幕的按钮上。动画依次按照一定的时间差来执行,解决的办法我是用的 ** :
[self.visableArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
BHBCustomBtn * btn = obj;
CGFloat x = btn.frame.origin.x;
CGFloat y = btn.frame.origin.y;
CGFloat width = btn.frame.size.width;
CGFloat height = btn.frame.size.height;
btn.frame = CGRectMake(x, [UIScreen mainScreen].bounds.size.height + y - self.frame.origin.y, width, height)
btn.alpha = 0.0
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(idx * 0.03 * NSEC_PER_SEC)), dispatch_get_main_queue(), completion:^(BOOL finished) {
if ([btn isEqual:[self.visableArray lastObject]]) {
self.superview.superview.userInteractionEnabled = YES;
}
}]
})
}]
这样按钮就会依次以弹性动画的形式弹动出来了,是不是很简单,对于动画结束后所作的处理我将在下一节中说明。
三、动画进行时对用户交互的处理
动画过程中,处理用户交互的问题相当关键,视图动画中默认自动停止响应用户交互,因此当按钮进行弹性动画时,触摸它并不会生成任何事件。
但是当你触摸加号按钮的时候,会再次进行弹出框动画,这时就会弹出两个弹出框,这是我们不希望看到的,我们可以将加号按钮的 enable 属性设置为 YES 但是这样做我们要在封装的视图内部获取外部的加号按钮,这一点违背了封装性原则,并不是一个好的设计。所以我在所有按钮的弹性动画开始时,设置:
self.superview