根据可见度百分比调整单元格大小

huangapple go评论73阅读模式
英文:

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.81.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)
		}
	}

huangapple
  • 本文由 发表于 2023年7月18日 16:52:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/76711039.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定