iOS彈幕之swift實(shí)現(xiàn)

彈幕在直播,視頻類(lèi)app上,會(huì)經(jīng)??吹?。這段時(shí)間研究了下彈幕的原理,并用swift實(shí)現(xiàn)了下。以此來(lái)記錄。實(shí)現(xiàn)效果如下。github地址:SwiftDanmuView

danmu.gif

彈幕原理

假設(shè)方向是從右到左,那么彈幕就是頭部從屏幕最右邊,向左移動(dòng),直至尾部完全離開(kāi)屏幕最左端的一個(gè)過(guò)程。

danmu.png

彈幕動(dòng)畫(huà)

彈幕動(dòng)畫(huà)比較簡(jiǎn)單,水平位移,從右到左的過(guò)程。

動(dòng)畫(huà)時(shí)間 = (屏幕寬度+彈幕長(zhǎng)度) / speed,speed可自行設(shè)置。

彈軌

彈幕一般會(huì)有N條彈軌,這樣彈幕可以同時(shí)在不同的彈軌中顯示。

從待播放彈幕list中,從頭取出一條,計(jì)算將要放到哪條彈軌。

主要算法是:遍歷所有彈軌,計(jì)算該彈幕放入該彈軌,是否會(huì)與最后一條彈幕碰撞,若不會(huì),則放到該軌道。若都不符合,那么繼續(xù)放在list中,等待下一次的取出(有個(gè)定時(shí)器,每個(gè)0.1s從list中取出彈幕來(lái)播放)。

for i in 0..<N {
    if 滿足條件不發(fā)生碰撞
        return i
}
防碰撞

防碰撞的原理:記錄每條彈軌的最后一條彈幕的最右端顯示到屏幕上的時(shí)間 + 時(shí)間間隔 + 當(dāng)前時(shí)間 = t,即t = 彈幕寬度 / speed + interval(默認(rèn)0.5s) + curTime,在要將一條彈幕放到彈軌時(shí),若當(dāng)前的時(shí)間>=t,則滿足條件。

var shouldShow = true
// 檢查是否滿足條件
if let time = timeDict[index] {
  let currentTime = NSDate()
  if currentTime.timeIntervalSince1970 < TimeInterval(time) {
      shouldShow = false
  }
}
        
// 彈幕完全顯示在屏幕的時(shí)間+間隔
let time = itemView.width / speed + 0.5 + CGFloat(NSDate().timeIntervalSince1970)
timeDict[index] = time
view重用

在彈幕量較大時(shí),每次都新創(chuàng)建view,會(huì)耗費(fèi)內(nèi)存。在當(dāng)彈幕動(dòng)畫(huà)結(jié)束后,可將其添加到重用池中,注意這里的動(dòng)畫(huà)結(jié)束有普通的結(jié)束和暫?;謴?fù)后的結(jié)束,2種情況都要處理放入重用池。在播放彈幕時(shí),首先從重用池中取,沒(méi)有就重新創(chuàng)建。

因?yàn)榭紤]到會(huì)有不同樣式的彈幕,我這里的處理是,以樣式的className為key來(lái)存要重用的view。

// key:className
lazy var reuseItemViewPool: [String: UIView] = {
   var reusePool = [String: UIView]()
   return reusePool
}()

// 取重用view
func reuseItemView(cls: AnyClass) -> UIView? {
   guard reuseItemViewPool.count > 0 else {
       return nil
   }
        
   let className = NSStringFromClass(cls)
   if let reuseView = reuseItemViewPool[className] {
       reuseItemViewPool.removeValue(forKey: className)

       return reuseView
   }
   
   return nil
}
  • 普通結(jié)束放入重用池:
let duration = (self.width + itemView.width) / speed
        
UIView.animate(withDuration: TimeInterval(duration), delay: 0, options: .curveLinear, animations: {
  itemView.x = -itemView.width
}) { (finished) in
  if (finished) {
      // add to reusePool
      self.reuseItemViewPool[NSStringFromClass(itemViewClass)] = itemView
      print("reusePool:\(self.reuseItemViewPool)")
      itemView.removeFromSuperview()
  }
}
  • 暫?;謴?fù)后放入重用池
let duration = (itemView.x + itemView.width) / speed

