什么是卡片式布局?
卡片式布局(Card-based Layout) 是一种以“卡片”为单元的 UI 布局方式,每张卡片作为一个独立的内容模块,包含图片、文字、按钮等元素。卡片通常具有圆角、阴影和立体感,通过重叠或堆叠效果营造视觉层次,常用于滑动、拖拽等交互场景。类似于现实中的纸牌,卡片式布局在移动应用中广泛应用,因其美观、模块化和交互性强而备受欢迎。
特点:
-
模块化:每张卡片独立,便于动态管理。
-
视觉层次:通过重叠、堆叠、阴影等效果增强深度感。
-
动态交互:支持滑动、点击、拖拽,结合动画提升体验。
-
响应式:适配不同屏幕尺寸,保持一致性。
典型应用:
-
社交媒体(如 Tinder 的用户卡片滑动)。
-
新闻应用(如 Apple News 的头条卡片)。
-
任务管理(如 Trello 的任务卡片)。
-
电商(如商品推荐卡片)。
iOS 中的卡片式布局实现方式
在 iOS 开发中,卡片式布局通常通过 UICollectionView 实现,结合 transform 动画(如缩放、位移、旋转)打造重叠或堆叠效果。以下是几种常见实现方式:
1. UICollectionView + 自定义 Card Layout
-
描述:通过继承 UICollectionViewLayout 或 UICollectionViewFlowLayout,自定义卡片的位置、缩放和重叠效果。核心是动态调整每张卡片的 transform 和 zIndex。
-
特点:
-
卡片以堆叠或重叠形式排列,靠前的卡片更大且位于顶层。
-
支持滑动交互,滑动时卡片动态调整位置和缩放。
-
可限制显示的卡片数量,隐藏靠后的卡片。
-
-
实现步骤:
-
创建自定义 UICollectionViewLayout,定义卡片尺寸和间距。
-
在 layoutAttributesForElements 中为每张卡片设置 transform(缩放、位移)和 zIndex。
-
通过 UIScrollViewDelegate 监听滑动,动态更新卡片动画。
-
-
代码示例(简化的 Card Layout):
class CardStackLayout: UICollectionViewFlowLayout { private let cardSpacing: CGFloat = 20 private let maxVisibleCards = 3 override func prepare() { super.prepare() scrollDirection = .vertical minimumLineSpacing = cardSpacing itemSize = CGSize(width: collectionView!.bounds.width - 40, height: 300) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil } for (index, attribute) in attributes.enumerated() { let offset = min(index, maxVisibleCards - 1) let scale = 1 - CGFloat(offset) * 0.05 let translation = CGFloat(offset) * cardSpacing attribute.zIndex = maxVisibleCards - offset attribute.transform = CGAffineTransform(scaleX: scale, y: scale) .translatedBy(x: 0, y: translation) if index >= maxVisibleCards { attribute.alpha = 0 } } return attributes } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } }
-
使用方式:
let layout = CardStackLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.dataSource = self collectionView.delegate = self collectionView.register(CardCell.self, forCellWithReuseIdentifier: "CardCell")
-
适用场景:需要堆叠效果的场景,如 Tinder 风格的卡片滑动、新闻头条展示。
2. UICollectionViewCompositional Layout (iOS 13+)
-
描述:使用 UICollectionViewCompositionalLayout 实现现代化的卡片布局,通过 NSCollectionLayoutGroup 配置卡片排列,结合 transform 动画实现重叠效果。
-
特点:
-
声明式布局,代码简洁。
-
支持动态尺寸,但需要配合 UICollectionViewDelegate 调整卡片样式。
-
适合现代 iOS 应用。
-
-
实现步骤:
-
定义 compositional layout,设置单列或多列 group。
-
使用 transform 或 zIndex 实现卡片重叠。
-
在 delegate 中动态调整卡片样式。
-
-
代码示例:
func createCardLayout() -> UICollectionViewCompositionalLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .absolute(300)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(320)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = 20 section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20) section.visibleItemsInvalidationHandler = { items, offset, environment in items.forEach { item in let index = item.indexPath.item let scale = 1 - CGFloat(index) * 0.05 let translation = CGFloat(index) * 20 item.transform = CGAffineTransform(scaleX: scale, y: scale) .translatedBy(x: 0, y: translation) item.zIndex = items.count - index } } return UICollectionViewCompositionalLayout(section: section) }
-
适用场景:现代应用,追求简洁代码和动态交互。
3. SwiftUI+ZStack
-
描述:在 SwiftUI 中,使用 ZStack 和 ForEach 实现卡片堆叠布局,通过 scaleEffect 和 offset 添加重叠效果。
-
特点:
-
声明式语法,开发效率高。
-
适合快速原型或跨平台开发。
-
对于复杂交互可能需要额外逻辑。
-
-
代码示例:
struct ContentView: View { let cards = ["Card 1", "Card 2", "Card 3", "Card 4"] var body: some View { ZStack { ForEach(cards.indices, id: \.self) { index in CardView(title: cards[index]) .scaleEffect(1 - CGFloat(index) * 0.05) .offset(y: CGFloat(index) * 20) .zIndex(Double(cards.count - index)) } } .padding() } } struct CardView: View { let title: String var body: some View { RoundedRectangle(cornerRadius: 10) .fill(Color.white) .frame(width: 300, height: 200) .shadow(radius: 8) .overlay(Text(title).font(.headline)) } }
-
适用场景:SwiftUI 项目,快速实现简单卡片堆叠。
4. 第三方库
-
描述:使用第三方库快速实现卡片式布局,减少开发时间。
-
推荐库:
-
Koloda:专为 Tinder 风格的卡片滑动设计,支持拖拽和动画。
-
iCarousel:支持多种 3D 卡片效果(如旋转、堆叠)。
-
-
使用方式:通过 CocoaPods 或 Swift Package Manager 引入,按文档配置。
-
注意:第三方库可能限制灵活性,需评估维护成本。
-
适用场景:快速原型开发或特定风格的卡片布局。
代码实现
以下是一个完整的实现示例,包含卡片式布局、堆叠效果和滑动动画。
1. 设置项目结构
创建一个新的 iOS 项目,选择 UIKit,使用 Storyboard 或纯代码均可。我们将使用纯代码实现以便更清晰地展示逻辑。
2. 创建卡片 Cell
首先定义卡片的 UICollectionViewCell,包含图片、标题和阴影效果。
import UIKit
class CardCell: UICollectionViewCell {
static let reuseIdentifier = "CardCell"
private let imageView = UIImageView()
private let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
// 卡片样式
contentView.layer.cornerRadius = 10
contentView.layer.masksToBounds = true
contentView.backgroundColor = .white
contentView.layer.shadowColor = UIColor.black.cgColor
contentView.layer.shadowOpacity = 0.3
contentView.layer.shadowOffset = CGSize(width: 0, height: 4)
contentView.layer.shadowRadius = 8
// 图片
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.6)
])
// 标题
titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
titleLabel.textAlignment = .center
contentView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8)
])
}
func configure(with title: String, image: UIImage?) {
titleLabel.text = title
imageView.image = image
}
}
3. 自定义卡片布局
创建一个自定义的 UICollectionViewFlowLayout,实现卡片的重叠和堆叠效果。
import UIKit
class CardStackLayout: UICollectionViewFlowLayout {
private let cardSpacing: CGFloat = 20 // 卡片间的垂直偏移
private let maxVisibleCards = 3 // 最多显示的卡片数
override func prepare() {
super.prepare()
scrollDirection = .vertical
minimumLineSpacing = cardSpacing
itemSize = CGSize(width: collectionView!.bounds.width - 40, height: 300) // 卡片尺寸
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
for (index, attribute) in attributes.enumerated() {
let offset = min(index, maxVisibleCards - 1) // 限制最多显示几张卡片
let scale = 1 - CGFloat(offset) * 0.05 // 靠后的卡片略微缩小
let translation = CGFloat(offset) * cardSpacing // 靠后的卡片向下偏移
attribute.zIndex = maxVisibleCards - offset // 前面的卡片 zIndex 更高
attribute.transform = CGAffineTransform(scaleX: scale, y: scale)
.translatedBy(x: 0, y: translation)
// 隐藏超出最大显示数量的卡片
if index >= maxVisibleCards {
attribute.alpha = 0
}
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true // 确保滑动时重新计算布局
}
}
4. 设置 ViewController
在 ViewController 中配置 UICollectionView,并实现滑动时的动画效果。
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
private var collectionView: UICollectionView!
private let cardTitles = ["Card 1", "Card 2", "Card 3", "Card 4", "Card 5"]
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
private func setupCollectionView() {
let layout = CardStackLayout()
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.backgroundColor = .systemGray6
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(CardCell.self, forCellWithReuseIdentifier: CardCell.reuseIdentifier)
view.addSubview(collectionView)
// Auto Layout
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cardTitles.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CardCell.reuseIdentifier, for: indexPath) as! CardCell
cell.configure(with: cardTitles[indexPath.item], image: UIImage(named: "placeholder"))
return cell
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// 动态调整卡片动画
guard let collectionView = scrollView as? UICollectionView else { return }
let visibleCells = collectionView.visibleCells
for case let cell in visibleCells {
let indexPath = collectionView.indexPath(for: cell)
let attributes = collectionView.layoutAttributesForItem(at: indexPath!)
// 根据滑动偏移调整 transform
let offsetY = scrollView.contentOffset.y
let translation = max(0, offsetY - (attributes?.frame.origin.y ?? 0))
let scale = max(0.8, 1 - translation / 1000) // 动态缩放
cell.transform = CGAffineTransform(scaleX: scale, y: scale)
.translatedBy(x: 0, y: translation / 2)
}
}
// MARK: - UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 点击卡片触发动画
let cell = collectionView.cellForItem(at: indexPath)
UIView.animate(withDuration: 0.3, animations: {
cell?.transform = .init(scaleX: 1.1, y: 1.1)
}, completion: { _ in
UIView.animate(withDuration: 0.3) {
cell?.transform = .identity
}
})
}
}
代码解析
-
卡片 Cell(CardCell):
-
使用 cornerRadius 和 shadow 创建卡片的外观。
-
imageView 和 titleLabel 分别展示图片和标题,布局使用 Auto Layout。
-
-
自定义布局(CardStackLayout):
-
重写 layoutAttributesForElements 方法,为每张卡片设置 zIndex、缩放和位移。
-
使用 scale 和 translation 实现堆叠效果,靠后的卡片略小且向下偏移。
-
设置 alpha 隐藏超出最大显示数量的卡片。
-
-
ViewController:
-
配置 UICollectionView,使用自定义的 CardStackLayout。
-
在 scrollViewDidScroll 中根据滑动偏移动态调整卡片的 transform,实现平滑动画。
-
添加点击交互,点击卡片时触发放大动画。
-
核心组件
-
UICollectionView:
-
用于管理卡片的展示和滚动。
-
自定义 UICollectionViewLayout 或使用 UICollectionViewFlowLayout 控制卡片位置。
-
-
Transform 动画:
-
使用 CGAffineTransform 实现卡片的缩放(scale)、位移(translate)和旋转(rotate)。
-
通过 UIView.animate 或 UIViewPropertyAnimator 添加平滑动画。
-
-
卡片 Cell:
-
自定义 UICollectionViewCell,包含卡片的 UI 元素(如图片、标题、阴影)。
-
-
交互逻辑:
-
通过 UIScrollViewDelegate 或 UICollectionViewDelegate 监听滑动,动态调整卡片的位置和 transform。
-
使用 UIGestureRecognizer 支持点击或拖拽交互。
-
布局思路
-
重叠效果:通过设置卡片的 zIndex 和 transform,让靠前的卡片覆盖靠后的卡片,同时施加缩放和位移。
-
堆叠效果:卡片按顺序排列,靠后的卡片略微偏移,模拟一叠纸牌。
-
动画触发:当用户滑动 UICollectionView 时,根据滑动偏移量动态调整卡片的 transform,实现平滑过渡。
适用场景
-
社交媒体:Tinder 的用户卡片滑动、Instagram 故事预览。
-
新闻应用:头条文章或推荐内容展示。
-
任务管理:Trello 风格的任务卡片。
-
电商:商品卡片快速浏览。
总结
卡片式布局通过 UICollectionView 和 transform 动画,能轻松实现重叠或堆叠效果,兼具美观和交互性。UICollectionViewLayout 提供最大灵活性,CompositionalLayout 适合现代应用,SwiftUI 则简化开发流程。根据项目需求选择合适的实现方式,能显著提升用户体验。