From cfd2e8d9d109bdd99358ddca0a2be4d0dc7f54e2 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 8 Nov 2018 10:28:07 -0600 Subject: [PATCH] Show captions in gallery page view --- .../MediaGalleryViewController.swift | 4 + .../MediaPageViewController.swift | 198 +++++++++++++++--- 2 files changed, 171 insertions(+), 31 deletions(-) diff --git a/Signal/src/ViewControllers/MediaGalleryViewController.swift b/Signal/src/ViewControllers/MediaGalleryViewController.swift index 9bffd3743..bcf604e07 100644 --- a/Signal/src/ViewControllers/MediaGalleryViewController.swift +++ b/Signal/src/ViewControllers/MediaGalleryViewController.swift @@ -31,6 +31,10 @@ public class MediaGalleryItem: Equatable, Hashable { return attachmentStream.isImage } + var caption: String? { + return attachmentStream.caption + } + public typealias AsyncThumbnailBlock = (UIImage) -> Void func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? { return attachmentStream.thumbnailImageSmall(success: async, failure: {}) diff --git a/Signal/src/ViewControllers/MediaPageViewController.swift b/Signal/src/ViewControllers/MediaPageViewController.swift index 2d92c09b5..82ce25980 100644 --- a/Signal/src/ViewControllers/MediaPageViewController.swift +++ b/Signal/src/ViewControllers/MediaPageViewController.swift @@ -101,11 +101,17 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou Logger.debug("deinit") } + var bottomContainer: UIView! var footerBar: UIToolbar! var videoPlayBarButton: UIBarButtonItem! var videoPauseBarButton: UIBarButtonItem! var pagerScrollView: UIScrollView! + // MARK: Caption + + var currentCaptionView: CaptionView! + var pendingCaptionView: CaptionView! + override func viewDidLoad() { super.viewDidLoad() @@ -142,6 +148,10 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // e.g. when getting to media details via message details screen, there's only // one "Page" so the bounce doesn't make sense. pagerScrollView.isScrollEnabled = sliderEnabled + pagerScrollViewContentOffsetObservation = pagerScrollView.observe(\.contentOffset, options: [.new]) { [weak self] object, change in + guard let strongSelf = self else { return } + strongSelf.pagerScrollView(strongSelf.pagerScrollView, contentOffsetDidChange: change) + } // Views @@ -152,12 +162,44 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou let footerBar = UIToolbar() self.footerBar = footerBar + let captionViewsContainer = UIView() + let kMaxCaptionHeight: CGFloat = ScaleFromIPhone5(300) + captionViewsContainer.autoSetDimension(.height, toSize: kMaxCaptionHeight, relation: .lessThanOrEqual) + captionViewsContainer.setContentHuggingHigh() + captionViewsContainer.setCompressionResistanceHigh() + + let currentCaptionView = CaptionView() + self.currentCaptionView = currentCaptionView + captionViewsContainer.addSubview(currentCaptionView) + currentCaptionView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) + currentCaptionView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) + currentCaptionView.setContentHuggingHigh() + currentCaptionView.setCompressionResistanceHigh() + currentCaptionView.text = currentItem.caption + + let pendingCaptionView = CaptionView() + self.pendingCaptionView = pendingCaptionView + pendingCaptionView.alpha = 0 + captionViewsContainer.addSubview(pendingCaptionView) + pendingCaptionView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) + pendingCaptionView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) + pendingCaptionView.setContentHuggingHigh() + pendingCaptionView.setCompressionResistanceHigh() + + let bottomContainer = UIView() + self.bottomContainer = bottomContainer + let bottomStack = UIStackView(arrangedSubviews: [captionViewsContainer, footerBar]) + bottomStack.axis = .vertical + bottomContainer.addSubview(bottomStack) + bottomStack.autoPinEdgesToSuperviewEdges() + 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(footerBar) - footerBar.autoPinWidthToSuperview() + self.view.addSubview(bottomContainer) + bottomContainer.autoPinWidthToSuperview() + bottomContainer.autoPinEdge(toSuperviewEdge: .bottom) footerBar.autoPin(toBottomLayoutGuideOf: self, withInset: 0) footerBar.autoSetDimension(.height, toSize: kFooterHeight) @@ -168,6 +210,44 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou view.addGestureRecognizer(verticalSwipe) } + // MARK: KVO + + var pagerScrollViewContentOffsetObservation: NSKeyValueObservation? + func pagerScrollView(_ pagerScrollView: UIScrollView, contentOffsetDidChange change: NSKeyValueObservedChange) { + guard let newValue = change.newValue else { + owsFailDebug("newValue was unexpectedly nil") + return + } + + let width = pagerScrollView.frame.size.width + guard width > 0 else { + return + } + + let ratioComplete = abs((newValue.x - width) / width) + updatePagerTransition(ratioComplete: ratioComplete) + } + + func updatePagerTransition(ratioComplete: CGFloat) { + if currentCaptionView.text != nil { + currentCaptionView.alpha = 1 - ratioComplete + } else { + currentCaptionView.alpha = 0 + } + + if pendingCaptionView.text != nil { + pendingCaptionView.alpha = ratioComplete + } else { + pendingCaptionView.alpha = 0 + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + let isLandscape = size.width > size.height + self.navigationItem.titleView = isLandscape ? nil : self.portraitHeaderView + } + override func didReceiveMemoryWarning() { Logger.info("") super.didReceiveMemoryWarning() @@ -189,32 +269,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } } - @objc - public func didPressAllMediaButton(sender: Any) { - Logger.debug("") - - currentViewController.stopAnyVideo() - - guard let mediaGalleryDataSource = self.mediaGalleryDataSource else { - owsFailDebug("mediaGalleryDataSource was unexpectedly nil") - return - } - mediaGalleryDataSource.showAllMedia(focusedItem: currentItem) - } - - @objc - public func didSwipeView(sender: Any) { - Logger.debug("") - - self.dismissSelf(animated: true) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - let isLandscape = size.width > size.height - self.navigationItem.titleView = isLandscape ? nil : self.portraitHeaderView - } - private var shouldHideToolbars: Bool = false { didSet { if (oldValue == shouldHideToolbars) { @@ -232,7 +286,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou UIView.animate(withDuration: 0.1) { self.currentViewController.setShouldHideToolbars(self.shouldHideToolbars) - self.footerBar.isHidden = self.shouldHideToolbars + self.bottomContainer.isHidden = self.shouldHideToolbars } } } @@ -266,6 +320,26 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // MARK: Actions + @objc + public func didPressAllMediaButton(sender: Any) { + Logger.debug("") + + currentViewController.stopAnyVideo() + + guard let mediaGalleryDataSource = self.mediaGalleryDataSource else { + owsFailDebug("mediaGalleryDataSource was unexpectedly nil") + return + } + mediaGalleryDataSource.showAllMedia(focusedItem: currentItem) + } + + @objc + public func didSwipeView(sender: Any) { + Logger.debug("") + + self.dismissSelf(animated: true) + } + @objc public func didPressDismissButton(_ sender: Any) { dismissSelf(animated: true) @@ -364,18 +438,31 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // MARK: UIPageViewControllerDelegate + var pendingViewController: MediaDetailViewController? public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { Logger.debug("") assert(pendingViewControllers.count == 1) pendingViewControllers.forEach { viewController in - guard let pendingPage = viewController as? MediaDetailViewController else { + guard let pendingViewController = viewController as? MediaDetailViewController else { owsFailDebug("unexpected mediaDetailViewController: \(viewController)") return } + self.pendingViewController = pendingViewController + + CATransaction.begin() + CATransaction.disableActions() + if let pendingCaptionText = pendingViewController.galleryItem.caption, pendingCaptionText.count > 0 { + self.pendingCaptionView.text = pendingCaptionText + } else { + self.pendingCaptionView.text = nil + } + self.pendingCaptionView.sizeToFit() + self.pendingCaptionView.superview?.layoutIfNeeded() + CATransaction.commit() // Ensure upcoming page respects current toolbar status - pendingPage.setShouldHideToolbars(self.shouldHideToolbars) + pendingViewController.setShouldHideToolbars(self.shouldHideToolbars) } } @@ -391,6 +478,20 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // Do any cleanup for the no-longer visible view controller if transitionCompleted { + pendingViewController = nil + + // This can happen when trying to page past the last (or first) view controller + // In that case, we don't want to change the captionView. + if (previousPage != currentViewController) { + updatePagerTransition(ratioComplete: 1) + + // promote "pending" to "current" caption view. + let oldCaptionView = self.currentCaptionView + self.currentCaptionView = self.pendingCaptionView + self.pendingCaptionView = oldCaptionView + self.pendingCaptionView.text = nil + } + updateTitle() previousPage.zoomOut(animated: false) previousPage.stopAnyVideo() @@ -650,3 +751,38 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } } } + +class CaptionView: UIView { + var label: UILabel = UILabel() + + var text: String? { + get { return label.text } + set { label.text = newValue } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + let gradientView = GradientView(from: .clear, to: .black) + addSubview(gradientView) + gradientView.autoPinEdgesToSuperviewEdges() + + addSubview(label) + label.font = UIFont.ows_dynamicTypeBody + label.textColor = .white + + // Usually captions are short, but they can be as long as 2k. + // We don't have UI for viewing infinitely large captions, so + // we do some not-ideal things to broaden the lenght of the + // captions we can support. + label.numberOfLines = 0 + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.5 + label.lineBreakMode = .byTruncatingTail + label.autoPinEdgesToSuperviewMargins() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +}