|
|
@ -1,6 +1,16 @@ |
|
|
import UIKit |
|
|
import UIKit |
|
|
|
|
|
|
|
|
public class PlayerView: UIView { |
|
|
public class PlayerView: UIView { |
|
|
|
|
|
/// 真正的卡片。视觉(底色/圆角/描边/阴影)全挂在它身上,**不挂在 self 上**。 |
|
|
|
|
|
/// |
|
|
|
|
|
/// ⚠️ 这是为了兜住 pip 插件的一个行为:PiP 启动时它会把本 view |
|
|
|
|
|
/// `insertSubview` 进 **App 自己的 keyWindow** 并 `frame = window.frame` |
|
|
|
|
|
/// 铺满全屏(PipController.m 的 pictureInPictureControllerDidStartPictureInPicture |
|
|
|
|
|
/// 只按「前台活跃场景的 keyWindow」找父视图,而我们是在前台主动 start 的)。 |
|
|
|
|
|
/// 底色若挂在 self 上,铺满之后就是一整块半透明黑罩子盖住整个 App。 |
|
|
|
|
|
/// 挂在 cardView 上、并且只把 cardView 钉在顶部,最坏情况也只是顶部一条字幕条。 |
|
|
|
|
|
private let cardView: UIView = UIView() |
|
|
|
|
|
private var cardHeightConstraint: NSLayoutConstraint? |
|
|
private let titleLabel: UILabel = UILabel() |
|
|
private let titleLabel: UILabel = UILabel() |
|
|
private let langLabel: UILabel = UILabel() |
|
|
private let langLabel: UILabel = UILabel() |
|
|
private let langContainer: UIView = UIView() |
|
|
private let langContainer: UIView = UIView() |
|
|
@ -32,14 +42,23 @@ public class PlayerView: UIView { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private func 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 |
|
|
// 不加这行的话,一旦它被 pip 插件铺满 App 窗口(见 cardView 的注释), |
|
|
layer.borderWidth = 1 |
|
|
// 整个屏幕就点不动了,而且关掉弹幕也恢复不了——插件的 restoreContentViewIfNeeded |
|
|
layer.borderColor = UIColor(white: 1.0, alpha: 46.0 / 255.0).cgColor |
|
|
// 在 originalParentView 为 nil 时直接 return,不会把它摘下来, |
|
|
layer.shadowColor = UIColor.black.cgColor |
|
|
// 而我们这个 view 是凭空 new 出来的、本来就没有 superview。 |
|
|
layer.shadowOpacity = 0.35 |
|
|
isUserInteractionEnabled = false |
|
|
layer.shadowRadius = 12 |
|
|
backgroundColor = .clear |
|
|
layer.shadowOffset = CGSize(width: 0, height: 4) |
|
|
|
|
|
|
|
|
cardView.backgroundColor = UIColor(red: 40.0 / 255.0, green: 40.0 / 255.0, blue: 40.0 / 255.0, alpha: 90.0 / 255.0) |
|
|
|
|
|
cardView.layer.cornerRadius = 16 |
|
|
|
|
|
cardView.layer.borderWidth = 1 |
|
|
|
|
|
cardView.layer.borderColor = UIColor(white: 1.0, alpha: 46.0 / 255.0).cgColor |
|
|
|
|
|
cardView.layer.shadowColor = UIColor.black.cgColor |
|
|
|
|
|
cardView.layer.shadowOpacity = 0.35 |
|
|
|
|
|
cardView.layer.shadowRadius = 12 |
|
|
|
|
|
cardView.layer.shadowOffset = CGSize(width: 0, height: 4) |
|
|
|
|
|
cardView.translatesAutoresizingMaskIntoConstraints = false |
|
|
|
|
|
|
|
|
titleLabel.text = "实时翻译" |
|
|
titleLabel.text = "实时翻译" |
|
|
titleLabel.font = .boldSystemFont(ofSize: 22) |
|
|
titleLabel.font = .boldSystemFont(ofSize: 22) |
|
|
@ -115,11 +134,21 @@ public class PlayerView: UIView { |
|
|
stackView.addArrangedSubview(cnLabel) |
|
|
stackView.addArrangedSubview(cnLabel) |
|
|
stackView.addArrangedSubview(enLabel) |
|
|
stackView.addArrangedSubview(enLabel) |
|
|
|
|
|
|
|
|
layoutMargins = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) |
|
|
cardView.layoutMargins = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) |
|
|
addSubview(stackView) |
|
|
addSubview(cardView) |
|
|
|
|
|
cardView.addSubview(stackView) |
|
|
|
|
|
|
|
|
let guide = layoutMarginsGuide |
|
|
// cardView 只钉 top/leading/trailing + 固定高度,**不钉 bottom**: |
|
|
|
|
|
// 在真正的 PiP 小窗里窗口高度就等于这个高度,看起来是铺满的; |
|
|
|
|
|
// 万一被铺到 App 全屏窗口上,也只占顶部一条,下面依然是透明的。 |
|
|
|
|
|
let height = cardView.heightAnchor.constraint(equalToConstant: compactPreferredHeight) |
|
|
|
|
|
cardHeightConstraint = height |
|
|
|
|
|
let guide = cardView.layoutMarginsGuide |
|
|
NSLayoutConstraint.activate([ |
|
|
NSLayoutConstraint.activate([ |
|
|
|
|
|
cardView.topAnchor.constraint(equalTo: topAnchor), |
|
|
|
|
|
cardView.leadingAnchor.constraint(equalTo: leadingAnchor), |
|
|
|
|
|
cardView.trailingAnchor.constraint(equalTo: trailingAnchor), |
|
|
|
|
|
height, |
|
|
stackView.topAnchor.constraint(equalTo: guide.topAnchor), |
|
|
stackView.topAnchor.constraint(equalTo: guide.topAnchor), |
|
|
stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor), |
|
|
stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor), |
|
|
stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor), |
|
|
stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor), |
|
|
@ -161,11 +190,12 @@ public class PlayerView: UIView { |
|
|
|
|
|
|
|
|
public override func layoutSubviews() { |
|
|
public override func layoutSubviews() { |
|
|
super.layoutSubviews() |
|
|
super.layoutSubviews() |
|
|
layer.cornerRadius = isExpanded ? 16 : 18 |
|
|
cardView.layer.cornerRadius = isExpanded ? 16 : 18 |
|
|
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath |
|
|
cardView.layer.shadowPath = |
|
|
|
|
|
UIBezierPath(roundedRect: cardView.bounds, cornerRadius: cardView.layer.cornerRadius).cgPath |
|
|
let currentLangText = langLabel.text |
|
|
let currentLangText = langLabel.text |
|
|
if bounds.size != lastBoundsSize || currentLangText != lastLangText { |
|
|
if cardView.bounds.size != lastBoundsSize || currentLangText != lastLangText { |
|
|
lastBoundsSize = bounds.size |
|
|
lastBoundsSize = cardView.bounds.size |
|
|
lastLangText = currentLangText |
|
|
lastLangText = currentLangText |
|
|
updateDynamicTextLineLimit() |
|
|
updateDynamicTextLineLimit() |
|
|
} |
|
|
} |
|
|
@ -176,14 +206,16 @@ public class PlayerView: UIView { |
|
|
isExpanded = expanded |
|
|
isExpanded = expanded |
|
|
cnLabel.isHidden = !expanded |
|
|
cnLabel.isHidden = !expanded |
|
|
enLabel.isHidden = !expanded |
|
|
enLabel.isHidden = !expanded |
|
|
layoutMargins = expanded |
|
|
cardView.layoutMargins = expanded |
|
|
? UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) |
|
|
? UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) |
|
|
: UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) |
|
|
: UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) |
|
|
|
|
|
cardHeightConstraint?.constant = |
|
|
|
|
|
expanded ? expandedPreferredHeight : compactPreferredHeight |
|
|
invalidateIntrinsicContentSize() |
|
|
invalidateIntrinsicContentSize() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private func updateDynamicTextLineLimit() { |
|
|
private func updateDynamicTextLineLimit() { |
|
|
let fittingWidth = max(0, bounds.width - layoutMargins.left - layoutMargins.right) |
|
|
let fittingWidth = max(0, cardView.bounds.width - cardView.layoutMargins.left - cardView.layoutMargins.right) |
|
|
if fittingWidth <= 0 { return } |
|
|
if fittingWidth <= 0 { return } |
|
|
|
|
|
|
|
|
let visibleArrangedSubviews = stackView.arrangedSubviews.filter { !$0.isHidden } |
|
|
let visibleArrangedSubviews = stackView.arrangedSubviews.filter { !$0.isHidden } |
|
|
@ -199,9 +231,9 @@ public class PlayerView: UIView { |
|
|
let totalSpacing = CGFloat(max(0, visibleArrangedSubviews.count - 1)) * stackView.spacing |
|
|
let totalSpacing = CGFloat(max(0, visibleArrangedSubviews.count - 1)) * stackView.spacing |
|
|
|
|
|
|
|
|
let availableHeight = |
|
|
let availableHeight = |
|
|
bounds.height |
|
|
cardView.bounds.height |
|
|
- layoutMargins.top |
|
|
- cardView.layoutMargins.top |
|
|
- layoutMargins.bottom |
|
|
- cardView.layoutMargins.bottom |
|
|
- fixedHeight |
|
|
- fixedHeight |
|
|
- totalSpacing |
|
|
- totalSpacing |
|
|
|
|
|
|
|
|
|