iOS 实现脉冲雷达以及动态增减元素 By Swift-感谢分享

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:

[objc] view plain copy

print?

  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。

时间: 2024-11-08 19:24:25

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

iOS 实现脉冲雷达以及动态增减元素 By Swift

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

动态增添元素和进行手势监听的环形菜单

动态增添元素和进行手势监听的环形菜单 本例子是一个自定义菜单View,包含菜单按钮和子菜单列表.环形的子菜单可以动态增减元素并且可以监听 轻触触摸屏,尚未松开或拖动.轻触触摸屏后松开.按下触摸屏,并拖动.长按触摸屏.上下左右滑动等多种手势,并且可以长按拖动到任意位置在接近边框的时候会自动调整子菜单的位置来让菜单不被边框遮蔽.本项目编译版本4.4.2项目编码UTF-8 下载地址:http://www.devstore.cn/code/info/582.html 运行截图:   

iOS 静态库,动态库与 Framework

iOS 静态库,动态库与 Framework 静态库与动态库的区别 首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用. 什么时候我们会用到库呢?一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件.另外一种情况是,对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间. 上面提到库在使用的时候需要

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

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Dom:动态创建元素</title> </head> <body> <ul id="demo1"> </ul> <input type="text" id=&quo

2015.3.7小练习(动态增加元素及表格,以及浏览器兼容问题)

最近在对之前理解的HTML/CSS/javaScript查漏补缺,真是学会了Jquery马上就开始反感用javascript写很多jquery一下子就可以实现的选择和操作. 今天是做了一个根本无界面美观性可言的实验part,乱七八糟大乱炖,只为练习功能. 动态增加元素 function addBtn() { var textField = document.getElementById("main"); var input = document.createElement("

JS动态创建元素

动态创建元素的三种方法: 第一种document.write("") document.write("<input type=‘text’>  value='456'>"); 相当于创建了一个默认名是456的输入框 function f1(){        document.write("<input type='text' value='999999'>");    } 但是当document.write(&qu

JS动态创建元素的方式

动态创建元素的三种方式: 1.document.write(): a.写在函数里面的话,会冲刷掉之前的页面元素,所以一般不用或是少用: b.会打开document.open()或关闭document.close()文档流: c.文档流: 默认的浏览器的加载顺序是从上往下依次加载的,把当前页面加载完毕之后,就相当于关闭了文档流. 例: <input type="text" value="123"/><input type="button&q

动态脚本元素

动态脚本元素即在js中去创建<script>标签加载外部js并执行,这样加载的好处是文件的下载和执行过程不会阻塞页面的其他进程.通过下面两个例子对比出效果 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="ht

jquery访问动态生成元素

最近在做留言板过程中 ,遇到一个问题:通过jq动态生成的元素无法绑定事件,上网 查资料得知,动态生成元素需要通过事件代理来实现 这里直接介绍我采用的on()方式实现 : jQuery引入了全新的事件绑定机制,on()和off()两个函数统一处理事件绑定.因为在此之前有bind(), live(), delegate()等方法来处理事件绑定,jQuery从性能优化以及方式统一方面考虑决定推出新的函数来统一事件绑定方法并且替换掉以前的方法. on(events,[selector],[data],f