UIView.animate(withDuration: TimeInterval(duration), delay: 0, options: .curveLinear, animations: {
    itemView.x = -itemView.width
}) { (finished) in
     if (finished) {
         let mirror = Mirror(reflecting: itemView)
         self.reuseItemViewPool[NSStringFromClass(mirror.subjectType as! AnyClass)] = itemView
         itemView.removeFromSuperview()
     }
}
暫停/恢復(fù)
  • 暫停
    暫停說(shuō)白了就是將動(dòng)畫(huà)移除,然后將彈幕放在它正確的位置。在動(dòng)畫(huà)過(guò)程中,presentationLayer是表示正在做動(dòng)畫(huà)的layer,取出其frame,就是真正此時(shí)彈幕的位置。

    func pause() {
       stopTimer()
       
       for itemView in self.subviews {
           if itemView.isKind(of: SLDanmuItemView.self) {
           if let frame = itemView.layer.presentation()?.frame {
               itemView.frame = frame
           }
              
           itemView.layer.removeAllAnimations()
           }
        }
    

}

    
* 恢復(fù)
恢復(fù)的過(guò)程,重新開(kāi)始動(dòng)畫(huà),有一點(diǎn)要注意的是,`防碰撞的時(shí)間戳要更新`。
    
  假設(shè)有條軌道的時(shí)間戳是t,在彈幕的尾部還沒(méi)有完全顯示在屏幕上的時(shí)候,點(diǎn)擊了暫停,然后隔了2s,再點(diǎn)擊恢復(fù),那么這個(gè)時(shí)候,這條彈幕繼續(xù)做動(dòng)畫(huà),若沒(méi)有更新碰撞時(shí)間戳,新放入的彈幕在判斷時(shí),當(dāng)前時(shí)間有可能是會(huì)大于t的,然后會(huì)被放入這條軌道,從而會(huì)發(fā)生碰撞。
    
    ```
    // 更新時(shí)間,如果右邊未完全顯示在屏幕
     if (itemView.x + itemView.width > self.width) {
         let time = (itemView.x + itemView.width - self.width) / speed + 0.5 + CGFloat(NSDate().timeIntervalSince1970)
         timeDict[index] = time
     }
    ```
    
  恢復(fù)代碼:
    ```
    func resume() {
        startTimer()
        
        for itemView in self.subviews {
            if itemView.isKind(of: SLDanmuItemView.self) {
                let index = rowWithY(y: itemView.y)
               
                // 更新時(shí)間,如果右邊未完全顯示在屏幕
                if (itemView.x + itemView.width > self.width) {
                    let time = (itemView.x + itemView.width - self.width) / speed + 0.5 + CGFloat(NSDate().timeIntervalSince1970)
                    timeDict[index] = time
                }
                
                let duration = (itemView.x + itemView.width) / speed
                
                UIView.animate(withDuration: TimeInterval(duration), delay: 0, options: .curveLinear, animations: {
                    itemView.x = -itemView.width
                }) { (finished) in
                    if (finished) {
                        let mirror = Mirror(reflecting: itemView)
                        self.reuseItemViewPool[NSStringFromClass(mirror.subjectType as! AnyClass)] = itemView
                        itemView.removeFromSuperview()
                    }
                }
            }
        }
    }
    ```

####彈幕數(shù)據(jù)結(jié)構(gòu)


結(jié)構(gòu)定義如下:
    

class SLDanmuInfo {
var text: String
var textColor: UIColor = UIColor.black
var itemViewClass: AnyClass = SLDanmuItemView.self
...
}

    
更新ui,sizeToFit更新frame。
    

class SLDanmuItemView: UIView {
func updateDanmuInfo(info: SLDanmuInfo) {
label.text = info.text
label.textColor = info.textColor

    setNeedsLayout()
}

// 計(jì)算自身frame
override func sizeToFit() {
super.sizeToFit()

    label.sizeToFit()
    
    label.frame = CGRect(x: leftMargin, y: topMargin, width: label.frame.size.width, height: label.frame.size.height)
    
    self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: label.frame.size.width + 2 * leftMargin, height: label.frame.size.height + 2 * topMargin)
}

}

    
這種是最基礎(chǔ)的,只更新text。由于要支持不同樣式的彈幕,所以定義了`itemViewClass`??稍O(shè)置該條彈幕所展示ui的`class`。
    
同時(shí)也可以自定義彈幕ui繼承自`SLDanmuItemView`,danmuInfo繼承`SLDanmuInfo`,在自定義ui中更新danmuInfo,`注意要重寫(xiě)sizeToFit,設(shè)置好frame`。

