隐形细胞集合查看动画
Invisible Cell CollectionView Animation
我对在 collectionView 中管理动画不可见单元感到困惑。
所以我有堆叠卡片集合视图,当用户捏出集合单元格时,x 坐标会发生变化。更改坐标后,一些单元格在屏幕外。当我想通过
获取单元格时
collectionView?.cellForItem(at: indexPath)
return零。
查看绿色单元格。
func animateExpand(cells: [UICollectionViewCell], coordinateY: [CGFloat]) {
UIView.animate(
withDuration: 0.5,
delay: 0,
options: .curveEaseOut,
animations: {
for (index, cell) in cells.enumerated() {
cell.frame.origin.y = coordinateY[index]
}
},
completion: { [weak self] (_: Bool) in
self?.invalidateLayout()
self?.isMoving = false
}
)
}
只有 return 4 个单元格,因为 1 个单元格为零。
我怎样才能实现这个动画?谢谢
您遇到的几个问题。
首先,您使用自定义集合视图布局来定位单元格,然后您还显式设置了单元格 y-origins。
其次,集合视图只会呈现可见单元格,因此当您尝试“un-expand”布局时,底部单元格不存在。
我将建议一种稍微不同的方法。
使用 protocol/delegate 模式,这样您的自定义 UICollectionViewLayout
可以告诉 控制器 在出现捏合手势时展开/折叠布局。然后控制器将创建自定义布局的新实例并调用 .setCollectionViewLayout(...)
- 包裹在动画块中 - 展开或折叠。
此外,控制器将临时扩展集合视图的高度,以便呈现“off-screen”个单元格。
这是一些示例代码 - 我对您现有的自定义布局几乎没有做任何更改。我包含的评论应该足以让事情变得清楚。
不过请注意,这只是 示例代码 -- 它尚未经过全面测试,旨在作为一个开始点数:
protocol PinchProtocol: AnyObject {
func toggleExpanded(_ expand: Bool)
}
class MyWalletVC: UIViewController, PinchProtocol {
var data: [UIColor] = [
.red, .green, .blue, .cyan, .magenta,
//.yellow, .orange, .systemYellow,
]
var collectionView: UICollectionView!
var cvBottom: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
let lay = WalletStackLayout()
lay.isExpanded = false
lay.pinchDelegate = self
collectionView = UICollectionView(frame: .zero, collectionViewLayout: lay)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
cvBottom = collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
cvBottom,
])
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "c")
collectionView.dataSource = self
collectionView.delegate = self
}
func toggleExpanded(_ expand: Bool) {
// increase collection view height
// so "off-screen" cells will be rendered
// I just picked a value of 800 to make sure it's enough
self.cvBottom.constant = 800
UIView.animate(
withDuration: 0.5,
delay: 0,
options: .curveEaseOut,
animations: { [weak self] in
guard let self = self else { return }
// create a NEW layout object
let lay = WalletStackLayout()
// set its isExpanded property
lay.isExpanded = expand
// set self as its pinchDelegate
lay.pinchDelegate = self
// set the new layout
// use "animated: false" because we're animating it with UIView.animate
self.collectionView.setCollectionViewLayout(lay, animated: false)
},
completion: { [weak self] (_: Bool) in
guard let self = self else { return }
// reset collection view height
self.cvBottom.constant = 0
}
)
}
}
extension MyWalletVC: UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath)
c.contentView.backgroundColor = data[indexPath.item]
c.contentView.layer.cornerRadius = 16
return c
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected item from Tap gesture:", indexPath)
}
}
typealias CellAndLayoutAttributes = (cell: UICollectionViewCell, layoutAttributes: UICollectionViewLayoutAttributes)
class WalletStackLayout: UICollectionViewLayout {
// so we can tell the controller we got pinched
weak var pinchDelegate: PinchProtocol?
// expanded / collapsed layout
var isExpanded: Bool = false
private let heightRatio: CGFloat = 196/343
private let sidePadding: CGFloat = 16.0
private let peekStack: CGFloat = 40
private var cellWidth: CGFloat {
return UIScreen.main.bounds.width - sidePadding * 2.0
//return Device.screenWidth - sidePadding*2
}
private var cellHeight: CGFloat {
return heightRatio * cellWidth
}
private var isMoving: Bool = false
private var collectionLayoutAttributes: [UICollectionViewLayoutAttributes] = []
private var tapGestureRecognizer: UITapGestureRecognizer?
private var pinchGestureRecognizer: UIPinchGestureRecognizer?
// this is needed to keep the Top cell at the Top of the collection view
// when changing the layout
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return .zero
}
override var collectionViewContentSize: CGSize {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return CGSize(width: 0, height: 0)
}
var contentHeight: CGFloat = 0
for index in 0..<collectionView.numberOfSections {
contentHeight += calculateSectionCardHeight(section: index)
}
return CGSize(
width: collectionView.bounds.width,
height: contentHeight
)
}
override func prepare() {
super.prepare()
collectionLayoutAttributes.removeAll()
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return
}
initializeCardCollectionViewLayout()
collectionLayoutAttributes = makeCardsLayoutAttributes()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return nil
}
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
var r = rect
r.size.height += 500
for attributes in collectionLayoutAttributes where attributes.frame.intersects(r) {
visibleLayoutAttributes.append(attributes)
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return collectionLayoutAttributes[indexPath.row]
}
private func getCell(at indexPath: IndexPath) -> UICollectionViewCell? {
return collectionView?.cellForItem(at: indexPath)
}
private func getLayoutAttributes(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return collectionView?.layoutAttributesForItem(at: indexPath)
}
private func getCellAndLayoutAttributes(at indexPath: IndexPath) -> CellAndLayoutAttributes? {
guard let cell = getCell(at: indexPath),
let layoutAttributes = getLayoutAttributes(at: indexPath) else {
return nil
}
return (cell: cell, layoutAttributes: layoutAttributes)
}
// MARK: - BEGIN SET CARDS -
private func makeCardsLayoutAttributes() -> [UICollectionViewLayoutAttributes] {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return []
}
var collectionViewLayout: [UICollectionViewLayoutAttributes] = []
for section in 0..<collectionView.numberOfSections {
for row in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(row: row, section: section)
collectionViewLayout.append(makeCardLayoutAttributes(forCellWith: indexPath))
}
}
return collectionViewLayout
}
private func makeInitialLayoutAttributes(forCellWith indexPath: IndexPath, height: CGFloat) -> UICollectionViewLayoutAttributes {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let preferredSize = CGSize(width: cellWidth, height: height)
attributes.size = preferredSize
return attributes
}
private func makeCardLayoutAttributes(forCellWith indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let attributes = makeInitialLayoutAttributes(forCellWith: indexPath, height: cellHeight)
let coordinateY = calculateSectionYCoordinate(indexPath: indexPath)
attributes.frame.origin.y = coordinateY
attributes.frame.origin.x = sidePadding
attributes.zIndex = indexPath.item
return attributes
}
private func calculateSectionYCoordinate(indexPath: IndexPath) -> CGFloat {
var sectionYCoordinate: CGFloat = 0
for section in 0..<indexPath.section {
sectionYCoordinate += calculateSectionCardHeight(section: section)
}
if isExpanded {
return (cellHeight + sidePadding) * CGFloat(indexPath.row) + sectionYCoordinate
} else {
return peekStack * CGFloat(indexPath.row) + sectionYCoordinate
}
}
private func calculateSectionCardHeight(section: Int) -> CGFloat {
guard let numberOfItems = collectionView?.numberOfItems(inSection: section) else {
return 0
}
if isExpanded {
let totalExpandedCards: Int = numberOfItems
return (cellHeight + sidePadding) * CGFloat(totalExpandedCards)
} else {
let visibleCardCount: Int = 1
let totalStackedCards: Int = numberOfItems > 1 ? numberOfItems - visibleCardCount : 0
return peekStack * CGFloat(totalStackedCards) + cellHeight + sidePadding
}
}
// MARK: - TAP GESTURE -
private func initializeCardCollectionViewLayout() {
if tapGestureRecognizer == nil {
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGestureHandler))
if let tapGesture = tapGestureRecognizer {
collectionView?.addGestureRecognizer(tapGesture)
}
}
if pinchGestureRecognizer == nil {
pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinchGesture(_:)))
pinchGestureRecognizer?.delegate = self
if let pinchGesture = pinchGestureRecognizer {
collectionView?.addGestureRecognizer(pinchGesture)
}
}
}
}
extension WalletStackLayout: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc private func handlePinchGesture(_ pinchGesture: UIPinchGestureRecognizer) {
if pinchGesture.state == .began || pinchGesture.state == .changed {
guard let collectionView = collectionView,
let tapLocation = pinchGestureRecognizer?.location(in: collectionView),
let indexPath = collectionView.indexPathForItem(at: tapLocation),
!isMoving else {
return
}
if pinchGesture.scale > 1 {
// tell the controller to switch to Expanded layout
pinchDelegate?.toggleExpanded(true)
} else if pinchGesture.scale < 1 {
// tell the controller to switch to Collapsed layout
pinchDelegate?.toggleExpanded(false)
}
}
}
@objc
private func tapGestureHandler() {
guard let collectionView = collectionView,
let tapLocation = tapGestureRecognizer?.location(in: collectionView),
let indexPath = collectionView.indexPathForItem(at: tapLocation) else {
return
}
print("TapGestureHandler Section: \(indexPath.section) Row: \(indexPath.row)")
collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
}
}
不需要 @IBOutlet
或 @IBAction
连接。只需将标准视图控制器的自定义 class 分配给 MyWalletVC
,它应该 运行 没有问题。
我对在 collectionView 中管理动画不可见单元感到困惑。
所以我有堆叠卡片集合视图,当用户捏出集合单元格时,x 坐标会发生变化。更改坐标后,一些单元格在屏幕外。当我想通过
获取单元格时collectionView?.cellForItem(at: indexPath)
return零。
查看绿色单元格。
func animateExpand(cells: [UICollectionViewCell], coordinateY: [CGFloat]) {
UIView.animate(
withDuration: 0.5,
delay: 0,
options: .curveEaseOut,
animations: {
for (index, cell) in cells.enumerated() {
cell.frame.origin.y = coordinateY[index]
}
},
completion: { [weak self] (_: Bool) in
self?.invalidateLayout()
self?.isMoving = false
}
)
}
只有 return 4 个单元格,因为 1 个单元格为零。 我怎样才能实现这个动画?谢谢
您遇到的几个问题。
首先,您使用自定义集合视图布局来定位单元格,然后您还显式设置了单元格 y-origins。
其次,集合视图只会呈现可见单元格,因此当您尝试“un-expand”布局时,底部单元格不存在。
我将建议一种稍微不同的方法。
使用 protocol/delegate 模式,这样您的自定义 UICollectionViewLayout
可以告诉 控制器 在出现捏合手势时展开/折叠布局。然后控制器将创建自定义布局的新实例并调用 .setCollectionViewLayout(...)
- 包裹在动画块中 - 展开或折叠。
此外,控制器将临时扩展集合视图的高度,以便呈现“off-screen”个单元格。
这是一些示例代码 - 我对您现有的自定义布局几乎没有做任何更改。我包含的评论应该足以让事情变得清楚。
不过请注意,这只是 示例代码 -- 它尚未经过全面测试,旨在作为一个开始点数:
protocol PinchProtocol: AnyObject {
func toggleExpanded(_ expand: Bool)
}
class MyWalletVC: UIViewController, PinchProtocol {
var data: [UIColor] = [
.red, .green, .blue, .cyan, .magenta,
//.yellow, .orange, .systemYellow,
]
var collectionView: UICollectionView!
var cvBottom: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
let lay = WalletStackLayout()
lay.isExpanded = false
lay.pinchDelegate = self
collectionView = UICollectionView(frame: .zero, collectionViewLayout: lay)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
cvBottom = collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
cvBottom,
])
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "c")
collectionView.dataSource = self
collectionView.delegate = self
}
func toggleExpanded(_ expand: Bool) {
// increase collection view height
// so "off-screen" cells will be rendered
// I just picked a value of 800 to make sure it's enough
self.cvBottom.constant = 800
UIView.animate(
withDuration: 0.5,
delay: 0,
options: .curveEaseOut,
animations: { [weak self] in
guard let self = self else { return }
// create a NEW layout object
let lay = WalletStackLayout()
// set its isExpanded property
lay.isExpanded = expand
// set self as its pinchDelegate
lay.pinchDelegate = self
// set the new layout
// use "animated: false" because we're animating it with UIView.animate
self.collectionView.setCollectionViewLayout(lay, animated: false)
},
completion: { [weak self] (_: Bool) in
guard let self = self else { return }
// reset collection view height
self.cvBottom.constant = 0
}
)
}
}
extension MyWalletVC: UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath)
c.contentView.backgroundColor = data[indexPath.item]
c.contentView.layer.cornerRadius = 16
return c
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected item from Tap gesture:", indexPath)
}
}
typealias CellAndLayoutAttributes = (cell: UICollectionViewCell, layoutAttributes: UICollectionViewLayoutAttributes)
class WalletStackLayout: UICollectionViewLayout {
// so we can tell the controller we got pinched
weak var pinchDelegate: PinchProtocol?
// expanded / collapsed layout
var isExpanded: Bool = false
private let heightRatio: CGFloat = 196/343
private let sidePadding: CGFloat = 16.0
private let peekStack: CGFloat = 40
private var cellWidth: CGFloat {
return UIScreen.main.bounds.width - sidePadding * 2.0
//return Device.screenWidth - sidePadding*2
}
private var cellHeight: CGFloat {
return heightRatio * cellWidth
}
private var isMoving: Bool = false
private var collectionLayoutAttributes: [UICollectionViewLayoutAttributes] = []
private var tapGestureRecognizer: UITapGestureRecognizer?
private var pinchGestureRecognizer: UIPinchGestureRecognizer?
// this is needed to keep the Top cell at the Top of the collection view
// when changing the layout
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return .zero
}
override var collectionViewContentSize: CGSize {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return CGSize(width: 0, height: 0)
}
var contentHeight: CGFloat = 0
for index in 0..<collectionView.numberOfSections {
contentHeight += calculateSectionCardHeight(section: index)
}
return CGSize(
width: collectionView.bounds.width,
height: contentHeight
)
}
override func prepare() {
super.prepare()
collectionLayoutAttributes.removeAll()
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return
}
initializeCardCollectionViewLayout()
collectionLayoutAttributes = makeCardsLayoutAttributes()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return nil
}
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
var r = rect
r.size.height += 500
for attributes in collectionLayoutAttributes where attributes.frame.intersects(r) {
visibleLayoutAttributes.append(attributes)
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return collectionLayoutAttributes[indexPath.row]
}
private func getCell(at indexPath: IndexPath) -> UICollectionViewCell? {
return collectionView?.cellForItem(at: indexPath)
}
private func getLayoutAttributes(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return collectionView?.layoutAttributesForItem(at: indexPath)
}
private func getCellAndLayoutAttributes(at indexPath: IndexPath) -> CellAndLayoutAttributes? {
guard let cell = getCell(at: indexPath),
let layoutAttributes = getLayoutAttributes(at: indexPath) else {
return nil
}
return (cell: cell, layoutAttributes: layoutAttributes)
}
// MARK: - BEGIN SET CARDS -
private func makeCardsLayoutAttributes() -> [UICollectionViewLayoutAttributes] {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return []
}
var collectionViewLayout: [UICollectionViewLayoutAttributes] = []
for section in 0..<collectionView.numberOfSections {
for row in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(row: row, section: section)
collectionViewLayout.append(makeCardLayoutAttributes(forCellWith: indexPath))
}
}
return collectionViewLayout
}
private func makeInitialLayoutAttributes(forCellWith indexPath: IndexPath, height: CGFloat) -> UICollectionViewLayoutAttributes {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let preferredSize = CGSize(width: cellWidth, height: height)
attributes.size = preferredSize
return attributes
}
private func makeCardLayoutAttributes(forCellWith indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let attributes = makeInitialLayoutAttributes(forCellWith: indexPath, height: cellHeight)
let coordinateY = calculateSectionYCoordinate(indexPath: indexPath)
attributes.frame.origin.y = coordinateY
attributes.frame.origin.x = sidePadding
attributes.zIndex = indexPath.item
return attributes
}
private func calculateSectionYCoordinate(indexPath: IndexPath) -> CGFloat {
var sectionYCoordinate: CGFloat = 0
for section in 0..<indexPath.section {
sectionYCoordinate += calculateSectionCardHeight(section: section)
}
if isExpanded {
return (cellHeight + sidePadding) * CGFloat(indexPath.row) + sectionYCoordinate
} else {
return peekStack * CGFloat(indexPath.row) + sectionYCoordinate
}
}
private func calculateSectionCardHeight(section: Int) -> CGFloat {
guard let numberOfItems = collectionView?.numberOfItems(inSection: section) else {
return 0
}
if isExpanded {
let totalExpandedCards: Int = numberOfItems
return (cellHeight + sidePadding) * CGFloat(totalExpandedCards)
} else {
let visibleCardCount: Int = 1
let totalStackedCards: Int = numberOfItems > 1 ? numberOfItems - visibleCardCount : 0
return peekStack * CGFloat(totalStackedCards) + cellHeight + sidePadding
}
}
// MARK: - TAP GESTURE -
private func initializeCardCollectionViewLayout() {
if tapGestureRecognizer == nil {
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGestureHandler))
if let tapGesture = tapGestureRecognizer {
collectionView?.addGestureRecognizer(tapGesture)
}
}
if pinchGestureRecognizer == nil {
pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinchGesture(_:)))
pinchGestureRecognizer?.delegate = self
if let pinchGesture = pinchGestureRecognizer {
collectionView?.addGestureRecognizer(pinchGesture)
}
}
}
}
extension WalletStackLayout: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc private func handlePinchGesture(_ pinchGesture: UIPinchGestureRecognizer) {
if pinchGesture.state == .began || pinchGesture.state == .changed {
guard let collectionView = collectionView,
let tapLocation = pinchGestureRecognizer?.location(in: collectionView),
let indexPath = collectionView.indexPathForItem(at: tapLocation),
!isMoving else {
return
}
if pinchGesture.scale > 1 {
// tell the controller to switch to Expanded layout
pinchDelegate?.toggleExpanded(true)
} else if pinchGesture.scale < 1 {
// tell the controller to switch to Collapsed layout
pinchDelegate?.toggleExpanded(false)
}
}
}
@objc
private func tapGestureHandler() {
guard let collectionView = collectionView,
let tapLocation = tapGestureRecognizer?.location(in: collectionView),
let indexPath = collectionView.indexPathForItem(at: tapLocation) else {
return
}
print("TapGestureHandler Section: \(indexPath.section) Row: \(indexPath.row)")
collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
}
}
不需要 @IBOutlet
或 @IBAction
连接。只需将标准视图控制器的自定义 class 分配给 MyWalletVC
,它应该 运行 没有问题。