446 lines
15 KiB
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")
|
|
}
|
|
}
|