import UIKit public class PlayerView: UIView { private let titleLabel: UILabel = UILabel() private let langLabel: UILabel = UILabel() private let langContainer: UIView = UIView() private let cnLabel: UILabel = UILabel() private let enLabel: UILabel = UILabel() private let stackView: UIStackView private var lastDynamicTextLines: Int = -1 private let langRow: UIStackView = UIStackView() private var lastBoundsSize: CGSize = .zero private var lastLangText: String? = nil private var isExpanded: Bool = false private let compactPreferredHeight: CGFloat = 72 private let expandedPreferredHeight: CGFloat = 220 public override init(frame: CGRect) { stackView = UIStackView() super.init(frame: frame) setupUI() } public convenience init() { self.init(frame: .zero) } required init?(coder: NSCoder) { stackView = UIStackView() super.init(coder: coder) setupUI() } private func setupUI() { backgroundColor = UIColor(red: 40.0 / 255.0, green: 40.0 / 255.0, blue: 40.0 / 255.0, alpha: 90.0 / 255.0) layer.cornerRadius = 16 layer.borderWidth = 1 layer.borderColor = UIColor(white: 1.0, alpha: 46.0 / 255.0).cgColor layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.35 layer.shadowRadius = 12 layer.shadowOffset = CGSize(width: 0, height: 4) titleLabel.text = "实时翻译" titleLabel.font = .boldSystemFont(ofSize: 22) titleLabel.textColor = UIColor(white: 1.0, alpha: 235.0 / 255.0) titleLabel.textAlignment = .center titleLabel.numberOfLines = 1 titleLabel.isHidden = true langLabel.text = "src -> tgt" langLabel.font = .systemFont(ofSize: 12) langLabel.textColor = UIColor(white: 1.0, alpha: 235.0 / 255.0) langLabel.textAlignment = .center langLabel.numberOfLines = 1 langLabel.lineBreakMode = .byTruncatingTail langLabel.translatesAutoresizingMaskIntoConstraints = false langLabel.setContentHuggingPriority(.required, for: .horizontal) langLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) langContainer.backgroundColor = UIColor(white: 1.0, alpha: 36.0 / 255.0) langContainer.layer.cornerRadius = 10 langContainer.translatesAutoresizingMaskIntoConstraints = false langContainer.setContentHuggingPriority(.required, for: .horizontal) langContainer.setContentCompressionResistancePriority(.required, for: .horizontal) langContainer.addSubview(langLabel) NSLayoutConstraint.activate([ langLabel.topAnchor.constraint(equalTo: langContainer.topAnchor, constant: 6), langLabel.leadingAnchor.constraint(equalTo: langContainer.leadingAnchor, constant: 10), langLabel.trailingAnchor.constraint(equalTo: langContainer.trailingAnchor, constant: -10), langLabel.bottomAnchor.constraint(equalTo: langContainer.bottomAnchor, constant: -6), ]) cnLabel.font = .systemFont(ofSize: 14) cnLabel.textColor = UIColor(white: 1.0, alpha: 235.0 / 255.0) cnLabel.textAlignment = .left cnLabel.numberOfLines = 0 cnLabel.lineBreakMode = .byTruncatingTail cnLabel.isHidden = true enLabel.font = .systemFont(ofSize: 15) enLabel.textColor = UIColor(red: 144.0 / 255.0, green: 202.0 / 255.0, blue: 249.0 / 255.0, alpha: 1.0) enLabel.textAlignment = .left enLabel.numberOfLines = 0 enLabel.lineBreakMode = .byTruncatingTail enLabel.isHidden = true let para = NSMutableParagraphStyle() para.lineSpacing = 2 para.lineHeightMultiple = 1.1 cnLabel.attributedText = NSAttributedString(string: "", attributes: [.paragraphStyle: para]) enLabel.attributedText = NSAttributedString(string: "", attributes: [.paragraphStyle: para]) stackView.axis = .vertical stackView.spacing = 10 stackView.alignment = .fill stackView.distribution = .fill stackView.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(titleLabel) langRow.axis = .horizontal langRow.alignment = .fill langRow.distribution = .fill langRow.translatesAutoresizingMaskIntoConstraints = false let leftSpacer = UIView() let rightSpacer = UIView() leftSpacer.translatesAutoresizingMaskIntoConstraints = false rightSpacer.translatesAutoresizingMaskIntoConstraints = false langRow.addArrangedSubview(leftSpacer) langRow.addArrangedSubview(langContainer) langRow.addArrangedSubview(rightSpacer) NSLayoutConstraint.activate([ leftSpacer.widthAnchor.constraint(equalTo: rightSpacer.widthAnchor), ]) stackView.addArrangedSubview(langRow) stackView.addArrangedSubview(cnLabel) stackView.addArrangedSubview(enLabel) layoutMargins = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) addSubview(stackView) let guide = layoutMarginsGuide NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: guide.topAnchor), stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor), stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor), ]) } public func play() { } public func pause() { } public func update(withLang lang: String?, cn: String?, en: String?) { if let lang = lang { langLabel.text = lang } let para = NSMutableParagraphStyle() para.lineSpacing = 2 para.lineHeightMultiple = 1.1 if let cn = cn { cnLabel.attributedText = NSAttributedString(string: cn, attributes: [.paragraphStyle: para]) } if let en = en { enLabel.attributedText = NSAttributedString(string: en, attributes: [.paragraphStyle: para]) } let currentCn = cnLabel.attributedText?.string ?? "" let currentEn = enLabel.attributedText?.string ?? "" let hasContent = !currentCn.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !currentEn.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty setExpanded(hasContent) setNeedsLayout() } public override var intrinsicContentSize: CGSize { CGSize(width: UIView.noIntrinsicMetric, height: isExpanded ? expandedPreferredHeight : compactPreferredHeight) } public override func layoutSubviews() { super.layoutSubviews() layer.cornerRadius = isExpanded ? 16 : 18 layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath let currentLangText = langLabel.text if bounds.size != lastBoundsSize || currentLangText != lastLangText { lastBoundsSize = bounds.size lastLangText = currentLangText updateDynamicTextLineLimit() } } private func setExpanded(_ expanded: Bool) { if expanded == isExpanded { return } isExpanded = expanded cnLabel.isHidden = !expanded enLabel.isHidden = !expanded layoutMargins = expanded ? UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) : UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) invalidateIntrinsicContentSize() } private func updateDynamicTextLineLimit() { let fittingWidth = max(0, bounds.width - layoutMargins.left - layoutMargins.right) if fittingWidth <= 0 { return } let visibleArrangedSubviews = stackView.arrangedSubviews.filter { !$0.isHidden } let fixedSubviews = visibleArrangedSubviews.filter { $0 !== cnLabel && $0 !== enLabel } let fixedHeight = fixedSubviews.reduce(CGFloat(0)) { partialResult, view in partialResult + view.systemLayoutSizeFitting( CGSize(width: fittingWidth, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ).height } let totalSpacing = CGFloat(max(0, visibleArrangedSubviews.count - 1)) * stackView.spacing let availableHeight = bounds.height - layoutMargins.top - layoutMargins.bottom - fixedHeight - totalSpacing let cnLineHeight = cnLabel.font.lineHeight * 1.1 + 2 let enLineHeight = enLabel.font.lineHeight * 1.1 + 2 let combinedLineHeight = cnLineHeight + enLineHeight let perLabelLines = max(1, min(Int(availableHeight / combinedLineHeight), 10)) if perLabelLines != lastDynamicTextLines { lastDynamicTextLines = perLabelLines cnLabel.numberOfLines = perLabelLines enLabel.numberOfLines = perLabelLines } } }