mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Album rail in Gallery
This commit is contained in:
parent
fd424f3892
commit
84879b991d
4 changed files with 422 additions and 19 deletions
|
@ -450,6 +450,7 @@
|
|||
4C858A52212DC5E1001B45D3 /* UIImage+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */; };
|
||||
4C948FF72146EB4800349F0D /* BlockListCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C948FF62146EB4800349F0D /* BlockListCache.swift */; };
|
||||
4C9CA25D217E676900607C63 /* ZXingObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C9CA25C217E676900607C63 /* ZXingObjC.framework */; };
|
||||
4CA46F4A219C78050038ABDE /* GalleryRailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F49219C78050038ABDE /* GalleryRailView.swift */; };
|
||||
4CA5F793211E1F06008C2708 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5F792211E1F06008C2708 /* Toast.swift */; };
|
||||
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; };
|
||||
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
|
||||
|
@ -1137,7 +1138,6 @@
|
|||
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = "<group>"; };
|
||||
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
|
||||
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = "<group>"; };
|
||||
4C1D233C218B96A000A0598F /* typing-animation.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = "typing-animation.gif"; path = "../../../../../Downloads/typing-animation.gif"; sourceTree = "<group>"; };
|
||||
4C1D2333218B692800A0598F /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = translations/ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
4C1D2334218B6A1100A0598F /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = translations/az.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
4C1D2335218B6A7600A0598F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = translations/el.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -1159,6 +1159,7 @@
|
|||
4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+OWS.swift"; sourceTree = "<group>"; };
|
||||
4C948FF62146EB4800349F0D /* BlockListCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListCache.swift; sourceTree = "<group>"; };
|
||||
4C9CA25C217E676900607C63 /* ZXingObjC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZXingObjC.framework; path = ThirdParty/Carthage/Build/iOS/ZXingObjC.framework; sourceTree = "<group>"; };
|
||||
4CA46F49219C78050038ABDE /* GalleryRailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryRailView.swift; sourceTree = "<group>"; };
|
||||
4CA5F792211E1F06008C2708 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
|
||||
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
|
||||
4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitoringManager.swift; sourceTree = "<group>"; };
|
||||
|
@ -2269,6 +2270,7 @@
|
|||
4CA5F792211E1F06008C2708 /* Toast.swift */,
|
||||
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
||||
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
|
||||
4CA46F49219C78050038ABDE /* GalleryRailView.swift */,
|
||||
);
|
||||
name = Views;
|
||||
path = views;
|
||||
|
@ -3448,6 +3450,7 @@
|
|||
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
|
||||
45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */,
|
||||
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
|
||||
4CA46F4A219C78050038ABDE /* GalleryRailView.swift in Sources */,
|
||||
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
||||
|
|
|
@ -8,17 +8,41 @@ public enum GalleryDirection {
|
|||
case before, after, around
|
||||
}
|
||||
|
||||
class MediaGalleryAlbum {
|
||||
private(set) var items: [MediaGalleryItem]
|
||||
|
||||
init(items: [MediaGalleryItem]) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
func add(item: MediaGalleryItem) {
|
||||
guard !items.contains(item) else {
|
||||
return
|
||||
}
|
||||
|
||||
items.append(item)
|
||||
items.sort { (lhs, rhs) -> Bool in
|
||||
return lhs.albumIndex < rhs.albumIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MediaGalleryItem: Equatable, Hashable {
|
||||
let message: TSMessage
|
||||
let attachmentStream: TSAttachmentStream
|
||||
let galleryDate: GalleryDate
|
||||
let captionForDisplay: String?
|
||||
let albumIndex: Int
|
||||
var album: MediaGalleryAlbum?
|
||||
let orderingKey: MediaGalleryItemOrderingKey
|
||||
|
||||
init(message: TSMessage, attachmentStream: TSAttachmentStream) {
|
||||
self.message = message
|
||||
self.attachmentStream = attachmentStream
|
||||
self.captionForDisplay = attachmentStream.caption?.filterForDisplay
|
||||
self.galleryDate = GalleryDate(message: message)
|
||||
self.albumIndex = message.attachmentIds.index(of: attachmentStream.uniqueId!)
|
||||
self.orderingKey = MediaGalleryItemOrderingKey(messageSortKey: message.timestampForSorting(), attachmentSortKey: albumIndex)
|
||||
}
|
||||
|
||||
var isVideo: Bool {
|
||||
|
@ -33,6 +57,10 @@ public class MediaGalleryItem: Equatable, Hashable {
|
|||
return attachmentStream.isImage
|
||||
}
|
||||
|
||||
var imageSize: CGSize {
|
||||
return attachmentStream.imageSize()
|
||||
}
|
||||
|
||||
public typealias AsyncThumbnailBlock = (UIImage) -> Void
|
||||
func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? {
|
||||
return attachmentStream.thumbnailImageSmall(success: async, failure: {})
|
||||
|
@ -49,6 +77,29 @@ public class MediaGalleryItem: Equatable, Hashable {
|
|||
public var hashValue: Int {
|
||||
return attachmentStream.uniqueId?.hashValue ?? attachmentStream.hashValue
|
||||
}
|
||||
|
||||
// MARK: Sorting
|
||||
|
||||
struct MediaGalleryItemOrderingKey: Comparable {
|
||||
let messageSortKey: UInt64
|
||||
let attachmentSortKey: Int
|
||||
|
||||
// MARK: Comparable
|
||||
|
||||
static func < (lhs: MediaGalleryItem.MediaGalleryItemOrderingKey, rhs: MediaGalleryItem.MediaGalleryItemOrderingKey) -> Bool {
|
||||
if lhs.messageSortKey < rhs.messageSortKey {
|
||||
return true
|
||||
}
|
||||
|
||||
if lhs.messageSortKey == rhs.messageSortKey {
|
||||
if lhs.attachmentSortKey < rhs.attachmentSortKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct GalleryDate: Hashable, Comparable, Equatable {
|
||||
|
@ -648,7 +699,27 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|||
return nil
|
||||
}
|
||||
|
||||
return MediaGalleryItem(message: message, attachmentStream: attachmentStream)
|
||||
let galleryItem = MediaGalleryItem(message: message, attachmentStream: attachmentStream)
|
||||
galleryItem.album = getAlbum(item: galleryItem)
|
||||
|
||||
return galleryItem
|
||||
}
|
||||
|
||||
var galleryAlbums: [String: MediaGalleryAlbum] = [:]
|
||||
func getAlbum(item: MediaGalleryItem) -> MediaGalleryAlbum? {
|
||||
guard let albumMessageId = item.attachmentStream.albumMessageId else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let existingAlbum = galleryAlbums[albumMessageId] else {
|
||||
let newAlbum = MediaGalleryAlbum(items: [item])
|
||||
galleryAlbums[albumMessageId] = newAlbum
|
||||
|
||||
return newAlbum
|
||||
}
|
||||
|
||||
existingAlbum.add(item: item)
|
||||
return existingAlbum
|
||||
}
|
||||
|
||||
// Range instead of indexSet since it's contiguous?
|
||||
|
@ -760,13 +831,13 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|||
|
||||
Bench(title: "sorting gallery items") {
|
||||
galleryItems.sort { lhs, rhs -> Bool in
|
||||
return lhs.message.timestampForSorting() < rhs.message.timestampForSorting()
|
||||
return lhs.orderingKey < rhs.orderingKey
|
||||
}
|
||||
sectionDates.sort()
|
||||
|
||||
for (date, galleryItems) in sections {
|
||||
sortedSections[date] = galleryItems.sorted { lhs, rhs -> Bool in
|
||||
return lhs.message.timestampForSorting() < rhs.message.timestampForSorting()
|
||||
return lhs.orderingKey < rhs.orderingKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
|
||||
// Objc wrapper for the MediaGalleryItem struct
|
||||
@objc
|
||||
|
@ -53,10 +54,11 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
return
|
||||
}
|
||||
|
||||
self.updateTitle(item: item)
|
||||
self.updateCaption(item: item)
|
||||
self.setViewControllers([galleryPage], direction: direction, animated: isAnimated)
|
||||
self.updateFooterBarButtonItems(isPlayingVideo: false)
|
||||
updateTitle(item: item)
|
||||
updateCaption(item: item)
|
||||
setViewControllers([galleryPage], direction: direction, animated: isAnimated)
|
||||
updateFooterBarButtonItems(isPlayingVideo: false)
|
||||
updateMediaRail()
|
||||
}
|
||||
|
||||
private let uiDatabaseConnection: YapDatabaseConnection
|
||||
|
@ -108,6 +110,26 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
var currentCaptionView: CaptionView!
|
||||
var pendingCaptionView: CaptionView!
|
||||
|
||||
// MARK: ImageRail
|
||||
|
||||
var galleryRailView: GalleryRailView!
|
||||
|
||||
private func makeClearToolbar() -> UIToolbar {
|
||||
let toolbar = UIToolbar()
|
||||
|
||||
toolbar.backgroundColor = UIColor.clear
|
||||
|
||||
// Making a toolbar transparent requires setting an empty uiimage
|
||||
toolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default)
|
||||
|
||||
// hide 1px top-border
|
||||
toolbar.clipsToBounds = true
|
||||
|
||||
return toolbar
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -120,8 +142,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
|
||||
self.navigationItem.titleView = portraitHeaderView
|
||||
|
||||
self.updateTitle()
|
||||
|
||||
if showAllMediaButton {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: MediaStrings.allMedia, style: .plain, target: self, action: #selector(didPressAllMediaButton))
|
||||
}
|
||||
|
@ -151,15 +171,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
|
||||
// Views
|
||||
|
||||
let kFooterHeight: CGFloat = 44
|
||||
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
|
||||
let footerBar = UIToolbar()
|
||||
self.footerBar = footerBar
|
||||
|
||||
let captionViewsContainer = UIView()
|
||||
|
||||
captionViewsContainer.setContentHuggingHigh()
|
||||
captionViewsContainer.setCompressionResistanceHigh()
|
||||
|
||||
|
@ -177,9 +191,24 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
pendingCaptionView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
|
||||
pendingCaptionView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
|
||||
|
||||
let galleryRailView = GalleryRailView()
|
||||
galleryRailView.delegate = self
|
||||
galleryRailView.autoSetDimension(.height, toSize: 60)
|
||||
self.galleryRailView = galleryRailView
|
||||
|
||||
let footerBar = self.makeClearToolbar()
|
||||
self.footerBar = footerBar
|
||||
|
||||
let bottomContainer = UIView()
|
||||
self.bottomContainer = bottomContainer
|
||||
let bottomStack = UIStackView(arrangedSubviews: [captionViewsContainer, footerBar])
|
||||
|
||||
let toolbarStack = UIStackView(arrangedSubviews: [galleryRailView, footerBar])
|
||||
toolbarStack.axis = .vertical
|
||||
let toolbarBarBlurView = UIVisualEffectView(effect: Theme.barBlurEffect)
|
||||
toolbarStack.insertSubview(toolbarBarBlurView, at: 0)
|
||||
toolbarBarBlurView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
let bottomStack = UIStackView(arrangedSubviews: [captionViewsContainer, toolbarStack])
|
||||
bottomStack.axis = .vertical
|
||||
bottomContainer.addSubview(bottomStack)
|
||||
bottomStack.autoPinEdgesToSuperviewEdges()
|
||||
|
@ -187,12 +216,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
self.videoPlayBarButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(didPressPlayBarButton))
|
||||
self.videoPauseBarButton = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(didPressPauseBarButton))
|
||||
|
||||
self.updateFooterBarButtonItems(isPlayingVideo: true)
|
||||
self.view.addSubview(bottomContainer)
|
||||
bottomContainer.autoPinWidthToSuperview()
|
||||
bottomContainer.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
footerBar.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
|
||||
footerBar.autoSetDimension(.height, toSize: kFooterHeight)
|
||||
footerBar.autoSetDimension(.height, toSize: 44)
|
||||
|
||||
updateTitle()
|
||||
updateMediaRail()
|
||||
updateFooterBarButtonItems(isPlayingVideo: true)
|
||||
|
||||
// Gestures
|
||||
|
||||
|
@ -309,6 +341,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
self.footerBar.setItems(toolbarItems, animated: false)
|
||||
}
|
||||
|
||||
func updateMediaRail() {
|
||||
guard let currentItem = self.currentItem else {
|
||||
owsFailDebug("currentItem was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
galleryRailView.configure(itemProvider: currentItem.album, focusedItem: currentItem)
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
|
@ -484,6 +525,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
|
||||
updateTitle()
|
||||
updateMediaRail()
|
||||
previousPage.zoomOut(animated: false)
|
||||
previousPage.stopAnyVideo()
|
||||
updateFooterBarButtonItems(isPlayingVideo: false)
|
||||
|
@ -747,6 +789,19 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
}
|
||||
}
|
||||
|
||||
extension MediaPageViewController: GalleryRailViewDelegate {
|
||||
func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem) {
|
||||
guard let targetItem = imageRailItem as? MediaGalleryItem else {
|
||||
owsFailDebug("unexpected imageRailItem: \(imageRailItem)")
|
||||
return
|
||||
}
|
||||
|
||||
let direction: NavigationDirection = currentItem.albumIndex < targetItem.albumIndex ? .forward : .reverse
|
||||
|
||||
self.setCurrentItem(targetItem, direction: direction, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
class CaptionView: UIView {
|
||||
|
||||
var text: String? {
|
||||
|
|
274
Signal/src/views/GalleryRailView.swift
Normal file
274
Signal/src/views/GalleryRailView.swift
Normal file
|
@ -0,0 +1,274 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import PromiseKit
|
||||
|
||||
protocol GalleryRailItemProvider: class {
|
||||
var railItems: [GalleryRailItem] { get }
|
||||
}
|
||||
|
||||
protocol GalleryRailItem: class {
|
||||
func getRailImage() -> Guarantee<UIImage>
|
||||
var aspectRatio: CGFloat { get }
|
||||
}
|
||||
|
||||
extension CGSize {
|
||||
var aspectRatio: CGFloat {
|
||||
guard self.height > 0 else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return self.width / self.height
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaGalleryItem: GalleryRailItem {
|
||||
var aspectRatio: CGFloat {
|
||||
return self.imageSize.aspectRatio
|
||||
}
|
||||
|
||||
func getRailImage() -> Guarantee<UIImage> {
|
||||
let (guarantee, fulfill) = Guarantee<UIImage>.pending()
|
||||
if let image = self.thumbnailImage(async: { fulfill($0) }) {
|
||||
fulfill(image)
|
||||
}
|
||||
|
||||
return guarantee
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaGalleryAlbum: GalleryRailItemProvider {
|
||||
var railItems: [GalleryRailItem] {
|
||||
return self.items
|
||||
}
|
||||
}
|
||||
|
||||
protocol GalleryRailCellViewDelegate: class {
|
||||
func didTapGalleryRailCellView(_ galleryRailCellView: GalleryRailCellView)
|
||||
}
|
||||
|
||||
class GalleryRailCellView: UIView {
|
||||
|
||||
weak var delegate: GalleryRailCellViewDelegate?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
layoutMargins = .zero
|
||||
self.clipsToBounds = true
|
||||
adjustAspectRatio(isSelected: isSelected)
|
||||
addSubview(imageView)
|
||||
imageView.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(sender:)))
|
||||
addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
func didTap(sender: UITapGestureRecognizer) {
|
||||
self.delegate?.didTapGalleryRailCellView(self)
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
var item: GalleryRailItem?
|
||||
|
||||
func configure(item: GalleryRailItem, delegate: GalleryRailCellViewDelegate) {
|
||||
self.item = item
|
||||
self.delegate = delegate
|
||||
|
||||
item.getRailImage().done { image in
|
||||
guard self.item === item else { return }
|
||||
|
||||
self.imageView.image = image
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
// MARK: Selected
|
||||
|
||||
private(set) var isSelected: Bool = false
|
||||
|
||||
func setIsSelected(_ isSelected: Bool) {
|
||||
self.isSelected = isSelected
|
||||
adjustAspectRatio(isSelected: isSelected)
|
||||
if isSelected {
|
||||
self.layoutMargins = UIEdgeInsets(top: 0, left: 3, bottom: 0, right: 3)
|
||||
} else {
|
||||
self.layoutMargins = .zero
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Subview Helpers
|
||||
|
||||
var aspectRatioConstraint: NSLayoutConstraint?
|
||||
func adjustAspectRatio(isSelected: Bool) {
|
||||
if let oldConstraint = aspectRatioConstraint {
|
||||
NSLayoutConstraint.deactivate([oldConstraint])
|
||||
}
|
||||
|
||||
if isSelected, let itemAspectRatio = item?.aspectRatio {
|
||||
aspectRatioConstraint = imageView.autoPin(toAspectRatio: itemAspectRatio)
|
||||
} else {
|
||||
// Portrait mode AR by default
|
||||
let kDefaultAspectRatio: CGFloat = 9.0 / 16.0
|
||||
aspectRatioConstraint = imageView.autoPin(toAspectRatio: kDefaultAspectRatio)
|
||||
}
|
||||
}
|
||||
|
||||
let imageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
|
||||
return imageView
|
||||
}()
|
||||
}
|
||||
|
||||
protocol GalleryRailViewDelegate: class {
|
||||
func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem)
|
||||
}
|
||||
|
||||
class GalleryRailView: UIView, GalleryRailCellViewDelegate {
|
||||
|
||||
weak var delegate: GalleryRailViewDelegate?
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
addSubview(scrollView)
|
||||
scrollView.layoutMargins = .zero
|
||||
scrollView.autoPinEdgesToSuperviewMargins()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
public func configure(itemProvider: GalleryRailItemProvider?, focusedItem: GalleryRailItem?) {
|
||||
let animationDuration: TimeInterval = 0.2
|
||||
|
||||
guard let itemProvider = itemProvider else {
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
self.isHidden = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let areRailItemsIdentical = { (lhs: [GalleryRailItem], rhs: [GalleryRailItem]) -> Bool in
|
||||
guard lhs.count == rhs.count else {
|
||||
return false
|
||||
}
|
||||
for (index, element) in lhs.enumerated() {
|
||||
guard element === rhs[index] else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if itemProvider === self.itemProvider, areRailItemsIdentical(itemProvider.railItems, self.cellViewItems) {
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
self.updateFocusedItem(focusedItem)
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
self.itemProvider = itemProvider
|
||||
scrollView.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
guard itemProvider.railItems.count > 1 else {
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
self.isHidden = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
self.isHidden = false
|
||||
}
|
||||
|
||||
let cellViews = buildCellViews(items: itemProvider.railItems)
|
||||
self.cellViews = cellViews
|
||||
let stackView = UIStackView(arrangedSubviews: cellViews)
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 4
|
||||
|
||||
scrollView.addSubview(stackView)
|
||||
stackView.autoPinEdgesToSuperviewEdges()
|
||||
stackView.autoMatch(.height, to: .height, of: scrollView)
|
||||
|
||||
updateFocusedItem(focusedItem)
|
||||
}
|
||||
|
||||
// MARK: GalleryRailCellViewDelegate
|
||||
|
||||
func didTapGalleryRailCellView(_ galleryRailCellView: GalleryRailCellView) {
|
||||
guard let item = galleryRailCellView.item else {
|
||||
owsFailDebug("item was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
delegate?.galleryRailView(self, didTapItem: item)
|
||||
}
|
||||
|
||||
// MARK: Subview Helpers
|
||||
|
||||
private var itemProvider: GalleryRailItemProvider?
|
||||
|
||||
private let scrollView: UIScrollView = {
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.isScrollEnabled = true
|
||||
return scrollView
|
||||
}()
|
||||
|
||||
private func buildCellViews(items: [GalleryRailItem]) -> [GalleryRailCellView] {
|
||||
return items.map { item in
|
||||
let cellView = GalleryRailCellView()
|
||||
cellView.configure(item: item, delegate: self)
|
||||
return cellView
|
||||
}
|
||||
}
|
||||
|
||||
var cellViews: [GalleryRailCellView] = []
|
||||
var cellViewItems: [GalleryRailItem] {
|
||||
get { return cellViews.compactMap { $0.item } }
|
||||
}
|
||||
func updateFocusedItem(_ focusedItem: GalleryRailItem?) {
|
||||
var selectedCellView: GalleryRailCellView?
|
||||
cellViews.forEach { cellView in
|
||||
if cellView.item === focusedItem {
|
||||
assert(selectedCellView == nil)
|
||||
selectedCellView = cellView
|
||||
cellView.setIsSelected(true)
|
||||
} else {
|
||||
cellView.setIsSelected(false)
|
||||
}
|
||||
}
|
||||
|
||||
self.layoutIfNeeded()
|
||||
guard let selectedCell = selectedCellView else {
|
||||
owsFailDebug("selectedCell was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
let cellViewCenter = selectedCell.superview!.convert(selectedCell.center, to: scrollView)
|
||||
let additionalInset = scrollView.center.x - cellViewCenter.x
|
||||
|
||||
var inset = scrollView.contentInset
|
||||
inset.left = additionalInset
|
||||
scrollView.contentInset = inset
|
||||
|
||||
var offset = scrollView.contentOffset
|
||||
offset.x = -additionalInset
|
||||
scrollView.contentOffset = offset
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue