- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
306 lines
12 KiB
Swift
306 lines
12 KiB
Swift
|
|
import UIKit
|
|
|
|
public protocol RDEpubReaderFlowLayoutDataSoure: NSObjectProtocol {
|
|
|
|
func heigtOfVerticalScrollPage(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) -> CGFloat?
|
|
}
|
|
|
|
@objc public protocol RDEpubReaderFlowLayoutDelegate: NSObjectProtocol {
|
|
|
|
func pageNum(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int)
|
|
}
|
|
|
|
public class RDEpubReaderFlowLayout: UICollectionViewFlowLayout {
|
|
|
|
var displayType: RDEpubReaderView.DisplayType = .horizontalScroll {
|
|
didSet {
|
|
invalidateLayout()
|
|
}
|
|
}
|
|
|
|
var isLandscapeDualPage: Bool = false {
|
|
didSet {
|
|
if oldValue != isLandscapeDualPage {
|
|
invalidateLayout()
|
|
}
|
|
}
|
|
}
|
|
|
|
var coverPageIndex: Int? = nil {
|
|
didSet {
|
|
if oldValue != coverPageIndex {
|
|
invalidateLayout()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var lastPreparedBoundsSize: CGSize = .zero
|
|
|
|
var pagesPerScreen: Int {
|
|
guard isLandscapeDualPage else { return 1 }
|
|
switch displayType {
|
|
case .horizontalScroll:
|
|
if let cv = collectionView, cv.bounds.width > cv.bounds.height {
|
|
return 2
|
|
}
|
|
return 1
|
|
default:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
weak var dataSource: RDEpubReaderFlowLayoutDataSoure? = nil
|
|
|
|
weak var delegate: RDEpubReaderFlowLayoutDelegate? = nil
|
|
|
|
private var hasCoverPageInDualMode: Bool {
|
|
return pagesPerScreen > 1 && coverPageIndex != nil
|
|
}
|
|
|
|
private func coverAwareFrame(for index: Int, screenWidth: CGFloat, halfWidth: CGFloat, height: CGFloat) -> CGRect {
|
|
guard let coverIndex = coverPageIndex else {
|
|
let pairIdx = index / 2
|
|
let side = index % 2
|
|
let x = CGFloat(pairIdx) * screenWidth + CGFloat(side) * halfWidth
|
|
return CGRect(x: x, y: 0, width: halfWidth, height: height)
|
|
}
|
|
if index == coverIndex {
|
|
let screensBeforeCover = coverIndex / 2
|
|
let x = CGFloat(screensBeforeCover) * screenWidth
|
|
return CGRect(x: x, y: 0, width: screenWidth, height: height)
|
|
}
|
|
let adjustedIndex = index - (coverIndex + 1)
|
|
let pairIdx = adjustedIndex / 2
|
|
let side = adjustedIndex % 2
|
|
let coverScreens = coverIndex / 2 + 1
|
|
let x = CGFloat(coverScreens + pairIdx) * screenWidth + CGFloat(side) * halfWidth
|
|
return CGRect(x: x, y: 0, width: halfWidth, height: height)
|
|
}
|
|
|
|
private func coverAwareStartIndex(for offset: CGFloat, screenWidth: CGFloat) -> Int {
|
|
let pps = pagesPerScreen
|
|
guard let coverIdx = coverPageIndex, hasCoverPageInDualMode else {
|
|
let screenIndex = Int((offset / screenWidth).rounded(.down))
|
|
return screenIndex * pps
|
|
}
|
|
let coverScreens = coverIdx / 2 + 1
|
|
let coverEnd = CGFloat(coverScreens) * screenWidth
|
|
if offset < coverEnd {
|
|
let screenIdx = Int((offset / screenWidth).rounded(.down))
|
|
return screenIdx >= coverIdx / 2 ? coverIdx : screenIdx * pps
|
|
} else {
|
|
let pairIdx = Int(((offset - coverEnd) / screenWidth).rounded(.down))
|
|
return coverIdx + 1 + pairIdx * 2
|
|
}
|
|
}
|
|
|
|
private var currentPage: Int = 0 {
|
|
|
|
didSet {
|
|
if let delegate = delegate, delegate.responds(to: #selector(RDEpubReaderFlowLayoutDelegate.pageNum(flowLayout:pageIndex:))), currentPage != oldValue, currentPage >= 0 {
|
|
delegate.pageNum(flowLayout: self, pageIndex: currentPage)
|
|
}
|
|
}
|
|
}
|
|
|
|
init(displayType: RDEpubReaderView.DisplayType) {
|
|
self.displayType = displayType
|
|
super.init()
|
|
|
|
}
|
|
|
|
public override func prepare() {
|
|
super.prepare()
|
|
guard let collectionView = self.collectionView else {
|
|
return
|
|
}
|
|
guard collectionView.frame.width > 0, collectionView.frame.height > 0 else {
|
|
return
|
|
}
|
|
|
|
lastPreparedBoundsSize = collectionView.bounds.size
|
|
|
|
if #available(iOS 11.0, *) {
|
|
collectionView.contentInsetAdjustmentBehavior = .never
|
|
} else {
|
|
collectionView.ss_superViewController?.automaticallyAdjustsScrollViewInsets = false
|
|
}
|
|
collectionView.showsVerticalScrollIndicator = false
|
|
collectionView.showsHorizontalScrollIndicator = false
|
|
minimumLineSpacing = 0
|
|
minimumInteritemSpacing = 0
|
|
switch self.displayType {
|
|
case .horizontalScroll:
|
|
scrollDirection = .horizontal
|
|
let columns = CGFloat(pagesPerScreen)
|
|
itemSize = CGSize(width: collectionView.frame.width / columns, height: collectionView.frame.height)
|
|
collectionView.isPagingEnabled = true
|
|
case .verticalScroll:
|
|
itemSize = CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
|
|
scrollDirection = .vertical
|
|
collectionView.isPagingEnabled = false
|
|
collectionView.showsVerticalScrollIndicator = true
|
|
|
|
default: break
|
|
}
|
|
|
|
}
|
|
|
|
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
|
if newBounds.size != lastPreparedBoundsSize {
|
|
return true
|
|
}
|
|
switch displayType {
|
|
case .verticalScroll:
|
|
return true
|
|
case .horizontalScroll:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
public override var collectionViewContentSize: CGSize {
|
|
var size = super.collectionViewContentSize
|
|
guard let collectionView = collectionView else {
|
|
return size
|
|
}
|
|
if displayType == .verticalScroll {
|
|
let totalHeight = [Int](0..<collectionView.numberOfItems(inSection: 0)).reduce(0.0, { result, num in
|
|
result + (dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: num) ?? collectionView.frame.height)
|
|
})
|
|
size.height = totalHeight
|
|
}
|
|
if hasCoverPageInDualMode, displayType == .horizontalScroll {
|
|
let totalItems = collectionView.numberOfItems(inSection: 0)
|
|
let screenWidth = collectionView.frame.width
|
|
let remainingItems = max(0, totalItems - 1)
|
|
let pairedScreens = (remainingItems + 1) / 2
|
|
size.width = screenWidth * CGFloat(1 + pairedScreens)
|
|
}
|
|
return size
|
|
}
|
|
|
|
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
|
var attributes = super.layoutAttributesForElements(in: rect)
|
|
guard let collectionView = collectionView else {
|
|
return attributes
|
|
}
|
|
|
|
if self.displayType == .verticalScroll {
|
|
let rowCount = collectionView.numberOfItems(inSection: 0)
|
|
var totalHeight: CGFloat = 0
|
|
for index in 0..<rowCount {
|
|
totalHeight += dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: index) ?? collectionView.frame.height
|
|
if totalHeight > collectionView.contentOffset.y {
|
|
self.currentPage = index
|
|
break
|
|
}
|
|
}
|
|
(attributes ?? []).forEach({ attr in
|
|
let indexPath = attr.indexPath
|
|
let lastRow = max(0, indexPath.row - 1)
|
|
let height = dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: indexPath.row) ?? collectionView.frame.height
|
|
var lastTotalHeight: CGFloat = 0
|
|
if indexPath.row > 0 {
|
|
lastTotalHeight = [Int](0...lastRow).reduce(0.0) { result, row in
|
|
result + (dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: row) ?? collectionView.frame.height)
|
|
}
|
|
}
|
|
if let cell = collectionView.cellForItem(at: indexPath) {
|
|
cell.layer.shadowOpacity = 0
|
|
}
|
|
attr.frame = CGRect(x: 0, y: lastTotalHeight, width: collectionView.frame.width, height: height)
|
|
})
|
|
}
|
|
|
|
if self.displayType == .horizontalScroll {
|
|
let pps = pagesPerScreen
|
|
|
|
if hasCoverPageInDualMode {
|
|
let screenWidth = collectionView.frame.width
|
|
let halfWidth = screenWidth / 2.0
|
|
let rows = collectionView.numberOfItems(inSection: 0)
|
|
|
|
self.currentPage = coverAwareStartIndex(for: collectionView.contentOffset.x, screenWidth: screenWidth)
|
|
|
|
var attrs = [UICollectionViewLayoutAttributes]()
|
|
for index in 0..<rows {
|
|
let frame = coverAwareFrame(for: index, screenWidth: screenWidth, halfWidth: halfWidth, height: collectionView.frame.height)
|
|
guard frame.intersects(rect) else { continue }
|
|
let indexPath = IndexPath(item: index, section: 0)
|
|
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
|
attr.frame = frame
|
|
attrs.append(attr)
|
|
if let cell = collectionView.cellForItem(at: indexPath) {
|
|
cell.layer.shadowOpacity = 0
|
|
}
|
|
}
|
|
attributes = attrs
|
|
} else if pps > 1 {
|
|
let currentPage = Int((collectionView.contentOffset.x / collectionView.frame.width).rounded(.down)) * pps
|
|
self.currentPage = currentPage
|
|
(attributes ?? []).forEach({ attr in
|
|
if let cell = collectionView.cellForItem(at: attr.indexPath) {
|
|
cell.layer.shadowOpacity = 0
|
|
}
|
|
})
|
|
} else {
|
|
let currentPage = Int((collectionView.contentOffset.x / collectionView.frame.width).rounded(.down))
|
|
self.currentPage = currentPage
|
|
(attributes ?? []).forEach({ attr in
|
|
if let cell = collectionView.cellForItem(at: attr.indexPath) {
|
|
cell.layer.shadowOpacity = 0
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
return attributes
|
|
}
|
|
|
|
func currentContentOffset(count: Int) -> CGPoint {
|
|
guard let collectionView = collectionView else {
|
|
return .zero
|
|
}
|
|
guard collectionView.frame.width > 0, collectionView.frame.height > 0 else {
|
|
return .zero
|
|
}
|
|
let safeCount = max(0, count)
|
|
switch displayType {
|
|
case .verticalScroll:
|
|
guard safeCount > 0 else { return .zero }
|
|
let totalHeight = [Int](0...(safeCount - 1)).reduce(0.0) { result, pageNum in
|
|
result + (dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: pageNum) ?? collectionView.frame.height)
|
|
}
|
|
return CGPoint(x: 0, y: totalHeight)
|
|
default:
|
|
let pps = pagesPerScreen
|
|
let screenWidth = collectionView.frame.width
|
|
if hasCoverPageInDualMode, let coverIdx = coverPageIndex {
|
|
if safeCount == coverIdx {
|
|
let screensBeforeCover = coverIdx / 2
|
|
return CGPoint(x: CGFloat(screensBeforeCover) * screenWidth, y: 0)
|
|
}
|
|
if safeCount < coverIdx {
|
|
return CGPoint(x: CGFloat(safeCount / 2) * screenWidth, y: 0)
|
|
}
|
|
let coverScreens = coverIdx / 2 + 1
|
|
let adjustedIndex = safeCount - (coverIdx + 1)
|
|
let pairIndex = adjustedIndex / 2
|
|
return CGPoint(x: CGFloat(coverScreens + pairIndex) * screenWidth, y: 0)
|
|
} else if pps > 1 {
|
|
let screenIndex = safeCount / pps
|
|
return CGPoint(x: CGFloat(screenIndex) * screenWidth, y: 0)
|
|
} else {
|
|
return CGPoint(x: CGFloat(safeCount) * itemSize.width, y: 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|