我這里自定義了個(gè)有背景色的ui。
    

class SLDanmuBgItemView: SLDanmuItemView {
lazy var bgView: UIView = {
var bgView = UIView()

        bgView.backgroundColor = UIColor.lightGray
        bgView.layer.cornerRadius = 4
        bgView.clipsToBounds = true
        
        return bgView
    }()

    override func commonInit() {
        super.commonInit()
        self.insertSubview(bgView, belowSubview: label)
    }

override func updateDanmuInfo(info: SLDanmuInfo) {
        super.updateDanmuInfo(info: info)
    
        if let info = info as? SLBgDanmuInfo {
            bgView.backgroundColor = info.bgColor
        }
    }

override func sizeToFit() {
    super.sizeToFit()
    bgView.frame = self.bounds
}

}

    

class SLBgDanmuInfo: SLDanmuInfo {
var bgColor: UIColor
...
}



####使用

設(shè)置好數(shù)據(jù)源即可。

class ViewController: UIViewController {

    lazy var danmuView: SLDanmuView = {
        var danmuView = SLDanmuView(frame: CGRect(x: 0, y: 50, width: self.view.width, height: 150))
        return danmuView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        var list = [SLDanmuInfo]()
        
        //test
        var info = SLDanmuInfo(text: "hi色黑龍江凡士林", textColor: UIColor.red, itemViewClass: SLDanmuBgItemView.self)
        list.append(info)
        
        info = SLDanmuInfo(text: "arre咳咳咳看", textColor: UIColor.blue, itemViewClass: SLDanmuItemView.self)
        list.append(info)
        
        info = SLDanmuInfo(text: "fds分手快樂(lè)發(fā)送", textColor: UIColor.black, itemViewClass: SLDanmuBgItemView.self)
        list.append(info)
        
        info = SLDanmuInfo(text: "23誒偶無(wú)偶", textColor: UIColor.purple, itemViewClass: SLDanmuItemView.self)
        list.append(info)
        
        info = SLDanmuInfo(text: "ff你好風(fēng)刀霜?jiǎng)Ψ答佀芰洗桓兜目妓牧?jí)", textColor: UIColor.green, itemViewClass: SLDanmuBgItemView.self)
        list.append(info)
        
        info = SLDanmuInfo(text: "ff你好風(fēng)刀霜?jiǎng)Πl(fā)快遞擴(kuò)擴(kuò)擴(kuò)擴(kuò)塑料袋交付的考四六級(jí)", textColor: UIColor.yellow, itemViewClass: SLDanmuItemView.self)
        list.append(info)
        
        info = SLBgDanmuInfo(text: "just for test", textColor: UIColor.brown, itemViewClass: SLDanmuBgItemView.self, bgColor: UIColor.red)
        list.append(info)
        
        for i in 0...10 {
            info = SLDanmuInfo(text: "考四六級(jí)" + String(i), textColor: UIColor.red, itemViewClass: SLDanmuItemView.self)
            list.append(info)
        }
        
        danmuView.pendingList.append(contentsOf: list)

        self.view.addSubview(danmuView)
    }

詳細(xì)可以看源碼:https://github.com/silan-liu/SwiftDanmuView
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在iOS中隨處都可以看到絢麗的動(dòng)畫(huà)效果,實(shí)現(xiàn)這些動(dòng)畫(huà)的過(guò)程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫(huà)全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,690評(píng)論 6 30
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫(huà)效果,實(shí)現(xiàn)這些動(dòng)畫(huà)的過(guò)程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫(huà)全貌。在這里你可以看...
    F麥子閱讀 5,268評(píng)論 5 13
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,012評(píng)論 25 709
  • 寫(xiě)在開(kāi)篇 最近做了個(gè)視頻直播項(xiàng)目,當(dāng)用到彈幕時(shí),找了很多網(wǎng)上彈幕demo。當(dāng)時(shí)因?yàn)轫?xiàng)目進(jìn)度的原因,就隨便選了一個(gè)漂...
    Rasping閱讀 7,554評(píng)論 48 36
  • 每個(gè)小孩都見(jiàn)過(guò)萌萌的大臉泰迪熊,但殊不知他們抱著泰迪熊的感受,看了陽(yáng)光姐姐的《擁抱幸福的小熊》真的好感動(dòng),...
    潘多拉小熊閱讀 597評(píng)論 0 0

友情鏈接更多精彩內(nèi)容