feat: 优化相册图片预览交互
This commit is contained in:
@@ -44,11 +44,16 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
private let counterLabel = UILabel()
|
private let counterLabel = UILabel()
|
||||||
private let bottomChrome = UIView()
|
private let bottomChrome = UIView()
|
||||||
private let divider = UIView()
|
private let divider = UIView()
|
||||||
private let tabStack = UIStackView()
|
private let variantControls = UIView()
|
||||||
|
private let variantSegmentedControl = UISegmentedControl()
|
||||||
|
private let highResolutionButton = UIButton(type: .system)
|
||||||
private let actionStack = UIStackView()
|
private let actionStack = UIStackView()
|
||||||
private let deleteButton = UIButton(type: .system)
|
private let deleteButton = UIButton(type: .system)
|
||||||
private let refreshButton = UIButton(type: .system)
|
private let refreshButton = UIButton(type: .system)
|
||||||
private var tabHeightConstraint: Constraint?
|
private var variantControlsHeightConstraint: Constraint?
|
||||||
|
private var didConfigureVariantControls = false
|
||||||
|
private var showsVariantControls = false
|
||||||
|
private var variantControlsAnimationGeneration = 0
|
||||||
|
|
||||||
/// 创建全屏预览页。
|
/// 创建全屏预览页。
|
||||||
init(
|
init(
|
||||||
@@ -84,7 +89,8 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool { true }
|
override var prefersStatusBarHidden: Bool { false }
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
|
||||||
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { [.bottom] }
|
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { [.bottom] }
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
@@ -114,6 +120,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
collectionView.isPagingEnabled = true
|
collectionView.isPagingEnabled = true
|
||||||
collectionView.alwaysBounceHorizontal = nodes.count > 1
|
collectionView.alwaysBounceHorizontal = nodes.count > 1
|
||||||
collectionView.showsHorizontalScrollIndicator = false
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
|
collectionView.accessibilityIdentifier = "travelAlbum.previewCollectionView"
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dataSource = self
|
collectionView.dataSource = self
|
||||||
collectionView.register(
|
collectionView.register(
|
||||||
@@ -121,34 +128,57 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
forCellWithReuseIdentifier: TravelAlbumPreviewImageCell.reuseIdentifier
|
forCellWithReuseIdentifier: TravelAlbumPreviewImageCell.reuseIdentifier
|
||||||
)
|
)
|
||||||
|
|
||||||
topChrome.backgroundColor = UIColor.black.withAlphaComponent(0.72)
|
topChrome.backgroundColor = .black
|
||||||
bottomChrome.backgroundColor = UIColor.black.withAlphaComponent(0.78)
|
topChrome.accessibilityIdentifier = "travelAlbum.previewTopChrome"
|
||||||
divider.backgroundColor = UIColor.white.withAlphaComponent(0.2)
|
bottomChrome.backgroundColor = .black
|
||||||
|
bottomChrome.accessibilityIdentifier = "travelAlbum.previewBottomChrome"
|
||||||
|
divider.backgroundColor = UIColor.white.withAlphaComponent(0.12)
|
||||||
|
|
||||||
closeButton.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(weight: .semibold)), for: .normal)
|
var backConfiguration = UIButton.Configuration.plain()
|
||||||
closeButton.tintColor = .white
|
backConfiguration.image = UIImage(systemName: "chevron.backward")
|
||||||
|
backConfiguration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(
|
||||||
|
pointSize: 21,
|
||||||
|
weight: .semibold
|
||||||
|
)
|
||||||
|
backConfiguration.baseForegroundColor = .white
|
||||||
|
backConfiguration.contentInsets = .zero
|
||||||
|
closeButton.configuration = backConfiguration
|
||||||
closeButton.accessibilityLabel = "关闭图片预览"
|
closeButton.accessibilityLabel = "关闭图片预览"
|
||||||
|
closeButton.accessibilityIdentifier = "travelAlbum.previewBackButton"
|
||||||
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
||||||
|
|
||||||
titleLabel.textColor = .white
|
titleLabel.textColor = .white
|
||||||
titleLabel.font = .systemFont(ofSize: 15, weight: .medium)
|
titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
titleLabel.textAlignment = .center
|
titleLabel.textAlignment = .center
|
||||||
titleLabel.lineBreakMode = .byTruncatingMiddle
|
titleLabel.lineBreakMode = .byTruncatingMiddle
|
||||||
sizeLabel.textColor = UIColor.white.withAlphaComponent(0.65)
|
sizeLabel.textColor = UIColor.white.withAlphaComponent(0.52)
|
||||||
sizeLabel.font = .systemFont(ofSize: 11, weight: .regular)
|
sizeLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
sizeLabel.textAlignment = .center
|
sizeLabel.textAlignment = .center
|
||||||
|
|
||||||
counterLabel.textColor = .white
|
counterLabel.textColor = UIColor.white.withAlphaComponent(0.82)
|
||||||
counterLabel.font = .monospacedDigitSystemFont(ofSize: 14, weight: .medium)
|
counterLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .medium)
|
||||||
counterLabel.textAlignment = .right
|
counterLabel.textAlignment = .center
|
||||||
|
counterLabel.backgroundColor = UIColor(white: 0.11, alpha: 1)
|
||||||
|
counterLabel.layer.cornerRadius = 17
|
||||||
|
counterLabel.clipsToBounds = true
|
||||||
counterLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
counterLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
tabStack.axis = .horizontal
|
variantSegmentedControl.selectedSegmentTintColor = UIColor(white: 0.22, alpha: 1)
|
||||||
tabStack.distribution = .fillEqually
|
variantSegmentedControl.backgroundColor = UIColor(white: 0.065, alpha: 1)
|
||||||
tabStack.alignment = .fill
|
variantSegmentedControl.setTitleTextAttributes(
|
||||||
|
[.foregroundColor: UIColor.white.withAlphaComponent(0.62), .font: UIFont.systemFont(ofSize: 15)],
|
||||||
|
for: .normal
|
||||||
|
)
|
||||||
|
variantSegmentedControl.setTitleTextAttributes(
|
||||||
|
[.foregroundColor: UIColor.white, .font: UIFont.systemFont(ofSize: 15, weight: .semibold)],
|
||||||
|
for: .selected
|
||||||
|
)
|
||||||
|
variantSegmentedControl.accessibilityIdentifier = "travelAlbum.previewVariantSegmentedControl"
|
||||||
|
variantSegmentedControl.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
|
||||||
actionStack.axis = .horizontal
|
actionStack.axis = .horizontal
|
||||||
actionStack.distribution = .fillEqually
|
actionStack.distribution = .fillEqually
|
||||||
actionStack.alignment = .fill
|
actionStack.alignment = .fill
|
||||||
|
configureHighResolutionButton()
|
||||||
configureActions()
|
configureActions()
|
||||||
|
|
||||||
let titleStack = UIStackView(arrangedSubviews: [titleLabel, sizeLabel])
|
let titleStack = UIStackView(arrangedSubviews: [titleLabel, sizeLabel])
|
||||||
@@ -163,23 +193,25 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
topChrome.addSubview(counterLabel)
|
topChrome.addSubview(counterLabel)
|
||||||
view.addSubview(bottomChrome)
|
view.addSubview(bottomChrome)
|
||||||
bottomChrome.addSubview(divider)
|
bottomChrome.addSubview(divider)
|
||||||
bottomChrome.addSubview(tabStack)
|
bottomChrome.addSubview(variantControls)
|
||||||
|
variantControls.addSubview(variantSegmentedControl)
|
||||||
|
variantControls.addSubview(highResolutionButton)
|
||||||
bottomChrome.addSubview(actionStack)
|
bottomChrome.addSubview(actionStack)
|
||||||
|
|
||||||
collectionView.snp.makeConstraints { $0.edges.equalToSuperview() }
|
|
||||||
topChrome.snp.makeConstraints { make in
|
topChrome.snp.makeConstraints { make in
|
||||||
make.top.leading.trailing.equalToSuperview()
|
make.top.leading.trailing.equalToSuperview()
|
||||||
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.top).offset(64)
|
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.top).offset(72)
|
||||||
}
|
}
|
||||||
closeButton.snp.makeConstraints { make in
|
closeButton.snp.makeConstraints { make in
|
||||||
make.leading.equalToSuperview().offset(8)
|
make.leading.equalToSuperview().offset(10)
|
||||||
make.bottom.equalToSuperview().offset(-10)
|
make.bottom.equalToSuperview().offset(-14)
|
||||||
make.size.equalTo(44)
|
make.size.equalTo(44)
|
||||||
}
|
}
|
||||||
counterLabel.snp.makeConstraints { make in
|
counterLabel.snp.makeConstraints { make in
|
||||||
make.trailing.equalToSuperview().inset(16)
|
make.trailing.equalToSuperview().inset(16)
|
||||||
make.centerY.equalTo(closeButton)
|
make.centerY.equalTo(closeButton)
|
||||||
make.width.greaterThanOrEqualTo(52)
|
make.width.greaterThanOrEqualTo(48)
|
||||||
|
make.height.equalTo(34)
|
||||||
}
|
}
|
||||||
titleStack.snp.makeConstraints { make in
|
titleStack.snp.makeConstraints { make in
|
||||||
make.centerY.equalTo(closeButton)
|
make.centerY.equalTo(closeButton)
|
||||||
@@ -192,40 +224,72 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
bottomChrome.snp.makeConstraints { make in
|
bottomChrome.snp.makeConstraints { make in
|
||||||
make.leading.trailing.bottom.equalToSuperview()
|
make.leading.trailing.bottom.equalToSuperview()
|
||||||
}
|
}
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(topChrome.snp.bottom)
|
||||||
|
make.leading.trailing.equalToSuperview()
|
||||||
|
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-126.5)
|
||||||
|
}
|
||||||
divider.snp.makeConstraints { make in
|
divider.snp.makeConstraints { make in
|
||||||
make.top.leading.trailing.equalToSuperview()
|
make.top.leading.trailing.equalToSuperview()
|
||||||
make.height.equalTo(0.5)
|
make.height.equalTo(0.5)
|
||||||
}
|
}
|
||||||
tabStack.snp.makeConstraints { make in
|
variantControls.snp.makeConstraints { make in
|
||||||
make.top.equalTo(divider.snp.bottom)
|
make.top.equalTo(divider.snp.bottom)
|
||||||
make.leading.trailing.equalToSuperview().inset(12)
|
make.leading.trailing.equalToSuperview()
|
||||||
tabHeightConstraint = make.height.equalTo(0).constraint
|
variantControlsHeightConstraint = make.height.equalTo(56).constraint
|
||||||
|
}
|
||||||
|
variantSegmentedControl.snp.makeConstraints { make in
|
||||||
|
make.leading.equalToSuperview().offset(20)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.height.equalTo(40)
|
||||||
|
make.trailing.equalTo(highResolutionButton.snp.leading).offset(-12)
|
||||||
|
}
|
||||||
|
highResolutionButton.snp.makeConstraints { make in
|
||||||
|
make.trailing.equalToSuperview().inset(18)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.width.equalTo(116)
|
||||||
|
make.height.equalTo(40)
|
||||||
}
|
}
|
||||||
actionStack.snp.makeConstraints { make in
|
actionStack.snp.makeConstraints { make in
|
||||||
make.top.equalTo(tabStack.snp.bottom)
|
make.top.equalTo(variantControls.snp.bottom)
|
||||||
make.leading.trailing.equalToSuperview().inset(8)
|
make.leading.trailing.equalToSuperview().inset(8)
|
||||||
make.height.equalTo(70)
|
make.height.equalTo(70)
|
||||||
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureHighResolutionButton() {
|
||||||
|
var configuration = UIButton.Configuration.filled()
|
||||||
|
configuration.title = "查看高清图"
|
||||||
|
configuration.baseForegroundColor = .white
|
||||||
|
configuration.baseBackgroundColor = UIColor(white: 0.18, alpha: 1)
|
||||||
|
configuration.background.cornerRadius = 20
|
||||||
|
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
||||||
|
var attributes = attributes
|
||||||
|
attributes.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
|
highResolutionButton.configuration = configuration
|
||||||
|
highResolutionButton.accessibilityLabel = "查看高清图"
|
||||||
|
highResolutionButton.accessibilityIdentifier = "travelAlbum.previewHighResolutionButton"
|
||||||
|
highResolutionButton.addTarget(self, action: #selector(highResolutionTapped), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
private func configureActions() {
|
private func configureActions() {
|
||||||
let aiButton = makeActionButton(title: "AI修图", systemName: "wand.and.stars", color: UIColor(hex: 0x60A5FA))
|
let aiButton = makeActionButton(title: "AI修图", systemName: "tag", color: .white)
|
||||||
let deleteConfiguration = makeActionButton(
|
let deleteConfiguration = makeActionButton(
|
||||||
title: "删除",
|
title: "删除",
|
||||||
systemName: "trash",
|
systemName: "trash",
|
||||||
color: UIColor(hex: 0xF87171)
|
color: .white
|
||||||
).configuration
|
).configuration
|
||||||
deleteButton.configuration = deleteConfiguration
|
deleteButton.configuration = deleteConfiguration
|
||||||
deleteButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
|
||||||
deleteButton.accessibilityLabel = "删除"
|
deleteButton.accessibilityLabel = "删除"
|
||||||
deleteButton.accessibilityIdentifier = "travelAlbum.previewDeleteButton"
|
deleteButton.accessibilityIdentifier = "travelAlbum.previewDeleteButton"
|
||||||
refreshButton.configuration = makeActionButton(
|
refreshButton.configuration = makeActionButton(
|
||||||
title: "刷新",
|
title: "刷新",
|
||||||
systemName: "arrow.clockwise",
|
systemName: "arrow.counterclockwise.circle",
|
||||||
color: .white
|
color: .white
|
||||||
).configuration
|
).configuration
|
||||||
refreshButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
|
||||||
refreshButton.accessibilityLabel = "刷新"
|
refreshButton.accessibilityLabel = "刷新"
|
||||||
refreshButton.accessibilityIdentifier = "travelAlbum.previewRefreshButton"
|
refreshButton.accessibilityIdentifier = "travelAlbum.previewRefreshButton"
|
||||||
aiButton.addTarget(self, action: #selector(aiTapped), for: .touchUpInside)
|
aiButton.addTarget(self, action: #selector(aiTapped), for: .touchUpInside)
|
||||||
@@ -239,11 +303,19 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
configuration.title = title
|
configuration.title = title
|
||||||
configuration.image = UIImage(systemName: systemName)
|
configuration.image = UIImage(systemName: systemName)
|
||||||
configuration.imagePlacement = .top
|
configuration.imagePlacement = .top
|
||||||
configuration.imagePadding = 5
|
configuration.imagePadding = 8
|
||||||
|
configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(
|
||||||
|
pointSize: 25,
|
||||||
|
weight: .regular
|
||||||
|
)
|
||||||
configuration.baseForegroundColor = color
|
configuration.baseForegroundColor = color
|
||||||
configuration.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6)
|
configuration.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 6, bottom: 8, trailing: 6)
|
||||||
|
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
||||||
|
var attributes = attributes
|
||||||
|
attributes.font = .systemFont(ofSize: 14, weight: .regular)
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
let button = UIButton(configuration: configuration)
|
let button = UIButton(configuration: configuration)
|
||||||
button.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
|
||||||
button.accessibilityLabel = title
|
button.accessibilityLabel = title
|
||||||
return button
|
return button
|
||||||
}
|
}
|
||||||
@@ -278,45 +350,100 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
sizeLabel.text = TravelAlbumDisplayFormatter.fileSizeText(asset?.fileSize ?? 0)
|
sizeLabel.text = TravelAlbumDisplayFormatter.fileSizeText(asset?.fileSize ?? 0)
|
||||||
counterLabel.text = "\(node.projectIndex + 1)/\(max(totalCount, projects.count))"
|
counterLabel.text = "\(node.projectIndex + 1)/\(max(totalCount, projects.count))"
|
||||||
counterLabel.accessibilityLabel = "第 \(node.projectIndex + 1) 张,共 \(max(totalCount, projects.count)) 张"
|
counterLabel.accessibilityLabel = "第 \(node.projectIndex + 1) 张,共 \(max(totalCount, projects.count)) 张"
|
||||||
|
highResolutionButton.isEnabled = asset != nil
|
||||||
|
highResolutionButton.alpha = asset == nil ? 0.45 : 1
|
||||||
rebuildTabs()
|
rebuildTabs()
|
||||||
loadMoreIfNeeded(projectIndex: node.projectIndex)
|
loadMoreIfNeeded(projectIndex: node.projectIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rebuildTabs() {
|
private func rebuildTabs() {
|
||||||
tabStack.arrangedSubviews.forEach {
|
while variantSegmentedControl.numberOfSegments > 0 {
|
||||||
tabStack.removeArrangedSubview($0)
|
variantSegmentedControl.removeSegment(at: 0, animated: false)
|
||||||
$0.removeFromSuperview()
|
|
||||||
}
|
}
|
||||||
guard let project = currentProject, project.hasVariants else {
|
guard let project = currentProject, project.hasVariants else {
|
||||||
tabStack.isHidden = true
|
setVariantControlsVisible(false)
|
||||||
tabHeightConstraint?.update(offset: 0)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tabStack.isHidden = false
|
|
||||||
tabHeightConstraint?.update(offset: 44)
|
|
||||||
let activeKind = currentAsset?.kind ?? .original
|
let activeKind = currentAsset?.kind ?? .original
|
||||||
for asset in project.orderedAssets {
|
for (index, asset) in project.orderedAssets.enumerated() {
|
||||||
let button = UIButton(type: .system)
|
variantSegmentedControl.insertSegment(withTitle: asset.kind.title, at: index, animated: false)
|
||||||
button.tag = asset.kind.rawValue
|
|
||||||
button.setTitle(asset.kind.title, for: .normal)
|
|
||||||
button.titleLabel?.font = .systemFont(ofSize: 13, weight: asset.kind == activeKind ? .semibold : .medium)
|
|
||||||
button.setTitleColor(asset.kind == activeKind ? .white : UIColor.white.withAlphaComponent(0.55), for: .normal)
|
|
||||||
button.backgroundColor = asset.kind == activeKind ? UIColor.white.withAlphaComponent(0.12) : .clear
|
|
||||||
button.layer.cornerRadius = 8
|
|
||||||
button.accessibilityLabel = asset.kind.title
|
|
||||||
button.accessibilityTraits = asset.kind == activeKind ? [.button, .selected] : .button
|
|
||||||
button.addTarget(self, action: #selector(tabTapped(_:)), for: .touchUpInside)
|
|
||||||
tabStack.addArrangedSubview(button)
|
|
||||||
}
|
|
||||||
view.layoutIfNeeded()
|
|
||||||
if !chromeVisible {
|
|
||||||
bottomChrome.transform = CGAffineTransform(translationX: 0, y: bottomChrome.bounds.height)
|
|
||||||
}
|
}
|
||||||
|
variantSegmentedControl.selectedSegmentIndex = project.orderedAssets.firstIndex {
|
||||||
|
$0.kind == activeKind
|
||||||
|
} ?? 0
|
||||||
|
setVariantControlsVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func tabTapped(_ sender: UIButton) {
|
private func setVariantControlsVisible(_ visible: Bool) {
|
||||||
guard let kind = TravelAlbumPreviewAssetKind(rawValue: sender.tag),
|
let visibilityChanged = didConfigureVariantControls && showsVariantControls != visible
|
||||||
let node = currentNode,
|
didConfigureVariantControls = true
|
||||||
|
showsVariantControls = visible
|
||||||
|
variantControlsAnimationGeneration += 1
|
||||||
|
let animationGeneration = variantControlsAnimationGeneration
|
||||||
|
let shouldAnimate = visibilityChanged
|
||||||
|
&& chromeVisible
|
||||||
|
&& view.window != nil
|
||||||
|
&& UIView.areAnimationsEnabled
|
||||||
|
&& !UIAccessibility.isReduceMotionEnabled
|
||||||
|
|
||||||
|
view.layoutIfNeeded()
|
||||||
|
variantControls.isHidden = false
|
||||||
|
variantSegmentedControl.isHidden = false
|
||||||
|
highResolutionButton.isHidden = false
|
||||||
|
variantControlsHeightConstraint?.update(offset: visible ? 56 : 0)
|
||||||
|
|
||||||
|
guard shouldAnimate else {
|
||||||
|
variantControls.alpha = 1
|
||||||
|
variantControls.transform = .identity
|
||||||
|
view.layoutIfNeeded()
|
||||||
|
variantControls.isHidden = !visible
|
||||||
|
variantSegmentedControl.isHidden = !visible
|
||||||
|
highResolutionButton.isHidden = !visible
|
||||||
|
updateHiddenBottomChromePositionIfNeeded()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if visible {
|
||||||
|
variantControls.alpha = 0
|
||||||
|
variantControls.transform = CGAffineTransform(translationX: 0, y: 8)
|
||||||
|
}
|
||||||
|
UIView.animate(
|
||||||
|
withDuration: 0.24,
|
||||||
|
delay: 0,
|
||||||
|
options: [.curveEaseInOut, .beginFromCurrentState, .allowUserInteraction],
|
||||||
|
animations: {
|
||||||
|
self.variantControls.alpha = visible ? 1 : 0
|
||||||
|
self.variantControls.transform = visible
|
||||||
|
? .identity
|
||||||
|
: CGAffineTransform(translationX: 0, y: 8)
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
},
|
||||||
|
completion: { [weak self] finished in
|
||||||
|
guard let self,
|
||||||
|
finished,
|
||||||
|
self.variantControlsAnimationGeneration == animationGeneration
|
||||||
|
else { return }
|
||||||
|
self.variantControls.isHidden = !visible
|
||||||
|
self.variantSegmentedControl.isHidden = !visible
|
||||||
|
self.highResolutionButton.isHidden = !visible
|
||||||
|
self.variantControls.alpha = 1
|
||||||
|
self.variantControls.transform = .identity
|
||||||
|
self.updateHiddenBottomChromePositionIfNeeded()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHiddenBottomChromePositionIfNeeded() {
|
||||||
|
guard !chromeVisible else { return }
|
||||||
|
bottomChrome.transform = CGAffineTransform(translationX: 0, y: bottomChrome.bounds.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func segmentChanged(_ sender: UISegmentedControl) {
|
||||||
|
guard let project = currentProject,
|
||||||
|
project.orderedAssets.indices.contains(sender.selectedSegmentIndex)
|
||||||
|
else { return }
|
||||||
|
let kind = project.orderedAssets[sender.selectedSegmentIndex].kind
|
||||||
|
guard let node = currentNode,
|
||||||
currentProject?.asset(for: kind) != nil
|
currentProject?.asset(for: kind) != nil
|
||||||
else { return }
|
else { return }
|
||||||
switch configuration.swipeMode {
|
switch configuration.swipeMode {
|
||||||
@@ -454,6 +581,11 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func highResolutionTapped() {
|
||||||
|
guard chromeVisible else { return }
|
||||||
|
toggleChrome()
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func aiTapped() {
|
@objc private func aiTapped() {
|
||||||
guard presentedViewController == nil else { return }
|
guard presentedViewController == nil else { return }
|
||||||
guard let project = currentProject else { return }
|
guard let project = currentProject else { return }
|
||||||
@@ -661,7 +793,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
|||||||
refreshButton.accessibilityValue = isRefreshingProject ? "刷新中" : nil
|
refreshButton.accessibilityValue = isRefreshingProject ? "刷新中" : nil
|
||||||
var configuration = refreshButton.configuration
|
var configuration = refreshButton.configuration
|
||||||
configuration?.showsActivityIndicator = isRefreshingProject
|
configuration?.showsActivityIndicator = isRefreshingProject
|
||||||
configuration?.image = isRefreshingProject ? nil : UIImage(systemName: "arrow.clockwise")
|
configuration?.image = isRefreshingProject ? nil : UIImage(systemName: "arrow.counterclockwise.circle")
|
||||||
configuration?.title = isRefreshingProject ? "刷新中" : "刷新"
|
configuration?.title = isRefreshingProject ? "刷新中" : "刷新"
|
||||||
refreshButton.configuration = configuration
|
refreshButton.configuration = configuration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,12 +184,13 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
|||||||
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||||
controller.view.layoutIfNeeded()
|
controller.view.layoutIfNeeded()
|
||||||
|
|
||||||
let retouchedButton = try XCTUnwrap(
|
let segmentedControl = try XCTUnwrap(
|
||||||
controller.view.findSubview {
|
controller.view.findSubview {
|
||||||
($0 as? UIButton)?.accessibilityLabel == "精修后"
|
$0.accessibilityIdentifier == "travelAlbum.previewVariantSegmentedControl"
|
||||||
} as? UIButton
|
} as? UISegmentedControl
|
||||||
)
|
)
|
||||||
retouchedButton.sendActions(for: .touchUpInside)
|
segmentedControl.selectedSegmentIndex = 1
|
||||||
|
segmentedControl.sendActions(for: .valueChanged)
|
||||||
controller.view.layoutIfNeeded()
|
controller.view.layoutIfNeeded()
|
||||||
|
|
||||||
let imageView = try XCTUnwrap(
|
let imageView = try XCTUnwrap(
|
||||||
@@ -205,6 +206,148 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
|||||||
XCTAssertFalse(imageView.constraintsAffectingLayout(for: .vertical).isEmpty)
|
XCTAssertFalse(imageView.constraintsAffectingLayout(for: .vertical).isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPreviewUsesScreenshotChromeAndConstrainedImageViewport() throws {
|
||||||
|
UIView.setAnimationsEnabled(false)
|
||||||
|
defer { UIView.setAnimationsEnabled(true) }
|
||||||
|
|
||||||
|
let project = TravelAlbumPreviewProject(
|
||||||
|
originalMaterialId: 1,
|
||||||
|
assets: [
|
||||||
|
TravelAlbumPreviewAsset(
|
||||||
|
id: "original-1",
|
||||||
|
kind: .original,
|
||||||
|
fileURL: "https://example.com/original.jpg",
|
||||||
|
coverURL: "https://example.com/cover.jpg",
|
||||||
|
fileName: "IMG_0002.jpeg",
|
||||||
|
fileSize: 2_516_582
|
||||||
|
),
|
||||||
|
TravelAlbumPreviewAsset(
|
||||||
|
id: "retouched-1",
|
||||||
|
kind: .retouched,
|
||||||
|
fileURL: "",
|
||||||
|
coverURL: "",
|
||||||
|
fileName: "IMG_0002.jpeg",
|
||||||
|
fileSize: 2_516_582
|
||||||
|
),
|
||||||
|
TravelAlbumPreviewAsset(
|
||||||
|
id: "atmosphere-1",
|
||||||
|
kind: .atmosphere,
|
||||||
|
fileURL: "",
|
||||||
|
coverURL: "",
|
||||||
|
fileName: "IMG_0002.jpeg",
|
||||||
|
fileSize: 2_516_582
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
let controller = TravelAlbumPhotoPreviewViewController(
|
||||||
|
projects: [project],
|
||||||
|
totalCount: 10,
|
||||||
|
startProjectIndex: 0
|
||||||
|
)
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||||
|
controller.view.layoutIfNeeded()
|
||||||
|
|
||||||
|
let topChrome = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewTopChrome"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let bottomChrome = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewBottomChrome"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let collectionView = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewCollectionView"
|
||||||
|
} as? UICollectionView
|
||||||
|
)
|
||||||
|
let highResolutionButton = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewHighResolutionButton"
|
||||||
|
} as? UIButton
|
||||||
|
)
|
||||||
|
let segmentedControl = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewVariantSegmentedControl"
|
||||||
|
} as? UISegmentedControl
|
||||||
|
)
|
||||||
|
let backButton = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewBackButton"
|
||||||
|
} as? UIButton
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertFalse(controller.prefersStatusBarHidden)
|
||||||
|
XCTAssertEqual(topChrome.backgroundColor, .black)
|
||||||
|
XCTAssertEqual(bottomChrome.backgroundColor, .black)
|
||||||
|
XCTAssertEqual(highResolutionButton.configuration?.title, "查看高清图")
|
||||||
|
XCTAssertEqual(segmentedControl.numberOfSegments, 3)
|
||||||
|
XCTAssertEqual(segmentedControl.selectedSegmentIndex, 0)
|
||||||
|
XCTAssertEqual(backButton.bounds.size, CGSize(width: 44, height: 44))
|
||||||
|
XCTAssertNil(backButton.configuration?.baseBackgroundColor)
|
||||||
|
XCTAssertEqual(collectionView.frame.minY, topChrome.frame.maxY, accuracy: 0.5)
|
||||||
|
XCTAssertEqual(collectionView.frame.maxY, bottomChrome.frame.minY, accuracy: 0.5)
|
||||||
|
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "IMG_0002.jpeg" })
|
||||||
|
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "2.4MB" })
|
||||||
|
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "1/10" })
|
||||||
|
|
||||||
|
highResolutionButton.sendActions(for: .touchUpInside)
|
||||||
|
|
||||||
|
XCTAssertEqual(topChrome.alpha, 0)
|
||||||
|
XCTAssertEqual(bottomChrome.alpha, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPreviewHidesVariantControlsAndHighResolutionButtonWithoutVariants() throws {
|
||||||
|
let project = TravelAlbumPreviewProject(
|
||||||
|
originalMaterialId: 1,
|
||||||
|
assets: [
|
||||||
|
TravelAlbumPreviewAsset(
|
||||||
|
id: "original-1",
|
||||||
|
kind: .original,
|
||||||
|
fileURL: "https://example.com/original.jpg",
|
||||||
|
coverURL: "https://example.com/cover.jpg",
|
||||||
|
fileName: "IMG_0002.jpeg",
|
||||||
|
fileSize: 2_516_582
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
let controller = TravelAlbumPhotoPreviewViewController(
|
||||||
|
projects: [project],
|
||||||
|
totalCount: 1,
|
||||||
|
startProjectIndex: 0
|
||||||
|
)
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||||
|
controller.view.layoutIfNeeded()
|
||||||
|
|
||||||
|
let segmentedControl = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewVariantSegmentedControl"
|
||||||
|
} as? UISegmentedControl
|
||||||
|
)
|
||||||
|
let highResolutionButton = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewHighResolutionButton"
|
||||||
|
} as? UIButton
|
||||||
|
)
|
||||||
|
let collectionView = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewCollectionView"
|
||||||
|
} as? UICollectionView
|
||||||
|
)
|
||||||
|
let bottomChrome = try XCTUnwrap(
|
||||||
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewBottomChrome"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertTrue(segmentedControl.isHidden)
|
||||||
|
XCTAssertTrue(highResolutionButton.isHidden)
|
||||||
|
XCTAssertEqual(bottomChrome.frame.minY - collectionView.frame.maxY, 56, accuracy: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
func testSelectionModeShowsAIRetouchOnLeftAndDeleteOnRight() async throws {
|
func testSelectionModeShowsAIRetouchOnLeftAndDeleteOnRight() async throws {
|
||||||
let controller = TravelAlbumDetailViewController(albumId: 0)
|
let controller = TravelAlbumDetailViewController(albumId: 0)
|
||||||
controller.loadViewIfNeeded()
|
controller.loadViewIfNeeded()
|
||||||
@@ -479,10 +622,13 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
|||||||
controller.loadViewIfNeeded()
|
controller.loadViewIfNeeded()
|
||||||
controller.view.layoutIfNeeded()
|
controller.view.layoutIfNeeded()
|
||||||
|
|
||||||
let retouchedButton = try XCTUnwrap(
|
let segmentedControl = try XCTUnwrap(
|
||||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "精修后" } as? UIButton
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewVariantSegmentedControl"
|
||||||
|
} as? UISegmentedControl
|
||||||
)
|
)
|
||||||
retouchedButton.sendActions(for: .touchUpInside)
|
segmentedControl.selectedSegmentIndex = 1
|
||||||
|
segmentedControl.sendActions(for: .valueChanged)
|
||||||
let aiButton = try XCTUnwrap(
|
let aiButton = try XCTUnwrap(
|
||||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "AI修图" } as? UIButton
|
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "AI修图" } as? UIButton
|
||||||
)
|
)
|
||||||
@@ -563,10 +709,13 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
|||||||
controller.loadViewIfNeeded()
|
controller.loadViewIfNeeded()
|
||||||
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
||||||
controller.view.layoutIfNeeded()
|
controller.view.layoutIfNeeded()
|
||||||
let retouchedButton = try XCTUnwrap(
|
let segmentedControl = try XCTUnwrap(
|
||||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "精修后" } as? UIButton
|
controller.view.findSubview {
|
||||||
|
$0.accessibilityIdentifier == "travelAlbum.previewVariantSegmentedControl"
|
||||||
|
} as? UISegmentedControl
|
||||||
)
|
)
|
||||||
retouchedButton.sendActions(for: .touchUpInside)
|
segmentedControl.selectedSegmentIndex = 1
|
||||||
|
segmentedControl.sendActions(for: .valueChanged)
|
||||||
let refreshButton = try XCTUnwrap(
|
let refreshButton = try XCTUnwrap(
|
||||||
controller.view.findSubview {
|
controller.view.findSubview {
|
||||||
$0.accessibilityIdentifier == "travelAlbum.previewRefreshButton"
|
$0.accessibilityIdentifier == "travelAlbum.previewRefreshButton"
|
||||||
@@ -575,18 +724,12 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
|||||||
|
|
||||||
refreshButton.sendActions(for: .touchUpInside)
|
refreshButton.sendActions(for: .touchUpInside)
|
||||||
await waitUntil { reloadCount == 1 && refreshButton.isEnabled }
|
await waitUntil { reloadCount == 1 && refreshButton.isEnabled }
|
||||||
XCTAssertTrue(
|
XCTAssertEqual(segmentedControl.selectedSegmentIndex, 1)
|
||||||
controller.view.findSubview {
|
|
||||||
($0 as? UIButton)?.accessibilityLabel == "精修后"
|
|
||||||
&& $0.accessibilityTraits.contains(.selected)
|
|
||||||
} != nil
|
|
||||||
)
|
|
||||||
|
|
||||||
refreshButton.sendActions(for: .touchUpInside)
|
refreshButton.sendActions(for: .touchUpInside)
|
||||||
await waitUntil { reloadCount == 2 && refreshButton.isEnabled }
|
await waitUntil { reloadCount == 2 && refreshButton.isEnabled }
|
||||||
XCTAssertNil(
|
XCTAssertTrue(segmentedControl.isHidden)
|
||||||
controller.view.findSubview { ($0 as? UIButton)?.accessibilityLabel == "精修后" }
|
XCTAssertEqual(segmentedControl.numberOfSegments, 0)
|
||||||
)
|
|
||||||
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "原图.jpg" })
|
XCTAssertTrue(controller.view.allLabels().contains { $0.text == "原图.jpg" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user