274 lines
11 KiB
Swift
274 lines
11 KiB
Swift
//
|
|
// MediaGridContainerView.swift
|
|
// MediaGridContainerView
|
|
//
|
|
// Created by Cirno MainasuK on 2021-8-23.
|
|
// Copyright © 2021 Twidere. All rights reserved.
|
|
//
|
|
|
|
import os.log
|
|
import UIKit
|
|
import func AVFoundation.AVMakeRect
|
|
|
|
public protocol MediaGridContainerViewDelegate: AnyObject {
|
|
func mediaGridContainerView(_ container: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int)
|
|
func mediaGridContainerView(_ container: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
|
}
|
|
|
|
public final class MediaGridContainerView: UIView {
|
|
|
|
static let sensitiveToggleButtonSize = CGSize(width: 34, height: 34)
|
|
public static let maxCount = 9
|
|
|
|
let logger = Logger(subsystem: "MediaGridContainerView", category: "UI")
|
|
|
|
public weak var delegate: MediaGridContainerViewDelegate?
|
|
public private(set) lazy var viewModel: ViewModel = {
|
|
let viewModel = ViewModel()
|
|
viewModel.bind(view: self)
|
|
return viewModel
|
|
}()
|
|
|
|
// lazy var is required here to setup gesture recognizer target-action
|
|
// Swift not doesn't emit compiler error if without `lazy` here
|
|
private(set) lazy var _mediaViews: [MediaView] = {
|
|
var mediaViews: [MediaView] = []
|
|
for i in 0..<MediaGridContainerView.maxCount {
|
|
// init media view
|
|
let mediaView = MediaView()
|
|
mediaView.tag = i
|
|
mediaViews.append(mediaView)
|
|
|
|
// add gesture recognizer
|
|
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
|
tapGesture.addTarget(self, action: #selector(MediaGridContainerView.mediaViewTapGestureRecognizerHandler(_:)))
|
|
mediaView.container.addGestureRecognizer(tapGesture)
|
|
mediaView.container.isUserInteractionEnabled = true
|
|
}
|
|
return mediaViews
|
|
}()
|
|
|
|
let contentWarningOverlay = ContentWarningOverlayView()
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
_init()
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
_init()
|
|
}
|
|
|
|
public override var accessibilityElements: [Any]? {
|
|
get {
|
|
mediaViews
|
|
}
|
|
set { }
|
|
}
|
|
|
|
}
|
|
|
|
extension MediaGridContainerView {
|
|
private func _init() {
|
|
contentWarningOverlay.isUserInteractionEnabled = false
|
|
contentWarningOverlay.isHidden = true
|
|
}
|
|
}
|
|
|
|
extension MediaGridContainerView {
|
|
@objc private func mediaViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
|
guard let index = _mediaViews.firstIndex(where: { $0.container === sender.view }) else { return }
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(index)")
|
|
let mediaView = _mediaViews[index]
|
|
delegate?.mediaGridContainerView(self, didTapMediaView: mediaView, at: index)
|
|
}
|
|
|
|
@objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) {
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
delegate?.mediaGridContainerView(self, mediaSensitiveButtonDidPressed: sender)
|
|
}
|
|
}
|
|
|
|
extension MediaGridContainerView {
|
|
|
|
public func dequeueMediaView(adaptiveLayout layout: AdaptiveLayout) -> MediaView {
|
|
prepareForReuse()
|
|
|
|
let mediaView = _mediaViews[0]
|
|
layout.layout(in: self, mediaView: mediaView)
|
|
|
|
layoutContentWarningOverlay()
|
|
bringSubviewToFront(contentWarningOverlay)
|
|
|
|
return mediaView
|
|
}
|
|
|
|
public func dequeueMediaView(gridLayout layout: GridLayout) -> [MediaView] {
|
|
prepareForReuse()
|
|
|
|
let mediaViews = Array(_mediaViews[0..<layout.count])
|
|
layout.layout(in: self, mediaViews: mediaViews)
|
|
|
|
layoutContentWarningOverlay()
|
|
bringSubviewToFront(contentWarningOverlay)
|
|
|
|
return mediaViews
|
|
}
|
|
|
|
public func prepareForReuse() {
|
|
_mediaViews.forEach { view in
|
|
view.removeFromSuperview()
|
|
view.removeConstraints(view.constraints)
|
|
view.prepareForReuse()
|
|
}
|
|
|
|
subviews.forEach { view in
|
|
view.removeFromSuperview()
|
|
}
|
|
|
|
removeConstraints(constraints)
|
|
}
|
|
|
|
}
|
|
|
|
extension MediaGridContainerView {
|
|
private func layoutContentWarningOverlay() {
|
|
contentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(contentWarningOverlay)
|
|
contentWarningOverlay.pinToParent()
|
|
}
|
|
}
|
|
|
|
extension MediaGridContainerView {
|
|
|
|
public var mediaViews: [MediaView] {
|
|
_mediaViews.filter { $0.superview != nil }
|
|
}
|
|
|
|
public func setAlpha(_ alpha: CGFloat) {
|
|
_mediaViews.forEach { $0.alpha = alpha }
|
|
}
|
|
|
|
public func setAlpha(_ alpha: CGFloat, index: Int) {
|
|
if index < _mediaViews.count {
|
|
_mediaViews[index].alpha = alpha
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension MediaGridContainerView {
|
|
public struct AdaptiveLayout {
|
|
let aspectRatio: CGSize
|
|
let maxSize: CGSize
|
|
|
|
func layout(in view: UIView, mediaView: MediaView) {
|
|
let imageViewSize = AVMakeRect(aspectRatio: aspectRatio, insideRect: CGRect(origin: .zero, size: maxSize)).size
|
|
mediaView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(mediaView)
|
|
NSLayoutConstraint.activate([
|
|
mediaView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
mediaView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
mediaView.trailingAnchor.constraint(equalTo: view.trailingAnchor).priority(.defaultLow),
|
|
mediaView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
mediaView.widthAnchor.constraint(equalToConstant: imageViewSize.width).priority(.required - 1),
|
|
mediaView.heightAnchor.constraint(equalToConstant: imageViewSize.height).priority(.required - 1),
|
|
])
|
|
}
|
|
}
|
|
|
|
public struct GridLayout {
|
|
static let spacing: CGFloat = 1
|
|
|
|
let count: Int
|
|
let maxSize: CGSize
|
|
|
|
init(count: Int, maxSize: CGSize) {
|
|
self.count = min(count, 9)
|
|
self.maxSize = maxSize
|
|
|
|
}
|
|
|
|
private func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView {
|
|
let stackView = UIStackView()
|
|
stackView.axis = axis
|
|
stackView.semanticContentAttribute = .forceLeftToRight
|
|
stackView.spacing = GridLayout.spacing
|
|
stackView.distribution = .fillEqually
|
|
return stackView
|
|
}
|
|
|
|
public func layout(in view: UIView, mediaViews: [MediaView]) {
|
|
let containerVerticalStackView = createStackView(axis: .vertical)
|
|
containerVerticalStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(containerVerticalStackView)
|
|
containerVerticalStackView.pinToParent()
|
|
|
|
let count = mediaViews.count
|
|
switch count {
|
|
case 1:
|
|
assertionFailure("should use Adaptive Layout")
|
|
containerVerticalStackView.addArrangedSubview(mediaViews[0])
|
|
case 2:
|
|
let horizontalStackView = createStackView(axis: .horizontal)
|
|
containerVerticalStackView.addArrangedSubview(horizontalStackView)
|
|
horizontalStackView.addArrangedSubview(mediaViews[0])
|
|
horizontalStackView.addArrangedSubview(mediaViews[1])
|
|
case 3:
|
|
let horizontalStackView = createStackView(axis: .horizontal)
|
|
containerVerticalStackView.addArrangedSubview(horizontalStackView)
|
|
horizontalStackView.addArrangedSubview(mediaViews[0])
|
|
|
|
let verticalStackView = createStackView(axis: .vertical)
|
|
horizontalStackView.addArrangedSubview(verticalStackView)
|
|
verticalStackView.addArrangedSubview(mediaViews[1])
|
|
verticalStackView.addArrangedSubview(mediaViews[2])
|
|
case 4:
|
|
let topHorizontalStackView = createStackView(axis: .horizontal)
|
|
containerVerticalStackView.addArrangedSubview(topHorizontalStackView)
|
|
topHorizontalStackView.addArrangedSubview(mediaViews[0])
|
|
topHorizontalStackView.addArrangedSubview(mediaViews[1])
|
|
|
|
let bottomHorizontalStackView = createStackView(axis: .horizontal)
|
|
containerVerticalStackView.addArrangedSubview(bottomHorizontalStackView)
|
|
bottomHorizontalStackView.addArrangedSubview(mediaViews[2])
|
|
bottomHorizontalStackView.addArrangedSubview(mediaViews[3])
|
|
case 5...9:
|
|
let topHorizontalStackView = createStackView(axis: .horizontal)
|
|
containerVerticalStackView.addArrangedSubview(topHorizontalStackView)
|
|
topHorizontalStackView.addArrangedSubview(mediaViews[0])
|
|
topHorizontalStackView.addArrangedSubview(mediaViews[1])
|
|
topHorizontalStackView.addArrangedSubview(mediaViews[2])
|
|
|
|
func mediaViewOrPlaceholderView(at index: Int) -> UIView {
|
|
return index < mediaViews.count ? mediaViews[index] : UIView()
|
|
}
|
|
let middleHorizontalStackView = createStackView(axis: .horizontal)
|
|
containerVerticalStackView.addArrangedSubview(middleHorizontalStackView)
|
|
middleHorizontalStackView.addArrangedSubview(mediaViews[3])
|
|
middleHorizontalStackView.addArrangedSubview(mediaViews[4])
|
|
middleHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 5))
|
|
|
|
if count > 6 {
|
|
let bottomHorizontalStackView = createStackView(axis: .horizontal)
|
|
containerVerticalStackView.addArrangedSubview(bottomHorizontalStackView)
|
|
bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 6))
|
|
bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 7))
|
|
bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 8))
|
|
}
|
|
default:
|
|
assertionFailure()
|
|
return
|
|
}
|
|
|
|
let containerWidth = maxSize.width
|
|
let containerHeight = count > 6 ? containerWidth : containerWidth * 2 / 3
|
|
NSLayoutConstraint.activate([
|
|
view.widthAnchor.constraint(equalToConstant: containerWidth).priority(.required - 1),
|
|
view.heightAnchor.constraint(equalToConstant: containerHeight).priority(.required - 1),
|
|
])
|
|
}
|
|
}
|
|
}
|