Avoid flicker when loading more on top

Adjusting content offset in the CollectionViewLayout.prepareLayout
method avoids a flicker vs. the previous way we were doing it.

// FREEBIE
This commit is contained in:
Michael Kirk 2018-03-19 19:34:57 -04:00
parent 19988a872a
commit 10ee054d0c
1 changed files with 44 additions and 16 deletions

View File

@ -36,6 +36,8 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
Logger.debug("\(logTag) deinit")
}
fileprivate let mediaTileViewLayout: MediaTileViewLayout
init(mediaGalleryDataSource: MediaGalleryDataSource, uiDatabaseConnection: YapDatabaseConnection) {
self.mediaGalleryDataSource = mediaGalleryDataSource
@ -51,12 +53,13 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
let availableWidth = screenWidth - CGFloat(kItemsPerRow + 1) * kInterItemSpacing
let kItemWidth = floor(availableWidth / CGFloat(kItemsPerRow))
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
let layout: MediaTileViewLayout = MediaTileViewLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width: kItemWidth, height: kItemWidth)
layout.minimumInteritemSpacing = kInterItemSpacing
layout.minimumLineSpacing = kInterItemSpacing
layout.sectionHeadersPinToVisibleBounds = true
self.mediaTileViewLayout = layout
super.init(collectionViewLayout: layout)
}
@ -279,7 +282,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
}
// MARK: MediaGalleryDelegate
public func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem) {
fileprivate func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem) {
Logger.debug("\(logTag) in \(#function)")
self.delegate?.mediaTileViewController(self, didTapView: cell.imageView, mediaGalleryItem: item)
}
@ -287,8 +290,8 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
// MARK: Lazy Loading
// This should be substantially larger than one screen size so we don't have to call it
// multiple times in a rapid succession, but not so large that loading more get's chopping
let kMediaTileViewLoadBatchSize: UInt = 80
// multiple times in a rapid succession, but not so large that loading get's really chopping
let kMediaTileViewLoadBatchSize: UInt = 40
var oldestLoadedItem: MediaGalleryItem? {
guard let oldestDate = galleryDates.first else {
return nil
@ -350,10 +353,15 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
}
isFetchingMoreData = true
let scrollDistanceToBottom = oldContentHeight - contentOffsetY
CATransaction.begin()
CATransaction.setDisableActions(true)
// mediaTileViewLayout will adjust content offset to compensate for the change in content height so that
// the same content is visible after the update. I considered doing something like setContentOffset in the
// batchUpdate completion block, but it caused a distinct flicker, which I was able to avoid with the
// `CollectionViewLayout.prepare` based approach.
mediaTileViewLayout.isInsertingCellsToTop = true
mediaTileViewLayout.contentSizeBeforeInsertingToTop = collectionView.contentSize
collectionView.performBatchUpdates({
mediaGalleryDataSource.ensureGalleryItemsLoaded(.before, item: oldestLoadedItem, amount: self.kMediaTileViewLoadBatchSize) { addedSections, addedItems in
Logger.debug("\(self.logTag) in \(#function) insertingSections: \(addedSections) items: \(addedItems)")
@ -362,12 +370,6 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
collectionView.insertItems(at: addedItems)
}
}, completion: { finished in
// Adjust content offset to affect change in content height so that the same content is visible after
// the update.
let newContentOffset = CGPoint(x: 0, y: collectionView.contentSize.height - scrollDistanceToBottom)
collectionView.setContentOffset(newContentOffset, animated: false)
Logger.debug("\(self.logTag) in \(#function) performBatchUpdates finished: \(finished)")
self.isFetchingMoreData = false
CATransaction.commit()
@ -424,7 +426,33 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
}
}
class MediaGallerySectionHeader: UICollectionReusableView {
// MARK: - Private Helper Classes
// Accomodates remaining scrolled to the same "apparent" position when new content is insterted
// into the top of a collectionView. There are multiple ways to solve this problem, but this
// is the only one which avoided a perceptible flicker.
fileprivate class MediaTileViewLayout: UICollectionViewFlowLayout {
fileprivate var isInsertingCellsToTop: Bool = false
fileprivate var contentSizeBeforeInsertingToTop: CGSize?
override public func prepare() {
super.prepare()
if isInsertingCellsToTop {
if let collectionView = collectionView, let oldContentSize = contentSizeBeforeInsertingToTop {
let newContentSize = collectionViewContentSize
let contentOffsetY = collectionView.contentOffset.y + (newContentSize.height - oldContentSize.height)
let newOffset = CGPoint(x: collectionView.contentOffset.x, y: contentOffsetY)
collectionView.setContentOffset(newOffset, animated: false)
}
contentSizeBeforeInsertingToTop = nil
isInsertingCellsToTop = false
}
}
}
fileprivate class MediaGallerySectionHeader: UICollectionReusableView {
static let reuseIdentifier = "MediaGallerySectionHeader"
@ -486,11 +514,11 @@ class MediaGallerySectionHeader: UICollectionReusableView {
}
}
public protocol MediaGalleryCellDelegate: class {
fileprivate protocol MediaGalleryCellDelegate: class {
func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem)
}
public class MediaGalleryLoadingHeader: UICollectionViewCell {
fileprivate class MediaGalleryLoadingHeader: UICollectionViewCell {
static let reuseIdentifier = "MediaGalleryLoadingHeader"
@ -519,7 +547,7 @@ public class MediaGalleryLoadingHeader: UICollectionViewCell {
}
}
public class MediaGalleryCell: UICollectionViewCell {
fileprivate class MediaGalleryCell: UICollectionViewCell {
static let reuseIdentifier = "MediaGalleryCell"