开始之前
创建基本动画
这效果是从MOV文件转成GIF的,而且CSDN不支持大于2M的图片上传,优酷地址
创建一个Single View Application工程,再创建一个Swift文件,我创建的叫“PulsingRadarView”,目前结构为:
在ViewController里面持有一个Optional的PulsingRadarView的属性,表示可以为nil,然后在viewDidLoad里做一个简单的初始化工作:
class ViewController: UIViewController { var radarView: PulsingRadarView? override func viewDidLoad() { super.viewDidLoad() let radarSize = CGSizeMake(self.view.bounds.size.width, self.view.bounds.size.width) radarView = PulsingRadarView(frame: CGRectMake(0,(self.view.bounds.size.height-radarSize.height)/2, radarSize.width,radarSize.height)) self.view.addSubview(radarView) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }雷达是圆形的,所以宽高都是self.view.bounds.size.width。
PulsingRadarView里面现在应该是空的,我们首先导入QuartzCore,因为后面动画部分会用到CALayer,然后重写drawRect方法:
override func drawRect(rect: CGRect) { UIColor.whiteColor().setFill() UIRectFill(rect) let pulsingCount = 6 let animationDuration: Double = 4 var animationLayer = CALayer() for var i = 0; i < pulsingCount; i++ { var pulsingLayer = CALayer() pulsingLayer.frame = CGRectMake(0, 0, rect.size.width, rect.size.height) pulsingLayer.borderColor = UIColor.grayColor().CGColor pulsingLayer.borderWidth = 1 pulsingLayer.cornerRadius = rect.size.height / 2 var defaultCurve = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault) var animationGroup = CAAnimationGroup() animationGroup.fillMode = kCAFillModeBackwards animationGroup.beginTime = CACurrentMediaTime() + Double(i) * animationDuration / Double(pulsingCount) animationGroup.duration = animationDuration animationGroup.repeatCount = HUGE animationGroup.timingFunction = defaultCurve var scaleAnimation = CABasicAnimation(keyPath: "transform.scale") scaleAnimation.autoreverses = false scaleAnimation.fromValue = Double(0) scaleAnimation.toValue = Double(1.5) var opacityAnimation = CAKeyframeAnimation(keyPath: "opacity") opacityAnimation.values = [Double(1),Double(0.7),Double(0)] opacityAnimation.keyTimes = [Double(0),Double(0.5),Double(1)] animationGroup.animations = [scaleAnimation,opacityAnimation] pulsingLayer.addAnimation(animationGroup, forKey: "pulsing") animationLayer.addSublayer(pulsingLayer) } self.layer.addSublayer(animationLayer) }
先设置画布的背影色为白色,pulsingCount表示波形的条数,animationDuration表示动画的时长,然后我创建了一个animationLayer来存放所有的动画Layer------pulsingLayer,这样layer的结构看起来就像:
每个pulsingLayer代表一个圆形,循环里面先对pulsingLayer进行一些初始化工作:设置frame、边框颜色、边框大小以及radius(半径),radius自然就是自身的宽或高的一半。
CAMediaTimingFunction稍后再说。
接下来创建一个AnimationGroup,因为我们需要用到的动画将有两个:scale(缩放)、opacity(透明),而且需要控制动画开始的时间。
我们借用Controlling Animation Timing这篇文章中的几张图来说明fillMode、beginTime这两个属性:
以下每个方格代表1秒钟,下面这张图也就代表4秒钟,动画时间为1.5秒,黄色为动画开始,蓝色为动画结束,黄色到蓝色也就是动画的过程。从图中可以看到,蓝色部分结束后就是白色了,也就代表整个动画结束并且从layer上移除。
下面这张图开始动画时间偏移了1秒,其余不变。
默认情况下,所有的Layer无论创建的先后顺序有何不同,它们的时间线都是一致的,beginTime为0,表示加入Layer之后就立即开始动画(或者说在当前时间播放动画),而如果要偏移1秒(如上图),则要CACurrentMediaTime()+1,获取当前系统的绝对时间(秒数)并+1。我们要实现脉冲效果,就要使每一个animationGroup的动画以不同的beginTime来进行,所以要设置beginTime = CACurrentMediaTime() + Double(i) * animationDuration / Double(pulsingCount),Swift不支持隐式类型转换,用Double()显式的强转一下。
但是通过上图可以看到,偏移后动画开始前有一个空档,这是由fillMode决定的:
- kCAFillModeRemoved 默认值,在动画开始前和动画结束后,动画对layer都没有影响,layer原本是什么样就是什么样
- kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态
- kCAFillModeBackwards 和kCAFillModeForwards相对,具体参考上面的
- kCAFillModeBoth kCAFillModeForwards和kCAFillModeBackwards在一起的效果
- kCAMediaTimingFunctionLinear 线性,即匀速
- kCAMediaTimingFunctionEaseIn 先慢后快
- kCAMediaTimingFunctionEaseOut 先快后慢
- kCAMediaTimingFunctionEaseInEaseOut 先慢后快再慢
- kCAMediaTimingFunctionDefault 实际效果是在动画开始时和动画播放时比较快,将结束时会变慢
动态增减元素
这效果是从MOV文件转成GIF的,而且CSDN不支持大于2M的图片上传,优酷地址
class PulsingRadarView: UIView { let itemSize = CGSizeMake(44, 44) var items = NSMutableArray()
public func addOrReplaceItem() { let maxCount = 10 var radarButton = UIButton(frame: CGRectMake(0, 0, itemSize.width, itemSize.height)) radarButton.setImage(UIImage(named: "UK"), forState: UIControlState.Normal) var center = generateCenterPointInRadar() radarButton.center = CGPointMake(center.x, center.y) self.addSubview(radarButton) items.addObject(radarButton) if items.count > maxCount { var view = items.objectAtIndex(0) as UIView view.removeFromSuperview() items.removeObject(view) } }
在一个平面直角坐标系中,以原点为圆心,1 为半径画一个圆,这个圆交 x 轴于 A 点。以 O 为旋转中心,将 A 点逆时针旋转一定的角度α至 B 点,设此时 B 点的坐标是(x,y),那么此时 y 的值就叫做α的正弦,记作 sinα;此时 x 的值就叫做α的余弦,记作 cosα;y 与 x 的比值 y/x 就叫做α的正切,记作 tanα。
private func generateCenterPointInRadar() -> CGPoint{ var angle = Double(arc4random()) % 360 var radius = Double(arc4random()) % (Double)((self.bounds.size.width - itemSize.width)/2) var x = cos(angle) * radius var y = sin(angle) * radius return CGPointMake(CGFloat(x) + self.bounds.size.width / 2, CGFloat(y) + self.bounds.size.height / 2) }我们先在360°以内随机生成一个角度(θ),然后在半径范围内随机生成一个值,就当作是一个新的半径r,利用公式我们得到了x、y的点,有圆心(a,b)为辅助,就能生成一个坐标了,这个坐标在返回时就已经是基于圆心的了,所以在addOrReplaceItem这个接口里我们拿到坐标后就能直接当作center来用了,这实际上也是完全采用的公式的算法。
NSTimer.scheduledTimerWithTimeInterval(0.5, target: radarView, selector: Selector("addOrReplaceItem"), userInfo: nil, repeats: true)Timer在不用的时候一定要调用invalidate()方法,并且要在ViewController析构之前,不然ViewController不会被释放,也就永远不会被析构。这里我们就不考虑那么多了,毕竟只有一个页面,而且在真实场景里也不会这么去用,更多的情况是在网络请求回调的时候去处理。
优化
优化一
private func itemFrameIntersectsInOtherItem (frame: CGRect) -> Bool { for item in items { if CGRectIntersectsRect(item.frame, frame) { return true } } return false }接收一个frame,然后和每一个item比较,如果重叠返回true,反之则返回false。
... do { var center = generateCenterPointInRadar() radarButton.center = CGPointMake(center.x, center.y) } while (itemFrameIntersectsInOtherItem(radarButton.frame)) ...把设置center的地方用一个do-while循环包装起来即可。这么一来,生成的元素就不会重叠了。
优化二
class PRButton: UIButton { init(frame: CGRect) { super.init(frame: frame) self.alpha = 0 } override func didMoveToWindow() { super.didMoveToWindow() if self.window { UIView.animateWithDuration(1, animations: { self.alpha = 1 }) } } }把addOrReplaceItem中的UIButton替换为PRButton,这样在item被添加的时候,有一个简单的过渡动画,当我准备重写removeFromSuperview的时候,遇到了一点问题:
在Swift里面,闭包是不能用super的,那只能这样了:
override func removeFromSuperview() { UIView.beginAnimations("", context: nil) UIView.setAnimationDuration(1) self.alpha = 0 UIView.setAnimationDidStopSelector(Selector("callSuperRemoveFromSuperview")) UIView.commitAnimations() } private func callSuperRemoveFromSuperview() { super.removeFromSuperview() }运行起来应该可以看到完整的效果了。
优化三
... weak var animationLayer: CALayer? init(frame: CGRect) { super.init(frame: frame) NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("resume"), name: UIApplicationDidBecomeActiveNotification, object: nil) } func resume() { if self.animationLayer { self.animationLayer?.removeFromSuperlayer() self.setNeedsDisplay() } } deinit { NSNotificationCenter.defaultCenter().removeObserver(self) }这样一来,动画就可以在回到应用程序的时候重新开始了,我把animationLaye以weak的方式引用了在属性里面以便更好判断。