Swift simple barrage example, imitating Bilibili

Let’s look at the example first.
insert image description here
The speed of each barrage is different, and the barrage as a whole is supported to start and pause.
If there are too many bullet screens, there is a buffer queue to keep retrying whether it can be displayed, so as to ensure that all the text can be displayed, and each item can be displayed.

The implementation is based on CADisplayLink, so it is smoother than directly setting up a timer to calculate the offset. The simple translation animation is as follows:

import UIKit

class ViewController: UIViewController {
    
    
    
    let squareView = UIView()
    
    override func viewDidLoad() {
    
    
        super.viewDidLoad()
        
        // 创建 CADisplayLink 对象
        let displayLink = CADisplayLink(target: self, selector: #selector(update))
        
        // 将视图控制器添加到 displayLink 中
        displayLink.add(self, for: .common)
        
        // 设置视图属性
        squareView.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
        squareView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
        view.addSubview(squareView)
    }
    
    @objc func update(_ displayLink: CADisplayLink) {
    
    
        // 在每一帧更新时移动视图
        squareView.frame.origin.x += 5
    }
}

A slight modification on this basic version becomes the following code:

import Foundation
import UIKit

class XDanMu {
    
    
    var row: Int = 0
    var label: UILabel = UILabel()
    var speed: CGFloat = 0
    var isMe: Bool = false
}

class XDanMuView: UIView {
    
    
    var displayLink: CADisplayLink?
    
    var lineHeight: CGFloat = 26
    var gap: CGFloat = 20
    var minSpeed: CGFloat = 1
    var maxSpeed: CGFloat = 2
    var isPause: Bool = false
    
    var danmus: [XDanMu] = []
    var danmuQueue: [(String, Bool)] = []
    var timer: Timer?
    
    func start() {
    
    
        displayLink = CADisplayLink(target: self, selector: #selector(update))
        displayLink?.add(to: RunLoop.current, forMode: .common)
        
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleDanMuQueue), userInfo: nil, repeats: true)
    }
    
    @objc func handleDanMuQueue() {
    
    
        if danmuQueue.isEmpty {
    
    
            return
        }
        let danmu = danmuQueue.removeFirst()
        addDanMu(text: danmu.0, isMe: danmu.1)
    }
    
    @objc func addDanMu(text: String, isMe: Bool) {
    
    
        let danmu = XDanMu()
        danmu.label.frame.origin.x = self.frame.size.width
        danmu.label.text = text
        danmu.label.sizeToFit()
        
        if isMe {
    
    
            danmu.label.layer.borderWidth = 1
        }
        
        var linelasts: [XDanMu?] = []
        let rows: Int = Int(self.frame.size.height / lineHeight)
        for _ in 0..<rows {
    
    
            linelasts.append(nil)
        }
        
        for d in danmus {
    
    
            if d.row >= linelasts.count {
    
    
                break
            }
            if linelasts[d.row] != nil {
    
    
                let endx = danmu.label.frame.origin.x
                let targetx = linelasts[d.row]!.label.frame.origin.x
                if endx > targetx {
    
    
                    linelasts[d.row] = d
                }
            } else {
    
    
                linelasts[d.row] = d
            }
        }
        
        var isMatch = false
        for index in 0..<linelasts.count {
    
    
            if let d = linelasts[index] {
    
    
                let endx = d.label.frame.origin.x + d.label.frame.size.width + gap
                if endx < self.frame.size.width {
    
    
                    danmu.row = index
                    var ms = self.frame.size.width / endx * d.speed
                    ms = CGFloat.minimum(ms, maxSpeed)
                    danmu.speed = CGFloat.random(in: minSpeed...ms)
                    isMatch = true
                    break
                }
            } else {
    
    
                danmu.row = index
                danmu.speed = CGFloat.random(in: minSpeed...maxSpeed)
                isMatch = true
                break
            }
        }
        
        if isMatch == false {
    
    
            danmuQueue.append((text, isMe))
            return
        }
        
        danmu.label.frame.origin.y = lineHeight * CGFloat(danmu.row)
        
        self.addSubview(danmu.label)
        self.danmus.append(danmu)
    }
    
    @objc func update(_ displayLink: CADisplayLink) {
    
    
        if isPause == true {
    
    
            return
        }
        // 在每一帧更新时移动视图
        for index in 0..<danmus.count {
    
    
            let danmu = danmus[index]
            danmu.label.frame.origin.x -= danmu.speed
            if danmu.label.frame.origin.x < -danmu.label.frame.size.width {
    
    
                danmu.label.removeFromSuperview()
                danmus.remove(at: index)
                break
            }
        }
    }
}

Then find a place that needs to be used and add the following code to achieve the effect of the above picture

override func viewDidLoad() {
    
    
    super.viewDidLoad()
    var danmuView: XDanMuView = XDanMuView()
    danmuView.frame = .init(x: 0, y: 100, width: self.view.frame.size.width, height: self.view.frame.size.height - 200)
    self.view.addSubview(danmuView)

    // 配置项
    danmuView.minSpeed = 1
    danmuView.maxSpeed = 2
    danmuView.gap = 20
    danmuView.lineHeight = 30

    // 启动弹幕
    danmuView.start()
    // 启动一个定时器灌弹幕
    timer = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(addDanMu), userInfo: nil, repeats: false)
}

@objc func addDanMu() {
    
    
    let interval = CGFloat.random(in: 0.3...1.0)
    Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(addDanMu), userInfo: nil, repeats: false)
    
    var text = ""
    for _ in 0...Int.random(in: 1...30) {
    
    
        text += "嘿"
    }
    for _ in 0...Int.random(in: 1...2) {
    
    
        danmuView.addDanMu(text: text, isMe: Bool.random())
    }
}

The font of the text is modified according to the needs. Currently, no styles and colors have been added.
Complete project portal
github
gitee

Guess you like

Origin blog.csdn.net/xo19882011/article/details/131853163