UICollectionView 水平分页布局
Posted
技术标签:
【中文标题】UICollectionView 水平分页布局【英文标题】:UICollectionView horizontal paging layout 【发布时间】:2019-03-13 08:21:02 【问题描述】:嘿,我正在尝试实现这个 UI 元素,它看起来(对我来说)就像一个水平的 UIPickerView。这是在 ios 上创建“memoji”时的示例 GIF:
Example GIF
我一直在尝试使用 UICollectionView 和自定义 UICollectionViewFlowLayout 来完成此操作。但运气不佳。
到目前为止我尝试的是使用
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
在每个单元格上停止滚动,从而给它一种分页的感觉。但为了做到这一点,我实际上必须制作
var collectionViewContentSize: CGSize
返回比实际存在更大的内容大小,否则它只会反弹collectionView,无论我在前一个函数中返回什么,都不会立即到位。
我也尝试过使用
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
设置collectionView.contentOffset
但这会导致动画中出现奇怪的跳跃,并且再次无法正确更改此设置。
除了每个单元格的分页之外,我还想在那个 UI 元素上实现什么,当通过元素时每个滚动上都有一个小的触觉反馈,以及边框上左右元素的淡入和淡出。如果有人能指出我正确的方向,也许 UICollectionView 不是要走的路?我会很感激。谢谢
【问题讨论】:
为什么不能使用 UISegmentedControl? Hy Sachin,因为它不允许我做我想做的所有事情 :) 滚动、保持中心突出显示的元素、淡出边框元素等。 我认为可以使用两个collectionview 一个单元格宽度== screen.width,另一个单元格宽度== screen.width/2。第二个视图跟随第一个视图(滚动) @GaloTorresSevilla 是的,在单元格小于屏幕宽度的情况下,分页不起作用:) @DiogoAntunes 是的。绝对正确。我想这会对你有所帮助:***.com/a/49617263/9731167 【参考方案1】:我能够使用自定义 UICollectionViewFlowLayout
实现这一目标
final class PaginatedCollectionViewFlow: UICollectionViewFlowLayout
/* Distance from the midle to the other side of the screen */
var availableDistance: CGFloat = 0.0
var midX: CGFloat = 0
var lastElementIndex = 0
let maxAngle = CGFloat(-60.0.degree2Rad)
override func prepare ()
minimumInteritemSpacing = 40.0
scrollDirection = .horizontal
/* This should be cached */
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
guard
let layoutAttributes = super.layoutAttributesForElements(in: rect),
let cv = collectionView else return nil
/* Size of the collectionView */
let visibleRect = CGRect(origin: cv.contentOffset, size: cv.bounds.size)
let attributes: [UICollectionViewLayoutAttributes] = layoutAttributes.compactMap attribute in
guard let copy = attribute.copy() as? UICollectionViewLayoutAttributes else return nil
/* Distance from the middle of the screen to the middle of the cell attributes */
let distance = visibleRect.midX - attribute.center.x
/* Normalize the distance between [0, 1] */
let normalizedDistance = abs(distance / availableDistance)
/* Rotate the cell and apply alpha accordingly to the maximum distance from the center */
copy.alpha = 1.0 - normalizedDistance
copy.transform3D = CATransform3DMakeRotation(maxAngle * normalizedDistance, 0, 1, 0)
return copy
return attributes
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
return true
添加UICollectionView后一定要设置UICollectionViewFlowLayout
参数:
guard let flow = collectionView.collectionViewLayout as? PaginatedCollectionViewFlow else return
/* Distance from the middle to the other side of the screen */
flow.availableDistance = floor(view.bounds.width / 2.0)
/* Middle of the screen */
flow.midX = ceil(view.bounds.midX)
/* Index of the last element in the collectionView */
flow.lastElementIndex = vm.numberOfItems - 1
/* Left and Right Insets */
flow.sectionInset.left = flow.midX - 30.0
flow.sectionInset.right = flow.midX - 30.0
最后在符合UICollectionViewDelegate
后得到UIScrollView
委托方法:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
scrollToPosition(scrollView: scrollView)
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
guard !decelerate else return
scrollToPosition(scrollView: scrollView)
internal func scrollToPosition(scrollView: UIScrollView)
guard let ip = indexPathForCenterCell else return
scrollToIndex(ip.row, animated: true)
internal func scrollToIndex(_ index: Int, animated: Bool)
let ip = IndexPath(item: index, section: 0)
guard let attributes = collectionView.layoutAttributesForItem(at: ip) else return
let halfWidth = collectionView.frame.width / CGFloat(2.0)
let offset = CGPoint(x: attributes.frame.midX - halfWidth, y: 0)
collectionView.setContentOffset(offset, animated: animated)
guard let cell = collectionView.cellForItem(at: ip) else return
feedbackGenerator.selectionChanged()
cell.isHighlighted = true
collectionView.visibleCells.filter $0 != cell .forEach $0.isHighlighted = false
internal var indexPathForCenterCell: IndexPath?
let point = collectionView.convert(collectionView.center, from: collectionView.superview)
guard let indexPath = collectionView.indexPathForItem(at: point) else return collectionView.indexPathsForVisibleItems.first
return indexPath
/* Gets the CGSize based of a maximum size available for the provided String */
func sizeFor(text: String) -> CGSize
guard let font = UIFont(font: .sanFranciscoSemiBold, size: 15.0) else return .zero
let textNS = text as NSString
let maxSize = CGSize(width: collectionView.frame.width / 2, height: collectionView.frame.height)
let frame = textNS.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : font], context: nil)
return frame.size
这提供了 UICollectionViewCells 的分页,同时将其“捕捉”到最近的单元格,并使用 UISelectionFeedbackGenerator
生成触觉反馈。希望这可以帮助遇到同样问题的人。
【讨论】:
以上是关于UICollectionView 水平分页布局的主要内容,如果未能解决你的问题,请参考以下文章
UICollectionView - 一次一个单元格的水平分页