草庐IT

ios - 如何更改 UICollectionViewCell 初始布局位置?

coder 2023-09-05 原文

背景

因此,我正在开发自定义框架,并且为我的 UICollectionView 实现了自定义 UICollectionViewFlowLayout。 该实现允许您滚动浏览卡片堆,同时向左/向右滑动卡片(单元格)(Tinder + Shazam Discover 组合)。

我正在修改 UICollectionViewLayoutAttributes 以创建滚动卡片堆叠效果。

问题

在堆栈的末尾,当我刷掉一张卡片(单元格)时,新卡片不会从堆栈后面出现,而是从顶部出现。这只发生在堆栈的末尾,我不知道为什么。

我的想法-我尝试过的

我的猜测是我需要修改 initialLayoutAttributesForAppearingItem 中的一些东西,我试过了,但它似乎没有做任何事情。

我目前正在调用其中的 updateCellAttributes 函数来更新属性,但我也尝试过手动修改其中的属性。我真的没有看到这里的问题,除非有不同的方法来修改卡片在这种情况下的定位方式。

可能是因为单元格在技术上还不在“矩形”中(参见 layoutAttributesForElements(in rect: CGRect)),所以它们没有更新?

有什么我想念的吗? 有没有人更熟悉如何修改流程布局以实现我想要的行为?

例子和代码

这是它的动图:

这是我试图解决的错误的 gif:

如您所见,当滑开最后一张卡片时,新卡片会从顶部出现,而它应该出现在前一张卡片的后面。

您可以在下面找到自定义 UICollectionViewFlowLayout 代码。 最重要的函数是 updateCellAttributes 函数, 内联注释对此进行了详细记录(请参见下面的代码)。 此函数调用自: initialLayoutAttributesForAppearingItem
finalLayoutAttributesForDisappearingItem
layoutAttributesForItem
layoutAttributesForElements
修改布局信息并创建堆叠效果。

import UIKit

/// Custom `UICollectionViewFlowLayout` that provides the flowlayout information like paging and `CardCell` movements.
internal class VerticalCardSwiperFlowLayout: UICollectionViewFlowLayout {

    /// This property sets the amount of scaling for the first item.
    internal var firstItemTransform: CGFloat?
    /// This property enables paging per card. Default is true.
    internal var isPagingEnabled: Bool = true
    /// Stores the height of a CardCell.
    internal var cellHeight: CGFloat!
    /// Allows you to make the previous card visible or not visible (stack effect). Default is `true`.
    internal var isPreviousCardVisible: Bool = true

    internal override func prepare() {
        super.prepare()

        assert(collectionView?.numberOfSections == 1, "Number of sections should always be 1.")
        assert(collectionView?.isPagingEnabled == false, "Paging on the collectionview itself should never be enabled. To enable cell paging, use the isPagingEnabled property of the VerticalCardSwiperFlowLayout instead.")
    }

    internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let items = NSArray(array: super.layoutAttributesForElements(in: rect)!, copyItems: true)

