Swift经过Xcode6 Beta4一版更新后,基本上已经可以作为生产工具了,虽然有一些地方和ObjC比起来要“落后”一些,但也无伤大雅。这里就用Xcode6 Beta4+iOS SDK 8.0开发,如果用ObjC的话,只需把某些语法和调用方式替换一下就可以了。
最终效果:
这效果是从MOV文件转成GIF的,而且CSDN不支持大于2M的图片上传,优酷地址

创建基本动画

这效果是从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在一起的效果
在我们现在的这种情况下,pulsingLayer是设置过frame和border的,所以在动画的空档期,pulsingLayer会直接显示出一个带边框的圆形(动画还没有开始),当然,在动画播放过一次之后,这个边框就不会显示了,因为进入了正常的动画播放循环,不会出现空档期。我们只需要避免在动画播放前不出现空档期就行了,即设置fillMode = kCAFillModeBackwards(提前进入动画状态)。
repeatCount = HUGE就是字面意思,表示动画无限循环(HUGE可以认为是无限,如果是ObjC,用HUGE_VAL)。
CAMediaTimingFunction由系统预置了几个值:
  • kCAMediaTimingFunctionLinear 线性,即匀速
  • kCAMediaTimingFunctionEaseIn 先慢后快
  • kCAMediaTimingFunctionEaseOut 先快后慢
  • kCAMediaTimingFunctionEaseInEaseOut 先慢后快再慢
  • kCAMediaTimingFunctionDefault 实际效果是在动画开始时和动画播放时比较快,将结束时会变慢
CAMediaTimingFunction支持被定制。我们把timingFunction设置为kCAMediaTimingFunctionDefault,可以使动画播放的更加动感。
接下来的Scale动画就很简单了,从0(0倍)到1.5(放大1.5倍)变换即可。
Opacity透明动画只用设置values和与其对应的keyTimes就行了,需要注意的是keyTimes表示的是时间比例,取值0到1之间,如values的第一个元素为1,keyTimes第一个元素为0,表示动画开始时,opacity为1;values的第二个元素为0.7,keyTimes第二个元素为0.5,表示动画播放到一半的时候,opacity为0.7;依次类推,可自由定制。
然后将单独的scale动画与opacity动画封装到animationGroup里,在把包含了两个动画的animationGroup给pulsingLayer,animationLayer添加pulsingLayer,最后添加这个包含了所有动画Layer的animationLayer即可。
 

动态增减元素

这效果是从MOV文件转成GIF的,而且CSDN不支持大于2M的图片上传,优酷地址

动画部分已经完成了,接下来我们给PulsingRadarView增加接口,使其支持增减元素。
首先给PulsingRadarView添加两个属性: 

class PulsingRadarView: UIView {

let itemSize = CGSizeMake(44, 44)

var items = NSMutableArray()

第一个是每个item的尺寸,第二个用来存储所有的item。
添加addOrReplaceItem公共接口:

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)

}

}

maxCount是圆内显示item的最大值,这里简单的写死,你可以把它开放出去成为一个公共的属性。这里的每个item都是UIButton,初始化后设置一张图片即可,generateCenterPointInRadar方法返回一个圆内的中心坐标,这个坐标只会在圆的直径以内生成,稍后放出。最后判断一下有没有超出maxCount,如果超出了,就把最先添加的item移除掉。
在放出generateCenterPointInRadar这个方法之前,我们首先要了解,哪个范围是我们的坐标生成范围:
大家都知道,View的基本形状是矩形(红色区域),drawRect是以Rect为基础的,但是我们这个雷达是圆形,也就是蓝色区域才是我们的目标范围,所以生成的坐标要围绕中心的绿点(圆心),让我们重新翻开数学课本,看看高中数学对三角函数的定义:

在一个平面直角坐标系中,以原点为圆心,1 为半径画一个圆,这个圆交 x 轴于 A 点。以 O 为旋转中心,将 A 点逆时针旋转一定的角度α至 B 点,设此时 B 点的坐标是(x,y),那么此时 y 的值就叫做α的正弦,记作 sinα;此时 x 的值就叫做α的余弦,记作 cosα;y 与 x 的比值 y/x 就叫做α的正切,记作 tanα。

