// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @objc(OWSMediaAlbumView) public class MediaAlbumView: UIStackView { private let items: [ConversationMediaAlbumItem] @objc public let itemViews: [MediaView] @objc public var moreItemsView: MediaView? private static let kSpacingPts: CGFloat = 2 private static let kMaxItems = 5 @available(*, unavailable, message: "use other init() instead.") required public init(coder aDecoder: NSCoder) { notImplemented() } @objc public required init(mediaCache: NSCache, items: [ConversationMediaAlbumItem], isOutgoing: Bool, maxMessageWidth: CGFloat) { self.items = items self.itemViews = MediaAlbumView.itemsToDisplay(forItems: items).map { let result = MediaView(mediaCache: mediaCache, attachment: $0.attachment, isOutgoing: isOutgoing, maxMessageWidth: maxMessageWidth) return result } super.init(frame: .zero) // UIStackView's backgroundColor property has no effect. addBackgroundView(withBackgroundColor: Colors.navigationBarBackground) createContents(maxMessageWidth: maxMessageWidth) } private func createContents(maxMessageWidth: CGFloat) { switch itemViews.count { case 0: owsFailDebug("No item views.") return case 1: // X guard let itemView = itemViews.first else { owsFailDebug("Missing item view.") return } addSubview(itemView) itemView.autoPinEdgesToSuperviewEdges() case 2: // X X // side-by-side. let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2 autoSet(viewSize: imageSize, ofViews: itemViews) for itemView in itemViews { addArrangedSubview(itemView) } self.axis = .horizontal self.spacing = MediaAlbumView.kSpacingPts case 3: // x // X x // Big on left, 2 small on right. let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3 let bigImageSize = smallImageSize * 2 + MediaAlbumView.kSpacingPts guard let leftItemView = itemViews.first else { owsFailDebug("Missing view") return } autoSet(viewSize: bigImageSize, ofViews: [leftItemView]) addArrangedSubview(leftItemView) let rightViews = Array(itemViews[1..<3]) addArrangedSubview(newRow(rowViews: rightViews, axis: .vertical, viewSize: smallImageSize)) self.axis = .horizontal self.spacing = MediaAlbumView.kSpacingPts case 4: // X X // X X // Square let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2 let topViews = Array(itemViews[0..<2]) addArrangedSubview(newRow(rowViews: topViews, axis: .horizontal, viewSize: imageSize)) let bottomViews = Array(itemViews[2..<4]) addArrangedSubview(newRow(rowViews: bottomViews, axis: .horizontal, viewSize: imageSize)) self.axis = .vertical self.spacing = MediaAlbumView.kSpacingPts default: // X X // xxx // 2 big on top, 3 small on bottom. let bigImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2 let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3 let topViews = Array(itemViews[0..<2]) addArrangedSubview(newRow(rowViews: topViews, axis: .horizontal, viewSize: bigImageSize)) let bottomViews = Array(itemViews[2..<5]) addArrangedSubview(newRow(rowViews: bottomViews, axis: .horizontal, viewSize: smallImageSize)) self.axis = .vertical self.spacing = MediaAlbumView.kSpacingPts if items.count > MediaAlbumView.kMaxItems { guard let lastView = bottomViews.last else { owsFailDebug("Missing lastView") return } moreItemsView = lastView let tintView = UIView() tintView.backgroundColor = UIColor(white: 0, alpha: 0.4) lastView.addSubview(tintView) tintView.autoPinEdgesToSuperviewEdges() let moreCount = max(1, items.count - MediaAlbumView.kMaxItems) let moreCountText = OWSFormat.formatInt(Int32(moreCount)) let moreText = String(format: NSLocalizedString("MEDIA_GALLERY_MORE_ITEMS_FORMAT", comment: "Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}."), moreCountText) let moreLabel = UILabel() moreLabel.text = moreText moreLabel.textColor = UIColor.ows_white // We don't want to use dynamic text here. moreLabel.font = UIFont.systemFont(ofSize: 24) lastView.addSubview(moreLabel) moreLabel.autoCenterInSuperview() } } for itemView in itemViews { guard moreItemsView != itemView else { // Don't display the caption indicator on // the "more" item, if any. continue } guard let index = itemViews.firstIndex(of: itemView) else { owsFailDebug("Couldn't determine index of item view.") continue } let item = items[index] guard let caption = item.caption else { continue } guard caption.count > 0 else { continue } guard let icon = UIImage(named: "media_album_caption") else { owsFailDebug("Couldn't load icon.") continue } let iconView = UIImageView(image: icon) itemView.addSubview(iconView) itemView.layoutMargins = .zero iconView.autoPinTopToSuperviewMargin(withInset: 6) iconView.autoPinLeadingToSuperviewMargin(withInset: 6) } } private func autoSet(viewSize: CGFloat, ofViews views: [MediaView]) { for itemView in views { itemView.autoSetDimensions(to: CGSize(width: viewSize, height: viewSize)) } } private func newRow(rowViews: [MediaView], axis: NSLayoutConstraint.Axis, viewSize: CGFloat) -> UIStackView { autoSet(viewSize: viewSize, ofViews: rowViews) return newRow(rowViews: rowViews, axis: axis) } private func newRow(rowViews: [MediaView], axis: NSLayoutConstraint.Axis) -> UIStackView { let stackView = UIStackView(arrangedSubviews: rowViews) stackView.axis = axis stackView.spacing = MediaAlbumView.kSpacingPts return stackView } @objc public func loadMedia() { for itemView in itemViews { itemView.loadMedia() } } @objc public func unloadMedia() { for itemView in itemViews { itemView.unloadMedia() } } private class func itemsToDisplay(forItems items: [ConversationMediaAlbumItem]) -> [ConversationMediaAlbumItem] { // TODO: Unless design changes, we want to display // items which are still downloading and invalid // items. let validItems = items guard validItems.count < kMaxItems else { return Array(validItems[0.. CGSize { let itemCount = itemsToDisplay(forItems: items).count switch itemCount { case 0, 1, 4: // X // // or // // XX // XX // Square return CGSize(width: maxMessageWidth, height: maxMessageWidth) case 2: // X X // side-by-side. let imageSize = (maxMessageWidth - kSpacingPts) / 2 return CGSize(width: maxMessageWidth, height: imageSize) case 3: // x // X x // Big on left, 2 small on right. let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3 let bigImageSize = smallImageSize * 2 + kSpacingPts return CGSize(width: maxMessageWidth, height: bigImageSize) default: // X X // xxx // 2 big on top, 3 small on bottom. let bigImageSize = (maxMessageWidth - kSpacingPts) / 2 let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3 return CGSize(width: maxMessageWidth, height: bigImageSize + smallImageSize + kSpacingPts) } } @objc public func mediaView(forLocation location: CGPoint) -> MediaView? { var bestMediaView: MediaView? var bestDistance: CGFloat = 0 for itemView in itemViews { let itemCenter = convert(itemView.center, from: itemView.superview) let distance = CGPointDistance(location, itemCenter) if bestMediaView != nil && distance > bestDistance { continue } bestMediaView = itemView bestDistance = distance } return bestMediaView } @objc public func isMoreItemsView(mediaView: MediaView) -> Bool { return moreItemsView == mediaView } }