英文:
Scale cell according to visibility percentage
问题
当一个单元格完全可见在集合视图的中心时,我希望它比侧边的单元格更大。
目前,我正在使用 visibleItemsInvalidationHandler
闭包来实现这个效果:
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let ratio = (inter.width * inter.height) / (frame.width * frame.height)
let scale = ratio > 0.8 ? ratio : 0.8
UIView.animate(withDuration: 0.2) {
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}
}
然而,我仍然没有得到我想要的结果。这是我得到的结果:
当滑动时,单元格开始闪烁,当我尝试将单元格移到中心时,从 max(ratio, scale)
的结果值小于之前的值。我希望 max(ratio, scale)
的值随着单元格在屏幕上的可见性增加而增加。
理想情况下,我想要的效果如下:
最小可重现示例(MRE):
ViewController、枚举和结构体:
struct BannerEntity: Hashable {
private let id = UUID()
let bannerTitle: String
init(bannerTitle: String) {
self.bannerTitle = bannerTitle
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: BannerEntity, rhs: BannerEntity) -> Bool {
return lhs.id == rhs.id
}
}
struct HomePresentationModel {
var section: HomeSection
var items: [HomeSectionItem]
}
enum HomeSectionItem: Hashable {
case banner(BannerEntity)
}
struct HomeSection: Hashable {
var header: HomeSectionHeader
let sectionType: HomeSectionType
}
enum HomeSectionType: Int, Hashable {
case banner
}
enum HomeSectionHeader: Int, Hashable {
case tappableHeader
case empty
}
class ViewController: UIViewController {
// ... (省略了一些代码)
}
BannerCell:
import UIKit
class BannerCell: UICollectionViewCell {
// MARK: Init
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Helpers
private func setupSubviews(){
backgroundColor = .purple
layer.cornerRadius = 10
}
}
以上为代码的翻译部分。
英文:
When a cell is fully visible at the center of the collection view, I want it to appear larger than the cells in the sides.
Currently, I'm utilizing visibleItemsInvalidationHandler
closure to achieve that effect:
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let ratio = (inter.width * inter.height) / (frame.width * frame.height)
let scale = ratio > 0.8 ? ratio : 0.8
UIView.animate(withDuration: 0.2) {
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}
}
However, I still don't get the result I'm aiming for. This is what I get:
The cells start to flicker when swiped, and when I try to get a cell to the center, the result value from max(ratio, scale)
is less than what it was before. I want the value of max(ratio, scale)
to increase, the more the cell is visible on the screen.
Ideally, this is what I'm aiming for:
MRE:
ViewController, enums and structs:
struct BannerEntity: Hashable {
private let id = UUID()
let bannerTitle: String
init(bannerTitle: String) {
self.bannerTitle = bannerTitle
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: BannerEntity,
rhs: BannerEntity) -> Bool {
return lhs.id == rhs.id
}
}
struct HomePresentationModel {
var section: HomeSection
var items: [HomeSectionItem]
}
enum HomeSectionItem: Hashable {
case banner(BannerEntity)
}
struct HomeSection: Hashable {
var header: HomeSectionHeader
let sectionType: HomeSectionType
}
enum HomeSectionType: Int, Hashable {
case banner
}
enum HomeSectionHeader: Int, Hashable {
case tappableHeader
case empty
}
class ViewController: UIViewController {
// MARK: Subviews
private var collectionView: UICollectionView!
// MARK: Properties
private lazy var dataSource = makeDataSource()
private var sections = [HomePresentationModel(section: .init(header: .empty, sectionType: .banner),
items: [.banner(.init(bannerTitle: "firstBanner")),
.banner(.init(bannerTitle: "secondBanner")),
.banner(.init(bannerTitle: "thirdBanner")),
.banner(.init(bannerTitle: "fourthBanner"))]
)]
// MARK: Value Type
typealias DataSource = UICollectionViewDiffableDataSource<
HomeSection,
HomeSectionItem
>
typealias Snapshot = NSDiffableDataSourceSnapshot<
HomeSection,
HomeSectionItem
>
// MARK: Viewcycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
configureCollectionView()
applySnapshot()
}
// MARK: Helpers
private func configureCollectionView(){
collectionView = UICollectionView(frame: view.frame,
collectionViewLayout: generateLayout())
view.addSubview(collectionView)
collectionView.showsHorizontalScrollIndicator = false
collectionView.register(BannerCell.self,
forCellWithReuseIdentifier: "BannerCell")
}
private func generateLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout {
[self] (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment)
-> NSCollectionLayoutSection? in
let sectionType = HomeSectionType(rawValue: sectionIndex)
guard sectionType == .banner else {return nil}
return self.generateBannersLayout()
}
return layout
}
func generateBannersLayout() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9),
heightDimension: .fractionalHeight(0.28))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
group.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 0,
bottom: 0,
trailing: 0)
group.interItemSpacing = NSCollectionLayoutSpacing.fixed(0)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 10,
leading: 0,
bottom: 10,
trailing: 0)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let ratio = (inter.width * inter.height) / (frame.width * frame.height)
let scale = ratio > 0.8 ? ratio : 0.8
UIView.animate(withDuration: 0.2) {
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}
}
return section
}
private func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { (collectionView, indexPath, item) ->
UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "BannerCell",
for: indexPath) as? BannerCell
return cell
})
return dataSource
}
private func applySnapshot() {
var snapshot = Snapshot()
snapshot.appendSections(sections.map({$0.section}))
sections.forEach { section in
snapshot.appendItems(section.items, toSection: section.section)
}
dataSource.apply(snapshot, animatingDifferences: false)
}
}
BannerCell:
import UIKit
class BannerCell: UICollectionViewCell {
// MARK: Init
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Helpers
private func setupSubviews(){
backgroundColor = .purple
layer.cornerRadius = 10
}
}
答案1
得分: 1
你已经接近了...
首先,不需要UIView.animate
块。.visibleItemsInvalidationHandler
在每次布局周期中都会被调用,因此在我们滚动该部分时会连续触发它。
其次,你的高度比例计算略有不对...
你获取了单元格框架与视图框架的交集(CGRect
),所以我们希望高度比例与交集矩形的宽度与框架宽度的百分比匹配。在这种情况下,百分比在0.8
和1.0
之间变化。
如果你将.visibleItemsInvalidationHandler
更改为以下内容,它应该是你想要的,或者至少足够接近,以便你可以根据自己的需求进行微调:
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let percent: CGFloat = inter.width / frame.width
let scale = 0.8 + (0.2 * percent)
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}
英文:
You're close...
First, no need for the UIView.animate
block. .visibleItemsInvalidationHandler
is called for each layout cycle, so it's triggered (effectively) continuously as we scroll the section.
Second, your height scale calculation is not-quite-right...
You're getting the intersection (CGRect
) of the cell frame with the view frame, so we want the height scale to match the percentage of the width of the intersecting rect. Well, in this case, the percentage of the range between 0.8
and 1.0
.
If you change your .visibleItemsInvalidationHandler
to this, it should be what you're going for -- or at least close enough that you can tweak it to your satisfaction:
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let percent: CGFloat = inter.width / frame.width
let scale = 0.8 + (0.2 * percent)
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论