        for object in items {
            if let attributes = object as? UICollectionViewLayoutAttributes {
                self.updateCellAttributes(attributes)
            }
        }
        return items as? [UICollectionViewLayoutAttributes]
    }

    internal override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {

        if self.collectionView?.numberOfItems(inSection: 0) == 0 { return nil }

        if let attr = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes {
            self.updateCellAttributes(attr)
            return attr
        }
        return nil
    }

    internal override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // attributes for swiping card away
        return self.layoutAttributesForItem(at: itemIndexPath)
    }

    internal override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // attributes for adding card
        return self.layoutAttributesForItem(at: itemIndexPath)
    }

    // We invalidate the layout when a "bounds change" happens, for example when we scale the top cell. This forces a layout update on the flowlayout.
    internal override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

    // Cell paging
    internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        // If the property `isPagingEnabled` is set to false, we don't enable paging and thus return the current contentoffset.
        guard let collectionView = self.collectionView, isPagingEnabled else {
            let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
            return latestOffset
        }

        // Page height used for estimating and calculating paging.
        let pageHeight = cellHeight + self.minimumLineSpacing

        // Make an estimation of the current page position.
        let approximatePage = collectionView.contentOffset.y/pageHeight

        // Determine the current page based on velocity.
        let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)

        // Create custom flickVelocity.
        let flickVelocity = velocity.y * 0.4

        // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
        let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

        // Calculate newVerticalOffset.
        let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top

        return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
    }

    /**
     Updates the attributes.
     Here manipulate the zIndex of the cells here, calculate the positions and do the animations.

     Below we'll briefly explain how the effect of scrolling a card to the background instead of the top is achieved.
     Keep in mind that (x,y) coords in views start from the top left (x: 0,y: 0) and increase as you go down/to the right,
     so as you go down, the y-value increases, and as you go right, the x value increases.

     The two most important variables we use to achieve this effect are cvMinY and cardMinY.
     * cvMinY (A): The top position of the collectionView + inset. On the drawings below it's marked as "A".
     This position never changes (the value of the variable does, but the position is always at the top where "A" is marked).
     * cardMinY (B): The top position of each card. On the drawings below it's marked as "B". As the user scrolls a card,
     this position changes with the card position (as it's the top of the card).
     When the card is moving down, this will go up, when the card is moving up, this will go down.

     We then take the max(cvMinY, cardMinY) to get the highest value of those two and set that as the origin.y of the card.
     By doing this, we ensure that the origin.y of a card never goes below cvMinY, thus preventing cards from scrolling upwards.


     +---------+   +---------+
     |         |   |         |
     | +-A=B-+ |   |  +-A-+  | ---> The top line here is the previous card
     | |     | |   | +--B--+ |      that's visible when the user starts scrolling.
     | |     | |   | |     | |
     | |     | |   | |     | |  |  As the card moves down,
     | |     | |   | |     | |  v  cardMinY ("B") goes up.
     | +-----+ |   | |     | |
     |         |   | +-----+ |
     | +--B--+ |   | +--B--+ |
     | |     | |   | |     | |
     +-+-----+-+   +-+-----+-+


     - parameter attributes: The attributes we're updating.
     */
    private func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {

        guard let collectionView = collectionView else { return }

        var cvMinY = collectionView.bounds.minY + collectionView.contentInset.top
        let cardMinY = attributes.frame.minY
        var origin = attributes.frame.origin
        let cardHeight = attributes.frame.height

        if cvMinY > cardMinY + cardHeight + minimumLineSpacing + collectionView.contentInset.top {
            cvMinY = 0
        }

        let finalY = max(cvMinY, cardMinY)

        let deltaY = (finalY - cardMinY) / cardHeight
        transformAttributes(attributes: attributes, deltaY: deltaY)

        // Set the attributes frame position to the values we calculated
        origin.x = collectionView.frame.width/2 - attributes.frame.width/2 - collectionView.contentInset.left
        origin.y = finalY
        attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
        attributes.zIndex = attributes.indexPath.row
    }

    // Creates and applies a CGAffineTransform to the attributes to recreate the effect of the card going to the background.
    private func transformAttributes(attributes: UICollectionViewLayoutAttributes, deltaY: CGFloat) {

        if let itemTransform = firstItemTransform {

            let scale = 1 - deltaY * itemTransform
            let translationScale = CGFloat((attributes.zIndex + 1) * 10)
            var t = CGAffineTransform.identity

            t = t.scaledBy(x: scale, y: 1)
            if isPreviousCardVisible {
                t = t.translatedBy(x: 0, y: (deltaY * translationScale))
            }
            attributes.transform = t
        }
    }
}

Full project zip (即时下载)

Github repo

Github issue

如果您有任何其他问题,我很乐意回答。 感谢您投入时间和精力,我们将不胜感激您的帮助!

最佳答案

似乎在删除最后一个单元格后,我们得到两个动画同时发生。内容偏移量(由于内容大小变化)随动画而变化,新的最后一个单元格转到其新位置。但新的可见单元格已经在其位置。很遗憾,但我没有看到解决此问题的快速方法。

关于ios - 如何更改 UICollectionViewCell 初始布局位置?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54598199/

有关ios - 如何更改 UICollectionViewCell 初始布局位置?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby-on-rails - Ruby on Rails 迁移,将表更改为 MyISAM - 2

    如何正确创建Rails迁移,以便将表更改为MySQL中的MyISAM?目前是InnoDB。运行原始执行语句会更改表,但它不会更新db/schema.rb,因此当在测试环境中重新创建表时,它会返回到InnoDB并且我的全文搜索失败。我如何着手更改/添加迁移,以便将现有表修改为MyISAM并更新schema.rb,以便我的数据库和相应的测试数据库得到相应更新? 最佳答案 我没有找到执行此操作的好方法。您可以像有人建议的那样更改您的schema.rb,然后运行:rakedb:schema:load,但是,这将覆盖您的数据。我的做法是(假设

  4. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  5. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. ruby-on-rails - 未初始化的常量 Psych::Syck (NameError) - 2

    在我的gem中,我需要yaml并且在我的本地计算机上运行良好。但是在将我的gem推送到ruby​​gems.org之后,当我尝试使用我的gem时,我收到一条错误消息=>"uninitializedconstantPsych::Syck(NameError)"谁能帮我解决这个问题?附言RubyVersion=>ruby1.9.2,GemVersion=>1.6.2,Bundlerversion=>1.0.15 最佳答案 经过几个小时的研究,我发现=>“YAML使用未维护的Syck库,而Psych使用现代的LibYAML”因此,为了解决

  9. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  10. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

随机推荐