还有一个很重要的公式:圆的参数方程:以点O(a,b)为圆心,以r为半径的圆的参数方程是 x=a+r*cosθ, y=b+r*sinθ, (其中θ为参数)
到这里为止,思路就清晰了,以下是generateCenterPointInRadar的方法实现:

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来用了,这实际上也是完全采用的公式的算法。
这样一来,addOrReplaceItem这个接口也完成了,我们把ViewController里的调用也完善一下,具体的,在viewDidLoad方法的最后增加一个Timer,这个Timer每0.5秒调用一次addOrReplaceItem:
  1. NSTimer.scheduledTimerWithTimeInterval(0.5, target: radarView, selector: Selector("addOrReplaceItem"), userInfo: nil, repeats: true)

Timer在不用的时候一定要调用invalidate()方法,并且要在ViewController析构之前,不然ViewController不会被释放,也就永远不会被析构。这里我们就不考虑那么多了,毕竟只有一个页面,而且在真实场景里也不会这么去用,更多的情况是在网络请求回调的时候去处理。

这么一来,动态增减部分也完成了,但是完美了吗?显然没有。

优化

优化一

与其说是优化,不如说是修复Bug。很明显,在上一步中,我们动态生成的元素重叠了,这不能让人接受,而我们只要稍微做些改变就能防止这种情况的发生。
我们现在在生成每个item的center的时候,没有和已有的item进行比较,这是一个比较耗性能的操作,如果你的itemSize过大,maxCount过多,这甚至能导致死循环,如果是那样的话,你可能在对itemSize以及maxCount做出限制的同时,也对循环的数量也进行控制,如果在生成一个item的center的时候,进行了过多的循环,就可以视为进入死循环了,在这种情况下,你只能重新计算已有的centers。这里不考虑这种极端情况,因为目前的itemSize和maxCount的配合,不会出现死循环。
我们添加一个itemFrameIntersectsInOtherItem私有方法来判断是否和之前生成的center有了重叠:

private func itemFrameIntersectsInOtherItem (frame: CGRect) -> Bool {

for item in items {

if CGRectIntersectsRect(item.frame, frame) {

return true

}

}

return false

}

接收一个frame,然后和每一个item比较,如果重叠返回true,反之则返回false。
在addOrReplaceItem方法里的改造:

......

do {

var center = generateCenterPointInRadar()

radarButton.center = CGPointMake(center.x, center.y)

} while (itemFrameIntersectsInOtherItem(radarButton.frame))

......

把设置center的地方用一个do-while循环包装起来即可。这么一来,生成的元素就不会重叠了。

优化二

我打算给每一个item的显示和移除增加一点动画效果,以免显得太生硬,并且用派生类的方式来实现:

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()

}

运行起来应该可以看到完整的效果了。
 

优化三

这个同样也是修复Bug。如果在动画播放的时候你按下Home键(模拟器按下command+shift+h),就会出现下面这种情况:
这是因为在按下Home键的时候,所有的动画被移除了,具体的,每个Layer都调用了removeAllAnimations方法。我们如果想要在回到应用程序的时候继续动画,需要监听系统的UIApplicationDidBecomeActiveNotification通知:

weak var animationLayer: CALayer?

override init(frame: CGRect) {

super.init(frame: frame)

NSNotificationCenter.defaultCenter().addObserver(self,

selector: Selector("resume"),

name: UIApplicationDidBecomeActiveNotification,

object: nil)

}

required init(coder aDecoder: NSCoder) {

fatalError("init(coder:) has not been implemented")

}

func resume() {

if let animationLayer = self.animationLayer {

animationLayer.removeFromSuperlayer()

self.setNeedsDisplay()

}

}

deinit {

NSNotificationCenter.defaultCenter().removeObserver(self)

}

这样一来,动画就可以在回到应用程序的时候重新开始了,我把animationLaye以weak的方式引用到了属性里面,这是为了在resume里好方便判断。

GitHub下载地址

如果本文有任何问题,请及时指出,以免对后来者产生不必要的困扰,不胜感激!
 

UPDATED:

GitHub上的版本已更新至Xcode6 Beta6,在之前下载,然后更新Xcode的朋友,如果编译出现__TFSs26_forceBridgeFromObjectiveCU__FTPSs9AnyObject_MQ__Q_之类的问题,请看这里
------------------------------------------------------------------------------------------------------
GitHub上已更新至Xcode 6。

