iOS 實現脈衝雷達以及動態增減元素 By Swift

來源:互聯網
上載者:User

標籤:ios   ios動畫   雷達   脈衝   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公用介面:
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)    }}
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:
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的時候,進行了過多的迴圈,就可以視為進入死迴圈了,在這種情況下,你只能重新計算已有的center s。這裡不考慮這種極端情況,因為目前的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?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的方式引用了在屬性裡面以便更好判斷。CSDNGitHub
如果本文有任何問題,請及時指出,以免對後來者產生不必要的困擾,不勝感激!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.