如何使iOS collectionView中的特定单元格在collectionView滚动时淡出?

6

我希望在我的UICollectionView中,所有右侧的单元格在滚动时都会淡出,类似于苹果的消息应用程序,但不影响collectionView中其他单元格的颜色或透明度。是否有一种方法可以根据其滚动位置调整UICollectionViewCell的透明度以实现该效果?


1
尝试搜索渐变效果,这可能会有所帮助:https://dev59.com/A37aa4cB1Zd3GeqPmSWR - Saheb Singh
我喜欢使用渐变遮罩的想法,但我认为这将影响scrollView的所有内容,而不仅仅是右侧的单元格。 - alionthego
你是想让渐变从上到下还是从下到上,还是两者都要? - agibson007
3个回答

7

你可以对集合视图做很多有趣的事情。我喜欢子类化UICollectionViewFlowLayout。这里有一个例子,根据距离中心的远近淡化集合视图的顶部和底部。我可以修改它,只淡化非常边缘的部分,但是在查看代码后你应该能够理解。

import UIKit

class FadingLayout: UICollectionViewFlowLayout,UICollectionViewDelegateFlowLayout {

    //should be 0<fade<1
    private let fadeFactor: CGFloat = 0.5
    private let cellHeight : CGFloat = 60.0

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    init(scrollDirection:UICollectionViewScrollDirection) {
        super.init()
        self.scrollDirection = scrollDirection

    }

    override func prepare() {
        setupLayout()
        super.prepare()
    }

    func setupLayout() {
        self.itemSize = CGSize(width: self.collectionView!.bounds.size.width,height:cellHeight)
        self.minimumLineSpacing = 0
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

    func scrollDirectionOver() -> UICollectionViewScrollDirection {
        return UICollectionViewScrollDirection.vertical
    }
    //this will fade both top and bottom but can be adjusted
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributesSuper: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElements(in: rect) as [UICollectionViewLayoutAttributes]!
        if let attributes = NSArray(array: attributesSuper, copyItems: true) as? [UICollectionViewLayoutAttributes]{
            var visibleRect = CGRect()
            visibleRect.origin = collectionView!.contentOffset
            visibleRect.size = collectionView!.bounds.size
            for attrs in attributes {
                if attrs.frame.intersects(rect) {
                    let distance = visibleRect.midY - attrs.center.y
                    let normalizedDistance = abs(distance) / (visibleRect.height * fadeFactor)
                    let fade = 1 - normalizedDistance
                    attrs.alpha = fade
                }
            }
            return attributes
        }else{
            return nil
        }
    }
    //appear and disappear at 0
    override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItem(at: itemIndexPath)! as UICollectionViewLayoutAttributes
        attributes.alpha = 0
        return attributes
    }

    override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItem(at: itemIndexPath)! as UICollectionViewLayoutAttributes
        attributes.alpha = 0
        return attributes
    }
}

在你的控制器中设置集合视图,代码应该如下:

let layout = FadingLayout(scrollDirection: .vertical)
collectionView.delegate = self
collectionView.dataSource = self
self.collectionView.setCollectionViewLayout(layout, animated: false)

如果我更了解使用场景,我可以告诉您如何修改它。

用户开始滚动时是否可以进行淡出效果?此外,当用户滚动到scrollView的末尾时,底部单元格是否可以没有淡出效果? - Adrian Macarenco

4
如果您子类化UICollectionViewFlowLayout,这将非常简单。首先,您需要确保在bounds更改/滚动发生时重新计算可见属性,方法是在shouldInvalidateLayout(forBoundsChange newBounds: CGRect)中返回true。然后,在layoutAttributesForElements(in rect: CGRect)委托调用中,获取由超类计算的属性并根据可见边界内项目的偏移修改alpha值,就是这样。区分左/右侧项目可以使用控制器中具有任何逻辑的方法处理,并通知布局类避免对左侧项目应用此效果。(我使用CustomLayoutDelegate来实现,它在控制器中实现,只需标识indexPath.row为奇数的项作为左侧单元格即可) 下面是一个演示,它会跳过奇数行的项目,并对偶数indexPath.row上的项目应用此效果。
import UIKit