iOS 实现脉冲雷达以及动态增减元素 By Swift-感谢分享的更多相关文章

  1. 动态生成元素动作绑定,jquery 1.9如何实现

    1.7后增加了 live()1.9后被移除了 网上说可以用 on() 代替 可以实际在动态生成元素上绑定动作,没效果,求解绝方法(用低版本的jQuery这种方法,求别说..) 答: 之前有老兄回答过类 ...

  2. JavaScript 、jQuery动态创建元素的关键字~

    JavaScript动态创建元素: 1.创建元素  如:a 标签 var alink= document.createElement("a"); 2.j添加元素属性 alink.h ...

  3. js学习-DOM之动态创建元素的三种方式、插入元素、onkeydown与onkeyup两个事件整理

    动态创建元素的三种方式: 第一种: Document.write(); <body> <input type="button" id="btn" ...

  4. 移动web在ios和android下点击元素出现阴影问题

     移动web开发经验总结 1.-webkit-tap-highlight-color:rgba(255,255,255,0)可以同时屏蔽ios和android下点击元素时出现的阴影.备注:transp ...

  5. iOS 静态库和动态库的区别&静态库的生成

    linux中静态库和动态库的区别 一.不同 库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行.库分静态库和动态库两种. 1. 静态函数库 这类库的名字一般是libxxx.a:利用静态函 ...

  6. Javascript:DOM动态创建元素实例应用

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. iOS 静态库,动态库与 Framework 浅析

    静态库与动态库的区别 首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用. 什么时候我们会用到库呢?一种情况是某些代码需要给别人使用,但是我们不希望别人 ...

  8. css清浮动与动态计算元素宽度

    css常用清浮动写法 /*清除浮动*/| .clears:after{ display: block; content: ''; clear: both; height: ; visibility: ...

  9. JS脚本动态给元素/控件添加事件

    最近突然要用到JS脚本动态给元素添加事件.如TextBox的onclick事件.但有的onclick事件原先已经定义了相应代码!这里又不能替代原有方法,而JS脚本里面有个方法可以给控件在原有事件的基础 ...

随机推荐

  1. MISCONF Redis is configured to save RDB snapshots

    今天客户突然反馈用我们的api出现了下面的这个错误 MISCONF Redis is configured to save RDB snapshots, but is currently not ab ...

  2. 【转】Android Studio 的小小配置

    这篇博文的内容列表: 1.编辑器的字体设置 2.预览XML布局 3.导入项目  4.显示行号  5.新建Java类和Component的子类 额~~程序猿一般话都不多,嘿嘿,那咱就直接图文吧~ ^_^ ...

  3. JS实例(二)

    一:注册页面 包括非空验证.邮箱验证.密码相等验证,在输入之前提示文字,获得焦点时文字清除颜色变化,输入正确显示正确图片,错误显示错误图片,所有验证通过才可提交,重置会重置回初始模样. 效果图如下: ...

  4. less编码规范

    Less 编码规范 简介 因为自己最近写css用的比较多还是less,整理了一份less规范, 代码组织 代码按如下形式按顺序组织: @import 变量声明 样式声明 // ✓ @import &q ...

  5. PHP 内存的分布问题

    php运行,内存分为5个区域, 1.基本数据类型--->栈区 2.符合数据类型-->堆区 对象实例在堆区,对象名字在栈区(指向此对象实例的变量)

  6. Android开发手记(18) 数据存储三 SQLite存储数据

    Android为数据存储提供了五种方式: 1.SharedPreferences 2.文件存储 3.SQLite数据库 4.ContentProvider 5.网络存储 SQLite 是以嵌入式为目的 ...

  7. mvc5 + ef6 + autofac搭建项目(repository+uow)(一)

    直奔主题了,不那么啰嗦. 整体框架的参考来源是  O# 的框架,在此感谢锋哥一直以来的开源,让我们有的学 如下图: (图一) 一下分三个步骤说明,分别为 dbContext,repository,uo ...

  8. SQL小细节

    平时有些小细节,不留意的话很容易得到错误的答案,我们来看下下面的代码,看看你是否能答对呢? ) ,) SELECT @str = '中国CH',@info='MyTest' SELECT [字符串]= ...

  9. Ajax结合Js操作灵活操作表格

    Table页面: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head& ...

  10. ASP.NET页面生命周期总结(完结篇)

    补充: W3svc服务  负责把‘工作进程’启动起来 W3svc 连接工作进程.内核模块.IIS 主服务的一个核心的桥梁 W3svc还有一个作用就是维护应用程序池,可以设置多长时间回收,多长时间重启. ...