| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- //
- // LNRoomSeatSpeakingView.swift
- // Gami
- //
- // Created by OneeChan on 2026/3/17.
- //
- import Foundation
- import UIKit
- import SnapKit
- class LNRoomSeatSpeakingView: UIView {
- private var borderColor: UIColor = .fill
- private var borderWidth: CGFloat = 1
- private var fillColor: UIColor = .fill.withAlphaComponent(0.5)
- private var duration: Double = 2
- private var offset = 10.0
-
- private let waveCount = 2
- private var borderLayers: [CAShapeLayer] = []
-
- private weak var roomSession: LNRoomViewModel?
- private var curIndex = -1
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- clipsToBounds = false
- isUserInteractionEnabled = false
-
- for _ in 0..<waveCount {
- let borderLayer = CAShapeLayer()
- borderLayer.borderColor = borderColor.cgColor
- borderLayer.borderWidth = borderWidth
- borderLayer.backgroundColor = fillColor.cgColor
- layer.addSublayer(borderLayer)
- borderLayers.append(borderLayer)
- }
-
- LNEventDeliver.addObserver(self)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- func update(_ index: Int, room: LNRoomViewModel?) {
- roomSession = room
- curIndex = index
- }
-
- override func layoutSubviews() {
- super.layoutSubviews()
-
- if borderLayers.first?.bounds.height != bounds.height {
- rebuild()
- }
- }
- }
- extension LNRoomSeatSpeakingView: LNRoomViewModelNotify {
- func onRoomSeatsChanged() {
- check()
- }
-
- func onRoomSpeakingUsersChanged() {
- check()
- }
-
- private func check() {
- guard curIndex != -1,
- let seat = roomSession?.seatsInfo.first(where: { $0.index == curIndex }) else {
- dismiss()
- return
- }
-
- if roomSession?.speakingUser.contains(seat.uid) == true {
- show()
- } else {
- dismiss()
- }
- }
- }
-
- extension LNRoomSeatSpeakingView {
- private func startAnimate(layer: CAShapeLayer, index: Int) {
- let scaleAnim = CABasicAnimation(keyPath: "transform.scale")
- scaleAnim.fromValue = 1.0
- scaleAnim.toValue = 1.0 + offset / CGFloat(bounds.width / 2)
- scaleAnim.duration = duration
-
- let opacityAnim = CABasicAnimation(keyPath: "opacity")
- opacityAnim.fromValue = 1.0
- opacityAnim.toValue = 0.0
- opacityAnim.duration = duration
-
- let animGroup = CAAnimationGroup()
- animGroup.animations = [scaleAnim, opacityAnim]
- animGroup.duration = duration
- animGroup.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
- animGroup.isRemovedOnCompletion = false
- animGroup.fillMode = .forwards
- animGroup.repeatCount = .infinity
- animGroup.beginTime = CACurrentMediaTime() + Double(index) * duration * 0.5
- layer.add(animGroup, forKey: "scaleAndFadeGroup")
- }
-
- private func rebuild() {
- for (index, layer) in borderLayers.enumerated() {
- layer.frame = bounds
- layer.cornerRadius = bounds.height * 0.5
- layer.removeAllAnimations()
- startAnimate(layer: layer, index: index)
- }
- }
-
- private func show() {
- UIView.animate(withDuration: 0.25) {
- self.alpha = 1.0
- }
- }
-
- private func dismiss() {
- UIView.animate(withDuration: 0.25) {
- self.alpha = 0.0
- }
- }
- }
- #if DEBUG
- import SwiftUI
- struct LNRoomSeatSpeakingViewPreview: UIViewRepresentable {
- func makeUIView(context: Context) -> some UIView {
- let container = UIView()
- container.backgroundColor = .lightGray
-
- let view = LNRoomSeatSpeakingView()
- container.addSubview(view)
- view.snp.makeConstraints { make in
- make.center.equalToSuperview()
- make.width.height.equalTo(50)
- }
-
- return container
- }
-
- func updateUIView(_ uiView: UIViewType, context: Context) { }
- }
- #Preview(body: {
- LNRoomSeatSpeakingViewPreview()
- })
- #endif
|