class ViewController: UIViewController {

    /// Custom flow layout
    lazy var layout: CustomFlowLayout = {
        let l: CustomFlowLayout = CustomFlowLayout()
        l.itemSize = CGSize(width: self.view.bounds.width / 1.5, height: 100)
        l.delegate = self

        return l
    }()

    /// The collectionView if you're not using UICollectionViewController
    lazy var collectionView: UICollectionView = {
        let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.layout)
        cv.backgroundColor = UIColor.lightGray

        cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
        cv.dataSource = self

        return cv
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(collectionView)
    }

}

extension ViewController: UICollectionViewDataSource, CustomLayoutDelegate {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 30
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        cell.backgroundColor = UIColor.black

        return cell
    }

    // MARK: CustomLayoutDelegate

    func cellSide(forIndexPath indexPath: IndexPath) -> CellSide {

        // TODO: Your implementation to distinguish left/right indexPath

        // Even rows are .right and Odds .left
        if indexPath.row % 2 == 0 {
            return .right
        } else {
            return .left
        }
    }
}

public enum CellSide {
    case right
    case left
}

protocol CustomLayoutDelegate: class {

    func cellSide(forIndexPath indexPath: IndexPath) -> CellSide
}

class CustomFlowLayout: UICollectionViewFlowLayout {

    /// Delegates distinguishing between left and right items
    weak var delegate: CustomLayoutDelegate!

    /// Maximum alpha value
    let kMaxAlpha: CGFloat = 1

    /// Minimum alpha value. The alpha value you want the first visible item to have
    let kMinAlpha: CGFloat = 0.3

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let cv = collectionView, let rectAtts = super.layoutAttributesForElements(in: rect) else { return nil }

        for atts in rectAtts {

            // Skip left sides
            if delegate.cellSide(forIndexPath: atts.indexPath) == .left {
                continue
            }

            // Offset Y on visible bounds. you can use
            //      ´cv.bounds.height - (atts.frame.origin.y - cv.contentOffset.y)´
            // To reverse the effect
            let offset_y = (atts.frame.origin.y - cv.contentOffset.y)

            let alpha = offset_y * kMaxAlpha / cv.bounds.height

            atts.alpha = alpha + kMinAlpha
        }

        return rectAtts
    }

    // Invalidate layout when scroll happens. Otherwise atts won't be recalculated
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

}

谢谢!我缺少的关键部分是在 shouldInvalidateLayout 中返回 true。 - Ric Santos

1
当然!请注意,UICollectionView是UIScrollView的子类,而您的UICollectionViewController已经是集合视图的委托。这意味着它也符合UIScrollViewDelegate协议,其中包括一堆方法来通知您有关滚动位置更改的信息。
对我来说最值得注意的是scrollViewDidScroll(_ :),当集合视图中的contentOffset更改时将调用该方法。您可以实现该方法来迭代集合视图的visibleCells,或者自己调整单元格的alpha,或向单元格发送某些消息,以通知它根据其框架和偏移量调整自己的alpha。
我能想到的最简单的实现是遵循您的仅限右侧的要求,如下所示。请注意,由于单元格的alpha仅在滚动时而不是在初始出队或重用时进行调整,因此在视图顶部或底部可能会出现一些故障。
class FadingCollectionViewController: UICollectionViewController {

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 500
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        return cell
    }

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard let collectionView = collectionView else {
            return
        }

        let offset = collectionView.contentOffset.y
        let height = collectionView.frame.size.height
        let width = collectionView.frame.size.width
        for cell in collectionView.visibleCells {
            let left = cell.frame.origin.x
            if left >= width / 2 {
                let top = cell.frame.origin.y
                let alpha = (top - offset) / height
                cell.alpha = alpha
            } else {
                cell.alpha = 1
            }
        }
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接