Visual_Novel_iOS/crush/Crush/Src/Components/UI/Buttons/AudioButton.swift

446 lines
15 KiB
Swift

//
// AudioButton.swift
// Crush
//
// Created by Leon on 2025/8/1.
//
import UIKit
import SnapKit
import Lottie
enum AudioButtonEnumSize: Int {
/// height: 48
case large
/// height: 32
case medium
/// height: 32
case small
/// 20x20
case square20
case square24
}
// Define AudioButtonStyle enum
enum AudioButtonStyle: Int {
// Clear bg. used in AI Role create process, select voice
case clearBg
/// White triangle + semi-transparent black circle background
case videoPlay
/// +
case voiceThemePlay
}
class AudioButton: CLButton {
// MARK: - Properties
var bgImageView: UIImageView!
var leftImageView: UIImageView!
var voiceAnimation: LottieAnimationView!
private var indicator: IndicatorView!
private var buttonSize: AudioButtonEnumSize = .medium
private var path: String = ""
private var iconSize: CGSize = CGSize(width: 16, height: 16)
// MARK: - States
var isDisplay: Bool = true {
didSet {
isHidden = !isDisplay
}
}
var indicatorAnimated: Bool {
return indicator.isAnimating
}
var audioLottieAnimated: Bool{
return voiceAnimation.isAnimationPlaying
}
// MARK: - Initialization
static func buttonWithSize(_ size: AudioButtonEnumSize) -> AudioButton {
return AudioButton(size: size)
}
static func buttonWithStyle(_ style: AudioButtonStyle) -> AudioButton {
return AudioButton(style: style)
}
convenience init(size: AudioButtonEnumSize) {
self.init(frame: .zero)
self.buttonSize = size
initialViews()
}
convenience init(style: AudioButtonStyle, size:AudioButtonEnumSize = .square20) {
self.init(frame: .zero)
switch style {
case .videoPlay:
initialCirclePlayStyle()
case .clearBg:
initialClearBgPlayStyle()
case .voiceThemePlay:
initialVoiceThemePlay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
// Default initialization, but we discourage direct init
// assertionFailure("Use buttonWithSize or buttonWithStyle instead!")
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.buttonSize = .medium
initialViews()
}
// MARK: - 🚩Style 1: Traditional Audio Button
private func initialViews() {
isDisplay = true
var width: CGFloat = 88
var height: CGFloat = 48
var cornerRadius: CGFloat = 24
var imageEdgeInsets: UIEdgeInsets = .zero
switch buttonSize {
case .large:
height = 48
cornerRadius = 24
imageEdgeInsets = UIEdgeInsets(top: 0, left: 32, bottom: 0, right: 32)
iconSize = CGSize(width: 20, height: 20)
case .small:
height = 32
width = 52
cornerRadius = 16
imageEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
iconSize = CGSize(width: 16, height: 16)
case .medium:
height = 32
cornerRadius = 16
imageEdgeInsets = UIEdgeInsets(top: 0, left: 32, bottom: 0, right: 32)
iconSize = CGSize(width: 16, height: 16)
case .square20:
height = 20
cornerRadius = 0
imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
iconSize = CGSize(width: height, height: height)
case .square24:
height = 24
cornerRadius = 0
imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
iconSize = CGSize(width: height, height: height)
}
snp.makeConstraints { make in
make.height.equalTo(height)
}
layer.cornerRadius = cornerRadius
clipsToBounds = true
self.imageEdgeInsets = imageEdgeInsets
bgImageView = UIImageView()
bgImageView.clipsToBounds = true
bgImageView.image = UIImage.withColor(color: .purple, size: CGSize(width: width, height: 24))
addSubview(bgImageView)
bgImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
leftImageView = UIImageView()
leftImageView.image = iconOfWhitePlay()
addSubview(leftImageView)
leftImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 14, height: 14))
}
let animation = LottieAnimation.named("voice_white")
voiceAnimation = LottieAnimationView(animation: animation)
// voiceAnimation = LottieAnimationView.animationNamed("voice_white.json")
voiceAnimation.isUserInteractionEnabled = false
voiceAnimation.loopMode = .loop
voiceAnimation.animationSpeed = 1.4
voiceAnimation.contentMode = .scaleAspectFit
voiceAnimation.isHidden = true
voiceAnimation.tintColor = .white
addSubview(voiceAnimation)
voiceAnimation.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 14, height: 14))
}
indicator = IndicatorView(color: .white)
indicator.size = CGSize(width: 12, height: 12)
indicator.isHidden = true
addSubview(indicator)
indicator.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 12, height: 12))
}
}
// MARK: - Style(Custom) 2: Circle Play Style
private func initialCirclePlayStyle() {
let width: CGFloat = 32
iconSize = CGSize(width: 16, height: 16)
snp.makeConstraints { make in
make.height.equalTo(32)
}
layer.cornerRadius = 16
clipsToBounds = true
bgImageView = UIImageView()
bgImageView.clipsToBounds = true
bgImageView.image = UIImage.withColor(color: UIColor.black.withAlphaComponent(0.5), size: CGSize(width: width, height: width))
addSubview(bgImageView)
bgImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
leftImageView = UIImageView()
leftImageView.image = iconOfWhitePlay()
addSubview(leftImageView)
leftImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 16, height: 16))
}
let animation = LottieAnimation.named("voice_white")
voiceAnimation = LottieAnimationView(animation: animation)
//voiceAnimation = EPLottieAnimationView.animationNamed("voice_white.json")
voiceAnimation.isUserInteractionEnabled = false
voiceAnimation.loopMode = .loop
voiceAnimation.animationSpeed = 1.4
voiceAnimation.contentMode = .scaleAspectFit
voiceAnimation.isHidden = true
voiceAnimation.tintColor = .white
addSubview(voiceAnimation)
voiceAnimation.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 16, height: 16))
}
indicator = IndicatorView(color: .white)
indicator.size = CGSize(width: 16, height: 16)
indicator.isHidden = true
addSubview(indicator)
indicator.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 16, height: 16))
}
}
// MARK: - Style(Custom) 3: Clear bg
private func initialClearBgPlayStyle() {
iconSize = CGSize(width: 20, height: 20)
touchAreaInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
snp.makeConstraints { make in
make.width.equalTo(20)
make.height.equalTo(20)
}
layer.cornerRadius = 0
clipsToBounds = true
leftImageView = UIImageView()
leftImageView.image = iconOfWhitePlay()
addSubview(leftImageView)
leftImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 16, height: 16))
}
let animation = LottieAnimation.named("voice_white")//
voiceAnimation = LottieAnimationView(animation: animation)
voiceAnimation.isUserInteractionEnabled = false
voiceAnimation.loopMode = .loop
voiceAnimation.animationSpeed = 1.4
voiceAnimation.contentMode = .scaleAspectFit
voiceAnimation.isHidden = true
voiceAnimation.tintColor = .black
addSubview(voiceAnimation)
voiceAnimation.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 16, height: 16))
}
indicator = IndicatorView(color: .white) // .withAlphaComponent(0.5)
indicator.size = CGSize(width: 16, height: 16)
indicator.isHidden = true
addSubview(indicator)
indicator.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 16, height: 16))
}
}
private func initialVoiceThemePlay(){
iconSize = CGSize(width: 12, height: 12)
touchAreaInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
snp.makeConstraints { make in
make.width.equalTo(24)
make.height.equalTo(24)
}
layer.cornerRadius = 12
backgroundColor = .c.cpvn
clipsToBounds = true
leftImageView = UIImageView()
leftImageView.image = iconOfWhitePlay()
addSubview(leftImageView)
leftImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 12, height: 12))
}
let animation = LottieAnimation.named("voice_white")//
voiceAnimation = LottieAnimationView(animation: animation)
voiceAnimation.isUserInteractionEnabled = false
voiceAnimation.loopMode = .loop
voiceAnimation.animationSpeed = 1.4
voiceAnimation.contentMode = .scaleAspectFit
voiceAnimation.isHidden = true
voiceAnimation.tintColor = .black
addSubview(voiceAnimation)
voiceAnimation.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 12, height: 12))
}
indicator = IndicatorView(color: .white) // .withAlphaComponent(0.5)
indicator.size = CGSize(width: 12, height: 12)
indicator.isHidden = true
addSubview(indicator)
indicator.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 12, height: 12))
}
}
// MARK: - Icons
func iconOfWhitePlay() -> UIImage? {
return MWIconFont.image(fromIcon: .play, size: self.iconSize, color: .white)
}
func iconOfWhiteVoice() -> UIImage? {
return MWIconFont.image(fromIcon: .voiceLive, size: self.iconSize, color: .white)
}
func blackIcon() -> UIImage? {
return MWIconFont.image(fromIcon: .play, size: self.iconSize, color: .c.csbn)
}
// MARK: - Public Methods
func startIndicator() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.indicator.isAnimating {
self.leftImageView.isHidden = true
self.voiceAnimation.isHidden = true
return
}
self.indicator.startAnimating()
self.leftImageView.isHidden = true
self.voiceAnimation.isHidden = true
}
}
func stopIndicator() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if !self.indicator.isAnimating {
self.leftImageView.isHidden = false
self.voiceAnimation.isHidden = true
return
}
self.leftImageView.isHidden = false
self.voiceAnimation.isHidden = true
self.indicator.stopAnimating()
}
}
func startAnimation() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.voiceAnimation.isAnimationPlaying {
return
}
self.voiceAnimation.isHidden = false
self.leftImageView.isHidden = true
self.voiceAnimation.play()
}
}
func stopAnimation() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if !self.voiceAnimation.isAnimationPlaying {
return
}
self.voiceAnimation.stop()
self.voiceAnimation.isHidden = true
self.leftImageView.isHidden = false
}
}
func reloadState(with model: SpeechModel?) {
guard let speechModel = model else{
reloadViews(model: model)
return
}
self.path = speechModel.path
speechModel.stateChangedBlock = { [weak self] updatedModel in
guard let self = self, self.path == updatedModel.path else { return }
self.reloadViews(model: updatedModel)
self.checkState(model: updatedModel)
}
reloadViews(model: speechModel)
}
// MARK: - Private Methods
private func reloadViews(model: SpeechModel?) {
guard let audioModel = model else{
stopIndicator()
stopAnimation()
return
}
if audioModel.loadState == .loading {
startIndicator()
} else {
stopIndicator()
}
if audioModel.playState == .playing {
startAnimation()
} else {
stopAnimation()
}
}
private func checkState(model: SpeechModel) {
guard model.loadState == .complete else {
// SpeechManager.shared.stopPlay(with: model) //
return
}
switch model.playState {
case .default:
if model.canAutoPlay && isDisplay {
SpeechManager.shared.startPlay(with: model)
}
case .complete, .failed:
SpeechManager.shared.stopPlay(with: model)
case .playing:
break
}
}
// MARK: - Deinit
deinit {
// 🔥 Replacing DLog with print
print("♻️ AudioButton dealloc")
}
}