ReadViewSDK/Sources/RDReaderView/ReaderView/RDReaderView.swift

1387 lines
60 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// RDReaderView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/6.
//
// RDReaderView
// UIView RDReaderView
// 1. 仿
// 2. 1/31/31/3
// 3.
// 4. (LTR)(RTL)
// 5. DataSource/Delegate
// 6.
//
// RDReaderView
// EPUBCore EPUBTextRendering RDReaderView EPUBUI UI
//
import UIKit
///
/// RDReaderView
/// RDReaderView
@objc public protocol RDReaderDataSource: NSObjectProtocol {
///
/// - Parameter readerView:
/// - Returns:
func pageCountOfReaderView(readerView: RDReaderView) -> Int
///
/// - Parameters:
/// - readerView:
/// - pageNum: 0
/// - containerView: nil
/// - Returns:
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
/// UICollectionViewCell
/// - Parameters:
/// - readerView:
/// - pageNum:
/// - Returns: nil 使 cell
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
/// /
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
/// /
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
}
///
///
@objc public protocol RDReaderDelegate: NSObjectProtocol {
///
/// - Parameters:
/// - readerView:
/// - pageNum:
func pageNum(readerView: RDReaderView, pageNum: Int)
///
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
}
extension RDReaderView {
/// RDReaderView 使
public enum DisplayType {
/// 仿使 UIPageViewController
case pageCurl
/// 使 UICollectionView +
case horizontalScroll
/// 使 UICollectionView
case verticalScroll
}
///
public enum PageDirection {
/// /
case leftToRight
///
case rightToLeft
}
}
///
/// RDReaderView
///
///
/// 使
/// 1. ``dataSource``
/// 2. ``delegate``
/// 3. ``switchReaderDisplayType(_:)``
/// 4. ``reloadData()``
///
///
/// - pageCurl / horizontalScroll / verticalScroll
/// -
/// - RTL
/// - //
/// -
public class RDReaderView: UIView {
///
///
enum TapEvent {
///
case none
/// 1/3RTL
case left
/// 1/3/
case center
/// 1/3RTL
case right
}
/// 仿使 UIPageViewController
/// pageCurl 仿
private lazy var pageViewController: UIPageViewController = {
let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
pageVC.delegate = self
pageVC.dataSource = self
return pageVC
}()
///
private lazy var layout: RDReaderFlowLayout = {
let layout = RDReaderFlowLayout(displayType: .horizontalScroll)
layout.dataSource = self
layout.delegate = self
return layout
}()
/// 使 UICollectionView
/// horizontalScroll verticalScroll
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = UIColor.clear
return collectionView
}()
///
/// delegate
/// -1
public var currentPage: Int = -1 {
didSet {
if let delegate = delegate, currentPage != oldValue, delegate.responds(to: #selector(RDReaderDelegate.pageNum(readerView:pageNum:))) {
delegate.pageNum(readerView: self, pageNum: currentPage)
}
}
}
///
///
private(set) lazy var tapGestureRecognizer: UITapGestureRecognizer = {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(tap:)))
return tap
}()
///
///
private var tapEvent: TapEvent = .none {
didSet {
let isRTL = pageDirection == .rightToLeft
switch tapEvent {
case .left:
if currentDisplayType != .pageCurl {
if isRTL { goNextPage() } else { goPreviousPage() }
}
case .right:
if currentDisplayType != .pageCurl {
if isRTL { goPreviousPage() } else { goNextPage() }
}
case .center:
tapCenter()
default:
break
}
}
}
///
/// 使 adjacentDualPage +1
private func goNextPage() {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
if pagesPerScreen > 1 {
if let target = adjacentDualPage(from: currentPage, forward: true) {
transitionToPage(pageNum: target, animated: true)
}
} else if currentPage + 1 < totalPages {
transitionToPage(pageNum: currentPage + 1, animated: true)
}
}
///
/// 使 adjacentDualPage -1
private func goPreviousPage() {
if pagesPerScreen > 1 {
if let target = adjacentDualPage(from: currentPage, forward: false) {
transitionToPage(pageNum: target, animated: true)
}
} else if currentPage - 1 >= 0 {
transitionToPage(pageNum: currentPage - 1, animated: true)
}
}
///
public weak var dataSource: RDReaderDataSource? = nil
///
public weak var delegate: RDReaderDelegate? = nil
/// 仿
public var currentDisplayType: RDReaderView.DisplayType = .pageCurl
/// / 0.3
public var toolViewAnimationDuration: TimeInterval = 0.3
///
public var landscapeDualPageEnabled: Bool = false
/// /
public var pageDirection: RDReaderView.PageDirection = .leftToRight
///
/// nil 0+1, 2+3, 4+5...
public var coverPageIndex: Int? = nil
///
private var hasCoverPage: Bool {
return coverPageIndex != nil
}
/// >
private var isLandscape: Bool {
return bounds.width > bounds.height
}
///
/// 12
public var pagesPerScreen: Int {
if !landscapeDualPageEnabled { return 1 }
if currentDisplayType == .verticalScroll { return 1 }
return isLandscape ? 2 : 1
}
///
/// - Parameter pageNum:
/// - Returns:
public func isFullScreenPage(_ pageNum: Int) -> Bool {
guard landscapeDualPageEnabled, isLandscape, let coverIndex = coverPageIndex else { return false }
return pageNum == coverIndex
}
///
///
/// - Parameter pageNum:
/// - Returns: (, (nil))
private func dualPagePair(for pageNum: Int) -> (left: Int, right: Int?) {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
if let coverIndex = coverPageIndex {
if pageNum == coverIndex {
//
return (coverIndex, nil)
}
// 1
// 0 1+2, 3+4, 5+6...
let adjustedIndex = pageNum - (coverIndex + 1) //
let pairStart = coverIndex + 1 + (adjustedIndex / 2) * 2
let left = pairStart
let right = pairStart + 1 < totalPages ? pairStart + 1 : nil
return (left, right)
} else {
// 0+1, 2+3, 4+5...
let left = (pageNum / 2) * 2
let right = left + 1 < totalPages ? left + 1 : nil
return (left, right)
}
}
/// /
/// - Parameters:
/// - pageNum:
/// - forward: true=false=
/// - Returns: nil
private func adjacentDualPage(from pageNum: Int, forward: Bool) -> Int? {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
let pair = dualPagePair(for: pageNum)
if forward {
let nextStart = (pair.right ?? pair.left) + 1
return nextStart < totalPages ? nextStart : nil
} else {
let prevEnd = pair.left - 1
guard prevEnd >= 0 else { return nil }
return dualPagePair(for: prevEnd).left
}
}
///
private func clampedPageNumber(_ pageNum: Int) -> Int? {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
guard totalPages > 0 else { return nil }
return min(max(pageNum, 0), totalPages - 1)
}
/// layoutSubviews
private var previousIsLandscape: Bool?
/// key
private var contentViews = [String : UIView.Type]()
/// pageCurl
private var willPreviousTransitionToViewController: UIViewController? = nil
/// pageCurl
private var willNextTransitionToViewController: UIViewController? = nil
/// pageCurl
private var willTransitionToViewController: UIViewController? = nil
///
private var topToolView: UIView?
///
private var bottomToolView: UIView?
///
private var topToolViewHeightConstraint: NSLayoutConstraint?
///
private var bottomToolViewHeightConstraint: NSLayoutConstraint?
///
private var isShowToolView: Bool = false
///
private var isTransitioning: Bool = false
/// UI
private var didBuildUI = false
/// 宿
private let preloadHostView = UIView()
/// key
private var preloadedPageViews: [Int: UIView] = [:]
/// pageCurl key
private var pageCurlCachedViews: [Int: UIView] = [:]
/// true=false=
private var predictedPageDirection: Bool?
/// 1
public var preloadRadius: Int = 1
///
private var cacheSignature: CacheSignature?
/// pageCurl
static let blankPageNum = Int.max
/// pageCurl
static let blankEndPageNum = Int.max - 1
///
///
private struct CacheSignature: Equatable {
///
let displayType: DisplayType
///
let isLandscape: Bool
///
let pagesPerScreen: Int
///
let boundsSize: CGSize
}
/// pageCurl
private struct PageTransitionRequest: Equatable {
let pageNum: Int
let animated: Bool
}
private var pendingTransitionRequest: PageTransitionRequest?
override init(frame: CGRect) {
super.init(frame: frame)
}
///
///
public override func layoutSubviews() {
super.layoutSubviews()
guard bounds.width > 0, bounds.height > 0 else { return }
preloadHostView.frame = bounds
updateToolViewHeightConstraintsIfNeeded()
let nowLandscape = isLandscape
if let prev = previousIsLandscape, prev != nowLandscape {
previousIsLandscape = nowLandscape
// RunLoop layoutSubviews reloadData/layoutIfNeeded
// collectionView
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.orientationChanged(isNowLandscape: nowLandscape)
}
} else if previousIsLandscape == nil {
previousIsLandscape = nowLandscape
layout.isLandscapeDualPage = landscapeDualPageEnabled && nowLandscape
}
}
///
///
/// - pageCurl UIPageViewController spineLocation
/// -
/// - Parameter isNowLandscape:
private func orientationChanged(isNowLandscape: Bool) {
let savedPage = max(0, currentPage)
// 1.
delegate?.readerViewOrientationWillChange?(readerView: self, isLandscape: isNowLandscape)
// 2.
layout.isLandscapeDualPage = landscapeDualPageEnabled && isNowLandscape
layout.coverPageIndex = coverPageIndex
switch currentDisplayType {
case .pageCurl:
// 仿 spineLocation PageViewController
rebuildPageViewController()
transitionToPage(pageNum: savedPage)
primePageCache(around: savedPage, preferredForward: predictedPageDirection)
default:
//
UIView.performWithoutAnimation {
// 使
collectionView.collectionViewLayout.invalidateLayout()
collectionView.reloadData()
collectionView.layoutIfNeeded()
// 3.
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
let safePage = min(savedPage, max(0, totalPages - 1))
let targetOffset = layout.currentContentOffset(count: safePage)
collectionView.setContentOffset(targetOffset, animated: false)
primePageCache(around: safePage, preferredForward: predictedPageDirection)
}
}
}
/// spine UIPageViewController
/// - Parameter isDualPage: .mid
/// - Returns: UIPageViewController
private func createPageViewController(isDualPage: Bool) -> UIPageViewController {
let options: [UIPageViewController.OptionsKey: Any]?
if isDualPage {
options = [.spineLocation: NSNumber(value: UIPageViewController.SpineLocation.mid.rawValue)]
} else {
options = nil
}
let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: options)
pageVC.delegate = self
pageVC.dataSource = self
pageVC.isDoubleSided = isDualPage
return pageVC
}
/// UIPageViewController
/// spineLocation
/// PageViewController
private func rebuildPageViewController() {
detachPageViewControllerIfNeeded()
let isDualPage = landscapeDualPageEnabled && isLandscape
let pageVC = createPageViewController(isDualPage: isDualPage)
pageViewController = pageVC
attachPageViewControllerIfNeeded()
}
// MARK: - UIPageViewController Fault Detection
/// UIPageViewController
///
/// - Parameter pageVC: UIPageViewController
/// - Returns: true
private func detectPageViewControllerFault(_ pageVC: UIPageViewController) -> Bool {
guard currentDisplayType == .pageCurl else { return false }
let expectedCount = (landscapeDualPageEnabled && isLandscape) ? 2 : 1
guard let viewControllers = pageVC.viewControllers,
viewControllers.count == expectedCount else {
return true
}
let childViewControllers = viewControllers.compactMap { $0 as? RDReaderPageChildViewController }
guard childViewControllers.count == expectedCount else {
return true
}
if expectedCount == 1 {
return childViewControllers.first?.pageNum != currentPage
}
let expectedPair = dualPagePair(for: currentPage)
let expectedRightPage = expectedPair.right
?? (isFullScreenPage(expectedPair.left) ? RDReaderView.blankPageNum : RDReaderView.blankEndPageNum)
return childViewControllers[0].pageNum != expectedPair.left
|| childViewControllers[1].pageNum != expectedRightPage
}
/// UIPageViewController
/// PageViewController
private func patchPageViewControllerFault() {
guard currentPage >= 0 else { return }
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.isTransitioning = false
self.pendingTransitionRequest = nil
self.willPreviousTransitionToViewController = nil
self.willNextTransitionToViewController = nil
self.willTransitionToViewController = nil
self.invalidatePageCaches()
self.rebuildPageViewController()
self.transitionToPage(pageNum: self.currentPage, animated: false)
}
}
/// pageCurl
private func shouldQueuePageTransition(_ request: PageTransitionRequest) -> Bool {
guard currentDisplayType == .pageCurl, isTransitioning else { return false }
pendingTransitionRequest = request
return true
}
/// pageCurl
private func finishPageCurlTransition(repairFaultIfNeeded: Bool = true) {
isTransitioning = false
willPreviousTransitionToViewController = nil
willNextTransitionToViewController = nil
willTransitionToViewController = nil
if repairFaultIfNeeded, detectPageViewControllerFault(pageViewController) {
patchPageViewControllerFault()
return
}
guard let pendingTransitionRequest else { return }
self.pendingTransitionRequest = nil
transitionToPage(pageNum: pendingTransitionRequest.pageNum, animated: pendingTransitionRequest.animated)
}
// MARK: - Preload / Forecast
/// 宿
///
private func ensurePreloadHostView() {
guard preloadHostView.superview == nil else { return }
preloadHostView.isHidden = true
preloadHostView.isUserInteractionEnabled = false
preloadHostView.clipsToBounds = true
preloadHostView.frame = bounds
preloadHostView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(preloadHostView, at: 0)
}
///
/// - Parameter pageNum:
/// - Returns: true
private func shouldCachePage(_ pageNum: Int) -> Bool {
pageNum >= 0
&& pageNum != RDReaderView.blankPageNum
&& pageNum != RDReaderView.blankEndPageNum
&& pageNum < (dataSource?.pageCountOfReaderView(readerView: self) ?? 0)
}
///
private func currentCacheSignature() -> CacheSignature {
CacheSignature(
displayType: currentDisplayType,
isLandscape: isLandscape,
pagesPerScreen: pagesPerScreen,
boundsSize: bounds.size
)
}
///
///
private func invalidatePageCaches() {
pageCurlCachedViews.values.forEach { $0.removeFromSuperview() }
preloadedPageViews.values.forEach { $0.removeFromSuperview() }
pageCurlCachedViews.removeAll()
preloadedPageViews.removeAll()
cacheSignature = currentCacheSignature()
}
///
private func refreshCacheSignatureIfNeeded() {
let signature = currentCacheSignature()
if cacheSignature != signature {
invalidatePageCaches()
} else if cacheSignature == nil {
cacheSignature = signature
}
}
/// ""
///
private func spreadStart(for pageNum: Int) -> Int? {
guard shouldCachePage(pageNum) else { return nil }
let isDualPage = landscapeDualPageEnabled && isLandscape && currentDisplayType != .verticalScroll
guard isDualPage else { return pageNum }
return dualPagePair(for: pageNum).left
}
/// ""
///
private func spreadPageNumbers(startingAt pageNum: Int) -> Set<Int> {
guard shouldCachePage(pageNum) else { return [] }
let isDualPage = landscapeDualPageEnabled && isLandscape && currentDisplayType != .verticalScroll
guard isDualPage else { return [pageNum] }
let pair = dualPagePair(for: pageNum)
var pages: Set<Int> = [pair.left]
if let right = pair.right, shouldCachePage(right) {
pages.insert(right)
}
return pages
}
/// ""
private func adjacentSpreadStart(from pageNum: Int, forward: Bool) -> Int? {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
guard totalPages > 0,
let spreadStart = spreadStart(for: pageNum) else {
return nil
}
let isDualPage = landscapeDualPageEnabled && isLandscape && currentDisplayType != .verticalScroll
guard isDualPage else {
let target = forward ? spreadStart + 1 : spreadStart - 1
return shouldCachePage(target) ? target : nil
}
if forward {
return adjacentDualPage(from: spreadStart, forward: true)
}
let pair = dualPagePair(for: spreadStart)
let prevEnd = pair.left - 1
guard prevEnd >= 0 else { return nil }
return dualPagePair(for: prevEnd).left
}
///
private func visiblePageNumbers(for anchorPage: Int) -> Set<Int> {
spreadPageNumbers(startingAt: anchorPage)
}
///
///
private func pageViewForDisplay(pageNum: Int) -> UIView {
let reusableView = detachedReusablePageView(for: pageNum)
let view = dataSource?.pageContentView(readerView: self, pageNum: pageNum, containerView: reusableView)
?? reusableView
?? UIView()
pageCurlCachedViews[pageNum] = view
return view
}
/// page view
/// view pageCurl UIView
/// child controller
private func detachedReusablePageView(for pageNum: Int) -> UIView? {
if let preloaded = preloadedPageViews.removeValue(forKey: pageNum) {
preloaded.removeFromSuperview()
return preloaded
}
guard let cached = pageCurlCachedViews[pageNum], cached.superview == nil else {
return nil
}
pageCurlCachedViews.removeValue(forKey: pageNum)
return cached
}
///
///
private func trimCachedPageViews(keeping pageNumbers: Set<Int>) {
pageCurlCachedViews = pageCurlCachedViews.filter { key, value in
let keep = pageNumbers.contains(key)
if !keep {
value.removeFromSuperview()
}
return keep
}
preloadedPageViews = preloadedPageViews.filter { key, value in
let keep = pageNumbers.contains(key)
if !keep {
value.removeFromSuperview()
}
return keep
}
}
///
/// preloadRadius preloadRadius
/// preferredForward
private func forecastTargets(around pageNum: Int, preferredForward: Bool?) -> [Int] {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
guard totalPages > 0, shouldCachePage(pageNum) else { return [] }
var targets = Set<Int>()
var previousAnchor = pageNum
for _ in 0..<max(preloadRadius, 0) {
guard let prev = adjacentSpreadStart(from: previousAnchor, forward: false) else { break }
targets.formUnion(spreadPageNumbers(startingAt: prev))
previousAnchor = prev
}
var nextAnchor = pageNum
for _ in 0..<max(preloadRadius, 0) {
guard let next = adjacentSpreadStart(from: nextAnchor, forward: true) else { break }
targets.formUnion(spreadPageNumbers(startingAt: next))
nextAnchor = next
}
if let preferredForward,
let edgeAnchor = preferredForward ? adjacentSpreadStart(from: nextAnchor, forward: true)
: adjacentSpreadStart(from: previousAnchor, forward: false) {
targets.formUnion(spreadPageNumbers(startingAt: edgeAnchor))
}
return targets.sorted()
}
///
/// 1.
/// 2.
/// 3.
/// 4.
private func primePageCache(around pageNum: Int, preferredForward: Bool? = nil) {
refreshCacheSignatureIfNeeded()
let targets = forecastTargets(around: pageNum, preferredForward: preferredForward)
let keepSet = Set(targets).union(visiblePageNumbers(for: pageNum))
trimCachedPageViews(keeping: keepSet)
guard !targets.isEmpty else { return }
ensurePreloadHostView()
preloadHostView.frame = bounds
for targetPage in targets {
let existing = detachedReusablePageView(for: targetPage)
let contentView = dataSource?.pageContentView(readerView: self, pageNum: targetPage, containerView: existing) ?? existing ?? UIView()
preloadedPageViews[targetPage] = contentView
if contentView.superview !== preloadHostView {
contentView.removeFromSuperview()
preloadHostView.addSubview(contentView)
}
contentView.frame = preloadHostView.bounds
}
}
/// UI
public override func didMoveToSuperview() {
super.didMoveToSuperview()
if superview != nil {
makeUI()
}
}
/// pageViewController
///
private func attachPageViewControllerIfNeeded() {
guard let parentViewController = self.ss_superViewController else { return }
var didAddToParent = false
if pageViewController.parent !== parentViewController {
if pageViewController.parent != nil {
pageViewController.willMove(toParent: nil)
pageViewController.view.removeFromSuperview()
pageViewController.removeFromParent()
}
parentViewController.addChild(pageViewController)
didAddToParent = true
}
pageViewController.view.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
pageViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
if pageViewController.view.superview !== self {
insertSubview(pageViewController.view, at: 0)
}
if didAddToParent {
pageViewController.didMove(toParent: parentViewController)
}
if currentDisplayType == .pageCurl, !isTransitioning, pendingTransitionRequest != nil {
finishPageCurlTransition(repairFaultIfNeeded: false)
}
}
/// pageViewController
/// willMove/didMove
private func detachPageViewControllerIfNeeded() {
if pageViewController.parent != nil {
pageViewController.willMove(toParent: nil)
}
pageViewController.view.removeFromSuperview()
if pageViewController.parent != nil {
pageViewController.removeFromParent()
}
}
/// UI
/// cell
private func makeUI() {
guard !didBuildUI else {
if currentDisplayType == .pageCurl {
attachPageViewControllerIfNeeded()
}
return
}
didBuildUI = true
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(UICollectionViewCell.self))
collectionView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
if currentDisplayType == .pageCurl {
attachPageViewControllerIfNeeded()
}
ensurePreloadHostView()
cacheSignature = currentCacheSignature()
addGestureRecognizer(tapGestureRecognizer)
// touchUpInside
tapGestureRecognizer.cancelsTouchesInView = false
}
///
/// tapEvent
@objc private func tapAction(tap: UITapGestureRecognizer) {
let point = tap.location(in: tap.view)
if isShowToolView {
let hitView = hitTest(point, with: nil)
if let top = topToolView, isHitView(hitView, inside: top, point: point) { return }
if let bottom = bottomToolView, isHitView(hitView, inside: bottom, point: point) { return }
}
let viewFrame = tap.view!.frame
let leftFrame = CGRect(x: 0, y: 0, width: viewFrame.width / 3, height: viewFrame.height)
let centerFrame = CGRect(x: viewFrame.width / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height)
let rightFrame = CGRect(x: viewFrame.width * 2 / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height)
if leftFrame.contains(point) {
if isShowToolView {
tapEvent = .center
} else {
tapEvent = .left
}
}
if centerFrame.contains(point) {
tapEvent = .center
}
if rightFrame.contains(point) {
if isShowToolView {
tapEvent = .center
} else {
tapEvent = .right
}
}
}
/// /
///
///
private func tapCenter() {
isShowToolView = !isShowToolView
if isShowToolView {
if let topToolView = topToolView {
installToolViewIfNeeded(topToolView, position: .top)
layoutIfNeeded()
topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height)
UIView.animate(withDuration: toolViewAnimationDuration) {
topToolView.transform = .identity
}
}
if let bottomToolView = bottomToolView {
installToolViewIfNeeded(bottomToolView, position: .bottom)
layoutIfNeeded()
bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height)
UIView.animate(withDuration: toolViewAnimationDuration) {
bottomToolView.transform = .identity
}
}
collectionView.isUserInteractionEnabled = false
pageViewController.view.isUserInteractionEnabled = false
} else {
if let topToolView = topToolView {
UIView.animate(withDuration: toolViewAnimationDuration, animations: {
topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height)
}) { _ in
topToolView.removeFromSuperview()
}
}
if let bottomToolView = bottomToolView {
UIView.animate(withDuration: toolViewAnimationDuration, animations: {
bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height)
}) { _ in
bottomToolView.removeFromSuperview()
}
}
collectionView.isUserInteractionEnabled = true
pageViewController.view.isUserInteractionEnabled = true
}
}
///
private func isHitView(_ hitView: UIView?, inside toolView: UIView, point: CGPoint) -> Bool {
if toolView.frame.contains(point) {
return true
}
guard let hitView else { return false }
return hitView === toolView || hitView.isDescendant(of: toolView)
}
private enum ToolViewPosition {
case top
case bottom
}
private func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition) {
guard toolView.superview !== self else { return }
toolView.removeFromSuperview()
addSubview(toolView)
toolView.translatesAutoresizingMaskIntoConstraints = false
let heightConstraint: NSLayoutConstraint
switch position {
case .top:
topToolViewHeightConstraint?.isActive = false
heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .top))
topToolViewHeightConstraint = heightConstraint
NSLayoutConstraint.activate([
toolView.leadingAnchor.constraint(equalTo: leadingAnchor),
toolView.trailingAnchor.constraint(equalTo: trailingAnchor),
toolView.topAnchor.constraint(equalTo: topAnchor),
heightConstraint
])
case .bottom:
bottomToolViewHeightConstraint?.isActive = false
heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .bottom))
bottomToolViewHeightConstraint = heightConstraint
NSLayoutConstraint.activate([
toolView.leadingAnchor.constraint(equalTo: leadingAnchor),
toolView.trailingAnchor.constraint(equalTo: trailingAnchor),
toolView.bottomAnchor.constraint(equalTo: bottomAnchor),
heightConstraint
])
}
}
private func updateToolViewHeightConstraintsIfNeeded() {
topToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .top)
bottomToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .bottom)
}
private func resolvedToolViewHeight(for position: ToolViewPosition) -> CGFloat {
let contentHeight: CGFloat = 52
switch position {
case .top:
return safeAreaInsets.top + contentHeight
case .bottom:
return safeAreaInsets.bottom + contentHeight
}
}
/// 仿//
/// PageViewController CollectionView
/// - Parameter displayType:
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType) {
let previousDisplayType = currentDisplayType
self.currentDisplayType = displayType
if currentPage == -1 {
currentPage = 0
}
if previousDisplayType != displayType {
invalidatePageCaches()
}
//
layout.isLandscapeDualPage = landscapeDualPageEnabled && isLandscape
layout.coverPageIndex = coverPageIndex
switch displayType {
case .pageCurl:
self.collectionView.removeFromSuperview()
self.collectionView.transform = .identity
attachPageViewControllerIfNeeded()
rebuildPageViewController()
transitionToPage(pageNum: currentPage)
primePageCache(around: currentPage, preferredForward: predictedPageDirection)
default:
detachPageViewControllerIfNeeded()
// RTL collectionView
if pageDirection == .rightToLeft && displayType != .verticalScroll {
collectionView.transform = CGAffineTransform(scaleX: -1, y: 1)
} else {
collectionView.transform = .identity
}
// collectionView frame
collectionView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
insertSubview(self.collectionView, at: 0)
layout.displayType = displayType
transitionToPage(pageNum: currentPage)
primePageCache(around: currentPage, preferredForward: predictedPageDirection)
}
}
///
///
/// - pageCurl UIPageViewController.setViewControllers
/// - setContentOffset
/// - Parameters:
/// - pageNum: item
/// - animated:
public func transitionToPage(pageNum: Int, animated: Bool = false) {
guard let safePageNum = clampedPageNumber(pageNum) else { return }
switch currentDisplayType {
case .pageCurl:
let request = PageTransitionRequest(pageNum: safePageNum, animated: animated)
if shouldQueuePageTransition(request) {
return
}
let isDualPage = landscapeDualPageEnabled && isLandscape
// RTL
let direction: UIPageViewController.NavigationDirection
if pageDirection == .rightToLeft {
direction = safePageNum > currentPage ? .reverse : .forward
} else {
direction = safePageNum > currentPage ? .forward : .reverse
}
predictedPageDirection = currentPage >= 0 ? safePageNum >= currentPage : nil
attachPageViewControllerIfNeeded()
if isDualPage {
let pair = dualPagePair(for: safePageNum)
let leftContent = pageViewForDisplay(pageNum: pair.left)
let leftVC = RDReaderPageChildViewController(contentView: leftContent, pageNum: pair.left)
isTransitioning = animated
if let rightPage = pair.right {
let rightContent = pageViewForDisplay(pageNum: rightPage)
let rightVC = RDReaderPageChildViewController(contentView: rightContent, pageNum: rightPage)
pageViewController.setViewControllers([leftVC, rightVC], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
guard let self else { return }
self.finishPageCurlTransition()
}
} else {
//
let blankNum = isFullScreenPage(pair.left) ? RDReaderView.blankPageNum : RDReaderView.blankEndPageNum
let emptyVC = RDReaderPageChildViewController(contentView: UIView(), pageNum: blankNum)
pageViewController.setViewControllers([leftVC, emptyVC], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
guard let self else { return }
self.finishPageCurlTransition()
}
}
currentPage = pair.left
primePageCache(around: pair.left, preferredForward: predictedPageDirection)
} else {
let contentView = pageViewForDisplay(pageNum: safePageNum)
let vc = RDReaderPageChildViewController(contentView: contentView, pageNum: safePageNum)
isTransitioning = animated
pageViewController.setViewControllers([vc], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
guard let self else { return }
self.finishPageCurlTransition()
}
currentPage = safePageNum
primePageCache(around: safePageNum, preferredForward: predictedPageDirection)
}
default:
collectionView.reloadData()
collectionView.layoutIfNeeded()
predictedPageDirection = currentPage >= 0 ? safePageNum >= currentPage : nil
collectionView.setContentOffset(layout.currentContentOffset(count: safePageNum), animated: animated)
currentPage = safePageNum
primePageCache(around: safePageNum, preferredForward: predictedPageDirection)
}
}
///
///
public func reloadData() {
switchReaderDisplayType(currentDisplayType)
topToolView = self.dataSource?.topToolView?(readerView: self)
bottomToolView = self.dataSource?.bottomToolView?(readerView: self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
/// UIPageViewController
/// 仿
extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
///
///
private func makeSinglePageChildVC(for pageNum: Int) -> RDReaderPageChildViewController {
if pageNum == RDReaderView.blankPageNum || pageNum == RDReaderView.blankEndPageNum {
return RDReaderPageChildViewController(contentView: UIView(), pageNum: pageNum)
}
let contentView = pageViewForDisplay(pageNum: pageNum)
return RDReaderPageChildViewController(contentView: contentView, pageNum: pageNum)
}
/// ""
///
/// - Parameters:
/// - pageNum:
/// - isDualPage:
/// - Returns: nil
private func nextPageNum(after pageNum: Int, isDualPage: Bool) -> Int? {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
//
if pageNum == RDReaderView.blankEndPageNum {
return nil
}
if isDualPage, let coverIndex = coverPageIndex {
if pageNum == coverIndex {
return RDReaderView.blankPageNum
}
if pageNum == RDReaderView.blankPageNum {
let firstContent = coverIndex + 1
return firstContent < totalPages ? firstContent : nil
}
}
let next = pageNum + 1
if next < totalPages {
return next
}
// pageNum
if isDualPage {
if let coverIndex = coverPageIndex {
// =
let adjustedIndex = pageNum - (coverIndex + 1)
if adjustedIndex >= 0 && adjustedIndex % 2 == 0 {
return RDReaderView.blankEndPageNum
}
} else {
// =
if pageNum % 2 == 0 {
return RDReaderView.blankEndPageNum
}
}
}
return nil
}
/// ""
///
/// - Parameters:
/// - pageNum:
/// - isDualPage:
/// - Returns: nil
private func prevPageNum(before pageNum: Int, isDualPage: Bool) -> Int? {
//
if pageNum == RDReaderView.blankEndPageNum {
let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0
return totalPages > 0 ? totalPages - 1 : nil
}
if isDualPage, let coverIndex = coverPageIndex {
if pageNum == RDReaderView.blankPageNum {
return coverIndex
}
if pageNum == coverIndex + 1 {
return RDReaderView.blankPageNum
}
}
let prev = pageNum - 1
return prev >= 0 ? prev : nil
}
/// UIPageViewController /
/// RTL before/after before=
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? RDReaderPageChildViewController else { return nil }
let isDualPage = landscapeDualPageEnabled && isLandscape
let isRTL = pageDirection == .rightToLeft
// RTL before/after before=
let targetNum = isRTL
? nextPageNum(after: vc.pageNum, isDualPage: isDualPage)
: prevPageNum(before: vc.pageNum, isDualPage: isDualPage)
guard let num = targetNum else { return nil }
let targetVC = makeSinglePageChildVC(for: num)
willPreviousTransitionToViewController = targetVC
return targetVC
}
/// UIPageViewController /
/// RTL before/after
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? RDReaderPageChildViewController else { return nil }
let isDualPage = landscapeDualPageEnabled && isLandscape
let isRTL = pageDirection == .rightToLeft
let targetNum = isRTL
? prevPageNum(before: vc.pageNum, isDualPage: isDualPage)
: nextPageNum(after: vc.pageNum, isDualPage: isDualPage)
guard let num = targetNum else { return nil }
let targetVC = makeSinglePageChildVC(for: num)
willNextTransitionToViewController = targetVC
return targetVC
}
/// UIPageViewController
/// PageViewController
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed, let firstVC = pageViewController.viewControllers?.first as? RDReaderPageChildViewController {
let pn = firstVC.pageNum
if pn != RDReaderView.blankPageNum && pn != RDReaderView.blankEndPageNum {
currentPage = pn
primePageCache(around: pn, preferredForward: predictedPageDirection)
}
}
predictedPageDirection = nil
finishPageCurlTransition()
}
/// UIPageViewController
///
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
isTransitioning = true
willTransitionToViewController = pendingViewControllers.first
if let target = pendingViewControllers.first as? RDReaderPageChildViewController,
target.pageNum != RDReaderView.blankPageNum,
target.pageNum != RDReaderView.blankEndPageNum {
predictedPageDirection = target.pageNum >= currentPage
primePageCache(around: target.pageNum, preferredForward: predictedPageDirection)
}
}
}
/// UICollectionView
///
extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate, RDReaderFlowLayoutDataSoure {
/// UICollectionViewCell
/// DataSource cell
/// RTL cell 使
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let identifer = self.dataSource?.pageIdentifier(readerView: self, pageNum: indexPath.row) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifer, for: IndexPath(item: indexPath.row, section: 0)) as! RDReaderContentCell
let preloadedView = preloadedPageViews.removeValue(forKey: indexPath.row)
preloadedView?.removeFromSuperview()
let reusableView = cell.containerView ?? preloadedView
let conttainerView = self.dataSource?.pageContentView(readerView: self, pageNum: indexPath.row, containerView: reusableView)
if let conttainerView = conttainerView {
cell.containerView = conttainerView
}
// RTL cell 使collectionView
if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll {
cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1)
} else {
cell.contentView.transform = .identity
}
return cell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(UICollectionViewCell.self), for: indexPath)
return cell
}
/// item
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataSource?.pageCountOfReaderView(readerView: self) ?? 0
}
///
public func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int) {
if currentPage >= 0, currentPage != pageIndex {
predictedPageDirection = pageIndex >= currentPage
}
currentPage = pageIndex
primePageCache(around: pageIndex, preferredForward: predictedPageDirection)
}
///
/// nil 使
public func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat? {
return nil
}
}
///
/// UITableView register/dequeueReusable
extension RDReaderView {
///
/// - Parameters:
/// - contentView:
/// - identifier:
public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String) {
contentViews[identifier] = contentView
collectionView.register(RDReaderContentCell.self, forCellWithReuseIdentifier: identifier)
}
///
/// cell
public func dequeueReusableContentView(withReuseIdentifier identifier: String, for pageNum: Int) -> UIView {
if self.currentDisplayType != .pageCurl, let cell = self.collectionView.cellForItem(at: IndexPath(row: pageNum, section: 0)) as? RDReaderContentCell, let containerView = cell.containerView {
return containerView
}
let contentViewClass = contentViews[identifier]
assert(contentViewClass != nil, "请调用register(contentViewcontentViewWithReuseIdentifier")
var contentView = contentViewClass!.init()
return contentView
}
///
/// pageCurl PageViewController CollectionView cell
public func pageContentView(pageNum: Int) -> UIView? {
if currentDisplayType == .pageCurl {
return (self.pageViewController.viewControllers?.first as? RDReaderPageChildViewController)?.contentView
} else {
let cell = collectionView.cellForItem(at: IndexPath(item: pageNum, section: 0)) as? RDReaderContentCell
return cell?.containerView
}
}
///
/// 使退
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize {
let targetPage = pageNum ?? (currentPage >= 0 ? currentPage : nil)
if let targetPage,
let contentView = pageContentView(pageNum: targetPage),
contentView.bounds.width > 0,
contentView.bounds.height > 0 {
return contentView.bounds.size
}
if currentDisplayType == .pageCurl {
if let childViewController = pageViewController.viewControllers?.first as? RDReaderPageChildViewController,
childViewController.view.bounds.width > 0,
childViewController.view.bounds.height > 0 {
return childViewController.view.bounds.size
}
if pageViewController.view.bounds.width > 0, pageViewController.view.bounds.height > 0 {
return pageViewController.view.bounds.size
}
return bounds.size
}
let containerBounds = collectionView.bounds.size == .zero ? bounds.size : collectionView.bounds.size
guard containerBounds.width > 0, containerBounds.height > 0 else {
return bounds.size
}
switch currentDisplayType {
case .horizontalScroll:
return CGSize(
width: containerBounds.width / CGFloat(max(pagesPerScreen, 1)),
height: containerBounds.height
)
case .verticalScroll:
return containerBounds
case .pageCurl:
return bounds.size
}
}
}
private var cellViewKey: Int8 = 0
/// UIView UIViewController
extension UIView {
/// 沿 UIViewController
///
var ss_superViewController: UIViewController? {
var next = self.next
while next != nil {
if next is UIViewController {
return next as? UIViewController
} else {
next = next!.next
}
}
return nil
}
}