版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.04.27 星期六 |
前言
iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
源码
1. Swift
首先看下代码组织结构
接着看一下sb文件
1. CustomLayoutAttributes.swift
import UIKit
final class CustomLayoutAttributes: UICollectionViewLayoutAttributes {
// MARK: - Properties
var parallax: CGAffineTransform = .identity
var initialOrigin: CGPoint = .zero
var headerOverlayAlpha = CGFloat(0)
// MARK: - Life Cycle
override func copy(with zone: NSZone?) -> Any {
guard let copiedAttributes = super.copy(with: zone) as? CustomLayoutAttributes else {
return super.copy(with: zone)
}
copiedAttributes.parallax = parallax
copiedAttributes.initialOrigin = initialOrigin
copiedAttributes.headerOverlayAlpha = headerOverlayAlpha
return copiedAttributes
}
override func isEqual(_ object: Any?) -> Bool {
guard let otherAttributes = object as? CustomLayoutAttributes else {
return false
}
if NSValue(cgAffineTransform: otherAttributes.parallax) != NSValue(cgAffineTransform: parallax)
|| otherAttributes.initialOrigin != initialOrigin
|| otherAttributes.headerOverlayAlpha != headerOverlayAlpha {
return false
}
return super.isEqual(object)
}
}
2. CustomLayoutSettings.swift
import UIKit
struct CustomLayoutSettings {
// Elements sizes
var itemSize: CGSize?
var headerSize: CGSize?
var menuSize: CGSize?
var sectionsHeaderSize: CGSize?
var sectionsFooterSize: CGSize?
// Behaviours
var isHeaderStretchy: Bool
var isAlphaOnHeaderActive: Bool
var headerOverlayMaxAlphaValue: CGFloat
var isMenuSticky: Bool
var isSectionHeadersSticky: Bool
var isParallaxOnCellsEnabled: Bool
// Spacing
var minimumInteritemSpacing: CGFloat
var minimumLineSpacing: CGFloat
var maxParallaxOffset: CGFloat
}
extension CustomLayoutSettings {
init() {
self.itemSize = nil
self.headerSize = nil
self.menuSize = nil
self.sectionsHeaderSize = nil
self.sectionsFooterSize = nil
self.isHeaderStretchy = false
self.isAlphaOnHeaderActive = true
self.headerOverlayMaxAlphaValue = 0
self.isMenuSticky = false
self.isSectionHeadersSticky = false
self.isParallaxOnCellsEnabled = false
self.maxParallaxOffset = 0
self.minimumInteritemSpacing = 0
self.minimumLineSpacing = 0
}
}
3. CustomLayout.swift
import UIKit
final class CustomLayout: UICollectionViewLayout {
enum Element: String {
case header
case menu
case sectionHeader
case sectionFooter
case cell
var id: String {
return self.rawValue
}
var kind: String {
return "Kind\(self.rawValue.capitalized)"
}
}
override public class var layoutAttributesClass: AnyClass {
return CustomLayoutAttributes.self
}
override public var collectionViewContentSize: CGSize {
return CGSize(width: collectionViewWidth, height: contentHeight)
}
// MARK: - Properties
var settings = CustomLayoutSettings()
private var oldBounds = CGRect.zero
private var contentHeight = CGFloat()
private var cache = [Element: [IndexPath: CustomLayoutAttributes]]()
private var visibleLayoutAttributes = [CustomLayoutAttributes]()
private var zIndex = 0
private var collectionViewHeight: CGFloat {
return collectionView!.frame.height
}
private var collectionViewWidth: CGFloat {
return collectionView!.frame.width
}
private var cellHeight: CGFloat {
guard let itemSize = settings.itemSize else {
return collectionViewHeight
}
return itemSize.height
}
private var cellWidth: CGFloat {
guard let itemSize = settings.itemSize else {
return collectionViewWidth
}
return itemSize.width
}
private var headerSize: CGSize {
guard let headerSize = settings.headerSize else {
return .zero
}
return headerSize
}
private var menuSize: CGSize {
guard let menuSize = settings.menuSize else {
return .zero
}
return menuSize
}
private var sectionsHeaderSize: CGSize {
guard let sectionsHeaderSize = settings.sectionsHeaderSize else {
return .zero
}
return sectionsHeaderSize
}
private var sectionsFooterSize: CGSize {
guard let sectionsFooterSize = settings.sectionsFooterSize else {
return .zero
}
return sectionsFooterSize
}
private var contentOffset: CGPoint {
return collectionView!.contentOffset
}
}
// MARK: - LAYOUT CORE PROCESS
extension CustomLayout {
override public func prepare() {
guard let collectionView = collectionView,
cache.isEmpty else {
return
}
prepareCache()
contentHeight = 0
zIndex = 0
oldBounds = collectionView.bounds
let itemSize = CGSize(width: cellWidth, height: cellHeight)
let headerAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.header.kind,
with: IndexPath(item: 0, section: 0)
)
prepareElement(size: headerSize, type: .header, attributes: headerAttributes)
let menuAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.menu.kind,
with: IndexPath(item: 0, section: 0))
prepareElement(size: menuSize, type: .menu, attributes: menuAttributes)
for section in 0 ..< collectionView.numberOfSections {
let sectionHeaderAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: UICollectionElementKindSectionHeader,
with: IndexPath(item: 0, section: section))
prepareElement(
size: sectionsHeaderSize,
type: .sectionHeader,
attributes: sectionHeaderAttributes)
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let cellIndexPath = IndexPath(item: item, section: section)
let attributes = CustomLayoutAttributes(forCellWith: cellIndexPath)
let lineInterSpace = settings.minimumLineSpacing
attributes.frame = CGRect(
x: 0 + settings.minimumInteritemSpacing,
y: contentHeight + lineInterSpace,
width: itemSize.width,
height: itemSize.height
)
attributes.zIndex = zIndex
contentHeight = attributes.frame.maxY
cache[.cell]?[cellIndexPath] = attributes
zIndex += 1
}
let sectionFooterAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: UICollectionElementKindSectionFooter,
with: IndexPath(item: 1, section: section))
prepareElement(
size: sectionsFooterSize,
type: .sectionFooter,
attributes: sectionFooterAttributes)
}
updateZIndexes()
}
override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if oldBounds.size != newBounds.size {
cache.removeAll(keepingCapacity: true)
}
return true
}
private func prepareCache() {
cache.removeAll(keepingCapacity: true)
cache[.header] = [IndexPath: CustomLayoutAttributes]()
cache[.menu] = [IndexPath: CustomLayoutAttributes]()
cache[.sectionHeader] = [IndexPath: CustomLayoutAttributes]()
cache[.sectionFooter] = [IndexPath: CustomLayoutAttributes]()
cache[.cell] = [IndexPath: CustomLayoutAttributes]()
}
private func prepareElement(size: CGSize, type: Element, attributes: CustomLayoutAttributes) {
guard size != .zero else { return }
attributes.initialOrigin = CGPoint(x: 0, y: contentHeight)
attributes.frame = CGRect(origin: attributes.initialOrigin, size: size)
attributes.zIndex = zIndex
zIndex += 1
contentHeight = attributes.frame.maxY
cache[type]?[attributes.indexPath] = attributes
}
private func updateZIndexes(){
guard let sectionHeaders = cache[.sectionHeader] else { return }
var sectionHeadersZIndex = zIndex
for (_, attributes) in sectionHeaders {
attributes.zIndex = sectionHeadersZIndex
sectionHeadersZIndex += 1
}
cache[.menu]?.first?.value.zIndex = sectionHeadersZIndex
}
}
//MARK: - PROVIDING ATTRIBUTES TO THE COLLECTIONVIEW
extension CustomLayout {
public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
switch elementKind {
case UICollectionElementKindSectionHeader:
return cache[.sectionHeader]?[indexPath]
case UICollectionElementKindSectionFooter:
return cache[.sectionFooter]?[indexPath]
case Element.header.kind:
return cache[.header]?[indexPath]
default:
return cache[.menu]?[indexPath]
}
}
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[.cell]?[indexPath]
}
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
visibleLayoutAttributes.removeAll(keepingCapacity: true)
let halfHeight = collectionViewHeight * 0.5
let halfCellHeight = cellHeight * 0.5
for (type, elementInfos) in cache {
for (indexPath, attributes) in elementInfos {
attributes.parallax = .identity
attributes.transform = .identity
updateSupplementaryViews(type, attributes: attributes, collectionView: collectionView, indexPath: indexPath)
if attributes.frame.intersects(rect) {
if type == .cell,
settings.isParallaxOnCellsEnabled {
updateCells(attributes, halfHeight: halfHeight, halfCellHeight: halfCellHeight)
}
visibleLayoutAttributes.append(attributes)
}
}
}
return visibleLayoutAttributes
}
private func updateSupplementaryViews(_ type: Element, attributes: CustomLayoutAttributes, collectionView: UICollectionView, indexPath: IndexPath) {
if type == .sectionHeader,
settings.isSectionHeadersSticky {
let upperLimit = CGFloat(collectionView.numberOfItems(inSection: indexPath.section)) * (cellHeight + settings.minimumLineSpacing)
let menuOffset = settings.isMenuSticky ? menuSize.height : 0
attributes.transform = CGAffineTransform(
translationX: 0,
y: min(upperLimit, max(0, contentOffset.y - attributes.initialOrigin.y + menuOffset)))
} else if type == .header,
settings.isHeaderStretchy {
let updatedHeight = min(
collectionView.frame.height,
max(headerSize.height, headerSize.height - contentOffset.y))
let scaleFactor = updatedHeight / headerSize.height
let delta = (updatedHeight - headerSize.height) / 2
let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
let translation = CGAffineTransform(translationX: 0, y: min(contentOffset.y, headerSize.height) + delta)
attributes.transform = scale.concatenating(translation)
if settings.isAlphaOnHeaderActive {
attributes.headerOverlayAlpha = min(settings.headerOverlayMaxAlphaValue, contentOffset.y / headerSize.height)
}
} else if type == .menu,
settings.isMenuSticky {
attributes.transform = CGAffineTransform(translationX: 0, y: max(attributes.initialOrigin.y, contentOffset.y) - headerSize.height)
}
}
private func updateCells(_ attributes: CustomLayoutAttributes, halfHeight: CGFloat, halfCellHeight: CGFloat) {
let cellDistanceFromCenter = attributes.center.y - contentOffset.y - halfHeight
let parallaxOffset = -(settings.maxParallaxOffset * cellDistanceFromCenter) / (halfHeight + halfCellHeight)
let boundedParallaxOffset = min(max(-settings.maxParallaxOffset, parallaxOffset), settings.maxParallaxOffset)
attributes.parallax = CGAffineTransform(translationX: 0, y: boundedParallaxOffset)
}
}
4. MockDataManager.swift
protocol Team {
var marks: [String] { get }
var playerPictures: [[String]] { get }
}
struct Owls: Team {
let marks = ["4/5", "3/5", "4/5", "2/5"]
let playerPictures = [
["Owls-goalkeeper"],
["Owls-d1", "Owls-d2", "Owls-d3", "Owls-d4"],
["Owls-m1", "Owls-m2", "Owls-m3", "Owls-m4"],
["Owls-f1", "Owls-f2"]
]
}
struct Tigers: Team {
let marks = ["1/5", "3/5", "3/5", "5/5"]
let playerPictures = [
["Tigers-goalkeeper"],
["Tigers-d1", "Tigers-d2", "Tigers-d3", "Tigers-d4"],
["Tigers-m1", "Tigers-m2", "Tigers-m3", "Tigers-m4"],
["Tigers-f1", "Tigers-f2"]
]
}
struct Parrots: Team {
let marks = ["3/5", "2/5", "4/5", "5/5"]
let playerPictures = [
["Parrots-goalkeeper"],
["Parrots-d1", "Parrots-d2", "Parrots-d3", "Parrots-d4"],
["Parrots-m1", "Parrots-m2", "Parrots-m3", "Parrots-m4"],
["Parrots-f1", "Parrots-f2"]
]
}
struct Giraffes: Team {
let marks = ["5/5", "4/5", "3/5", "1/5"]
let playerPictures = [
["Giraffes-goalkeeper"],
["Giraffes-d1", "Giraffes-d2", "Giraffes-d3", "Giraffes-d4"],
["Giraffes-m1", "Giraffes-m2", "Giraffes-m3", "Giraffes-m4"],
["Giraffes-f1", "Giraffes-f2"]
]
}
5. HeaderView.swift
import UIKit
final class HeaderView: UICollectionReusableView {
// MARK: - IBOutlets
@IBOutlet weak var overlayView: UIView!
// MARK: - Life Cycle
open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
guard let customFlowLayoutAttributes = layoutAttributes as? CustomLayoutAttributes else {
return
}
overlayView?.alpha = customFlowLayoutAttributes.headerOverlayAlpha
}
}
6. MenuView.swift
import UIKit
protocol MenuViewDelegate {
func reloadCollectionViewDataWithTeamIndex(_ index: Int)
}
final class MenuView: UICollectionReusableView {
// MARK: - Properties
var delegate: MenuViewDelegate?
// MARK: - View Life Cycle
override func prepareForReuse() {
super.prepareForReuse()
delegate = nil
}
}
// MARK: - IBActions
extension MenuView {
@IBAction func tappedButton(_ sender: UIButton) {
delegate?.reloadCollectionViewDataWithTeamIndex(sender.tag)
}
}
7. SectionHeaderView.swift
import UIKit
final class SectionHeaderView: UICollectionReusableView {
// MARK: - IBOutlets
@IBOutlet weak var title: UILabel!
}
8. PlayerCell.swift
import UIKit
final class PlayerCell: UICollectionViewCell {
// MARK: - IBOutlets
@IBOutlet weak var picture: UIImageView!
// MARK: - View Life Cycle
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
guard let attributes = layoutAttributes as? CustomLayoutAttributes else {
return
}
picture.transform = attributes.parallax
}
override func prepareForReuse() {
super.prepareForReuse()
picture.transform = .identity
}
}
9. SectionFooterView.swift
import UIKit
final class SectionFooterView: UICollectionReusableView {
// MARK: - IBOutlets
@IBOutlet weak var mark: UILabel!
}
10. JungleCupCollectionViewController.swift
import UIKit
final class JungleCupCollectionViewController: UICollectionViewController {
// MARK: - Properties
var customLayout: CustomLayout? {
return collectionView?.collectionViewLayout as? CustomLayout
}
private let teams: [Team] = [Owls(), Giraffes(), Parrots(), Tigers()]
private let sections = ["Goalkeeper", "Defenders", "Midfielders", "Forwards"]
private var displayedTeam = 0
override var prefersStatusBarHidden: Bool {
return true
}
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionViewLayout()
}
}
private extension JungleCupCollectionViewController {
func setupCollectionViewLayout() {
guard let collectionView = collectionView, let customLayout = customLayout else { return }
collectionView.register(
UINib(nibName: "HeaderView", bundle: nil),
forSupplementaryViewOfKind: CustomLayout.Element.header.kind,
withReuseIdentifier: CustomLayout.Element.header.id
)
collectionView.register(
UINib(nibName: "MenuView", bundle: nil),
forSupplementaryViewOfKind: CustomLayout.Element.menu.kind,
withReuseIdentifier: CustomLayout.Element.menu.id
)
customLayout.settings.itemSize = CGSize(width: collectionView.frame.width, height: 200)
customLayout.settings.headerSize = CGSize(width: collectionView.frame.width, height: 300)
customLayout.settings.menuSize = CGSize(width: collectionView.frame.width, height: 70)
customLayout.settings.sectionsHeaderSize = CGSize(width: collectionView.frame.width, height: 50)
customLayout.settings.sectionsFooterSize = CGSize(width: collectionView.frame.width, height: 50)
customLayout.settings.isHeaderStretchy = true
customLayout.settings.isAlphaOnHeaderActive = true
customLayout.settings.headerOverlayMaxAlphaValue = CGFloat(0.6)
customLayout.settings.isMenuSticky = true
customLayout.settings.isSectionHeadersSticky = true
customLayout.settings.isParallaxOnCellsEnabled = true
customLayout.settings.maxParallaxOffset = 60
customLayout.settings.minimumInteritemSpacing = 0
customLayout.settings.minimumLineSpacing = 3
}
}
//MARK: - UICollectionViewDataSource
extension JungleCupCollectionViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return sections.count
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return teams[displayedTeam].playerPictures[section].count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomLayout.Element.cell.id, for: indexPath)
if let playerCell = cell as? PlayerCell {
playerCell.picture.image = UIImage(named: teams[displayedTeam].playerPictures[indexPath.section][indexPath.item])
}
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let supplementaryView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomLayout.Element.sectionHeader.id, for: indexPath)
if let sectionHeaderView = supplementaryView as? SectionHeaderView {
sectionHeaderView.title.text = sections[indexPath.section]
}
return supplementaryView
case UICollectionElementKindSectionFooter:
let supplementaryView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomLayout.Element.sectionFooter.id, for: indexPath)
if let sectionFooterView = supplementaryView as? SectionFooterView {
sectionFooterView.mark.text = "Strength: \(teams[displayedTeam].marks[indexPath.section])"
}
return supplementaryView
case CustomLayout.Element.header.kind:
let topHeaderView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: CustomLayout.Element.header.id,
for: indexPath
)
return topHeaderView
case CustomLayout.Element.menu.kind:
let menuView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: CustomLayout.Element.menu.id,
for: indexPath
)
if let menuView = menuView as? MenuView {
menuView.delegate = self
}
return menuView
default:
fatalError("Unexpected element kind")
}
}
}
// MARK: - MenuViewDelegate
extension JungleCupCollectionViewController: MenuViewDelegate {
func reloadCollectionViewDataWithTeamIndex(_ index: Int) {
displayedTeam = index
collectionView?.reloadData()
}
}
后记
本篇主要讲述了基于自定义UICollectionViewLayout布局的简单示例,感兴趣的给个赞或者关注~~~
网友评论