如何在使用“layoutAttributesForElements”时对集合视图布局更改进行动画处理?

2024-02-08

我制作了一个自定义集合视图流布局,可以在“胶片带”和“列表”布局之间切换(带动画)。但在向边缘单元添加一些奇特的动画后,切换动画中断了。这是目前的样子,没有进行这些更改:

Toggling between film strip and list mode with animation

动画很流畅,对吧?这是当前的工作代码(完整的演示项目在这里 https://github.com/aheze/AnimateCollectionViewLayoutAttributes):

enum LayoutType {
    case strip
    case list
}

class FlowLayout: UICollectionViewFlowLayout {
    
    var layoutType: LayoutType
    var layoutAttributes = [UICollectionViewLayoutAttributes]() /// store the frame of each item
    var contentSize = CGSize.zero /// the scrollable content size of the collection view
    override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
    
    /// pass attributes to the collection view flow layout
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return layoutAttributes[indexPath.item]
    }
    
    // MARK: - Problem is here
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        /// edge cells don't shrink, but the animation is perfect
        return layoutAttributes.filter { rect.intersects($0.frame) } /// try deleting this line
        
        /// edge cells shrink (yay!), but the animation glitches out
        return shrinkingEdgeCellAttributes(in: rect)
    }
    
    /// makes the edge cells slowly shrink as you scroll
    func shrinkingEdgeCellAttributes(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let collectionView = collectionView else { return nil }

        let rectAttributes = layoutAttributes.filter { rect.intersects($0.frame) }
        let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size) /// rect of the visible collection view cells

        let leadingCutoff: CGFloat = 50 /// once a cell reaches here, start shrinking it
        let trailingCutoff: CGFloat
        let paddingInsets: UIEdgeInsets /// apply shrinking even when cell has passed the screen's bounds

        if layoutType == .strip {
            trailingCutoff = CGFloat(collectionView.bounds.width - leadingCutoff)
            paddingInsets = UIEdgeInsets(top: 0, left: -50, bottom: 0, right: -50)
        } else {
            trailingCutoff = CGFloat(collectionView.bounds.height - leadingCutoff)
            paddingInsets = UIEdgeInsets(top: -50, left: 0, bottom: -50, right: 0)
        }

        for attributes in rectAttributes where visibleRect.inset(by: paddingInsets).contains(attributes.center) {
            /// center of each cell, converted to a point inside `visibleRect`
            let center = layoutType == .strip
                ? attributes.center.x - visibleRect.origin.x
                : attributes.center.y - visibleRect.origin.y

            var offset: CGFloat?
            if center <= leadingCutoff {
                offset = leadingCutoff - center /// distance from the cutoff, 0 if exactly on cutoff
            } else if center >= trailingCutoff {
                offset = center - trailingCutoff
            }

            if let offset = offset {
                let scale = 1 - (pow(offset, 1.1) / 200) /// gradually shrink the cell
                attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
            }
        }
        return rectAttributes
    }
    
    /// initialize with a LayoutType
    init(layoutType: LayoutType) {
        self.layoutType = layoutType
        super.init()
    }
    
    /// make the layout (strip vs list) here
    override func prepare() { /// configure the cells' frames
        super.prepare()
        guard let collectionView = collectionView else { return }
        
        var offset: CGFloat = 0 /// origin for each cell
        let cellSize = layoutType == .strip ? CGSize(width: 100, height: 50) : CGSize(width: collectionView.frame.width, height: 50)
        
        for itemIndex in 0..<collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: itemIndex, section: 0)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            
            let origin: CGPoint
            let addedOffset: CGFloat
            if layoutType == .strip {
                origin = CGPoint(x: offset, y: 0)
                addedOffset = cellSize.width
            } else {
                origin = CGPoint(x: 0, y: offset)
                addedOffset = cellSize.height
            }
            
            attributes.frame = CGRect(origin: origin, size: cellSize)
            layoutAttributes.append(attributes)
            offset += addedOffset
        }
        
        self.contentSize = layoutType == .strip /// set the collection view's `collectionViewContentSize`
            ? CGSize(width: offset, height: cellSize.height) /// if strip, height is fixed
            : CGSize(width: cellSize.width, height: offset) /// if list, width is fixed
    }
    
    /// boilerplate code
    required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }
    override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
        let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
        context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
        return context
    }
}
class ViewController: UIViewController {
    
    var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    var isExpanded = false
    lazy var listLayout = FlowLayout(layoutType: .list)
    lazy var stripLayout = FlowLayout(layoutType: .strip)
    
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
    @IBAction func toggleExpandPressed(_ sender: Any) {
        isExpanded.toggle()
        if isExpanded {
            collectionView.setCollectionViewLayout(listLayout, animated: true)
        } else {
            collectionView.setCollectionViewLayout(stripLayout, animated: true)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.collectionViewLayout = stripLayout /// start with the strip layout
        collectionView.dataSource = self
        collectionViewHeightConstraint.constant = 300
    }
}

/// sample data source
extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath) as! Cell
        cell.label.text = "\(data[indexPath.item])"
        cell.contentView.layer.borderWidth = 5
        cell.contentView.layer.borderColor = UIColor.red.cgColor
        return cell
    }
}

class Cell: UICollectionViewCell {
    @IBOutlet weak var label: UILabel!
}

同样,一切都很完美,包括动画。然后,我尝试让单元格在接近屏幕边缘时收缩。我推翻了layoutAttributesForElements去做这个。

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    return layoutAttributes.filter { rect.intersects($0.frame) } /// delete this line
    return shrinkingEdgeCellAttributes(in: rect) /// replace with this
}
Film-strip List
Edge cells shrink when scrolling horizontally Edge cells shrink when scrolling vertically

缩放/缩小动画很棒。但是,当我在布局之间切换时,过渡动画被破坏。

Before (return layoutAttributes.filter...) After (return shrinkingEdgeCellAttributes(in: rect))
Toggling between film strip and list mode with smooth animation Toggling between film strip and list mode, animation is very broken

我该如何修复这个动画?我应该使用自定义的UICollectionViewTransitionLayout,如果是这样,怎么办?


哇!这是一次锻炼。我能够修改你的FlowLayout这样动画就不会出现问题。见下文。

有用!

Problem

这就是正在发生的事情。当您更改布局时,layoutAttributesForElements中的方法FlowLayout如果集合视图的内容偏移量不是这样的,则被调用两次(0, 0).

这是因为您已经重写了“shouldInvalidateLayout”以返回true无论是否实际需要。我相信UICollectionView在布局更改之前和之后(根据观察)在布局上调用此方法。

这样做的副作用是您的缩放变换会应用两次 - 在动画到可见布局属性之前和之后。

不幸的是,尺度变换是基于contentOffset集合视图的(link https://github.com/aheze/AnimateCollectionViewLayoutAttributes/blob/main/AnimateCollectionViewLayoutAttributes/FlowLayout.swift#L42)

let visibleRect = CGRect(
    origin: collectionView.contentOffset, 
    size: collectionView.frame.size
)

在布局更改期间contentOffset不一致。动画开始之前contentOffset适用于之前的布局。动画结束后,是相对于新布局而言的。在这里我还注意到,如果没有充分的理由,contentOffset会“跳跃”(见注释1)

由于您使用visibleRect来查询要应用比例的布局属性集,因此它会引入更多错误。

Solution

我能够通过应用这些更改找到解决方案。

  1. 编写辅助方法以将前一个布局留下的内容偏移量(和依赖的visibleRect)转换为对此布局有意义的值。
  2. 防止多余的布局属性计算prepare method
  3. 跟踪布局何时和何时不动画
// In Flow Layout

class FlowLayout: UICollectionViewFlowLayout {
    var animating: Bool = false
    // ...
}

// In View Controller,

isExpanded.toggle()
        
if isExpanded {
    listLayout.reset()
    listLayout.animating = true // <--
    // collectionView.setCollectionViewLayout(listLayout)
} else {
    stripLayout.reset()
    stripLayout.animating = true // <--
    // collectionView.setCollectionViewLayout(stripLayout)
}
  1. 覆盖targetContentOffset处理内容偏移变化的方法(防止跳转)
// In Flow Layout

class FlowLayout: UICollectionViewFlowLayout {
    
    var animating: Bool = false
    var layoutType: LayoutType
    // ...
    
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        guard animating else {
            // return super
        }

        // Use our 'graceful' content content offset
        // instead of arbitrary "jump"
        
        switch(layoutType){
        case .list: return transformCurrentContentOffset(.fromStripToList)
        case .strip: return transformCurrentContentOffset(.fromListToStrip)
        }
    }

// ...

内容偏移变换的实现如下。

/**
 Transforms this layouts content offset, to the other layout
 as specified in the layout transition parameter.
*/
private func transformCurrentContentOffset(_ transition: LayoutTransition) -> CGPoint{
    
    let stripItemWidth: CGFloat = 100.0
    let listItemHeight: CGFloat = 50.0
    
    switch(transition){
    case .fromStripToList:
        let numberOfItems = collectionView!.contentOffset.x / stripItemWidth  // from strip
        var newPoint = CGPoint(x: 0, y: numberOfItems * CGFloat(listItemHeight)) // to list

        if (newPoint.y + collectionView!.frame.height) >= contentSize.height{
            newPoint = CGPoint(x: 0, y: contentSize.height - collectionView!.frame.height)
        }

        return newPoint

    case .fromListToStrip:
        let numberOfItems = collectionView!.contentOffset.y / listItemHeight // from list
        var newPoint = CGPoint(x: numberOfItems * CGFloat(stripItemWidth), y: 0) // to strip

        if (newPoint.x + collectionView!.frame.width) >= contentSize.width{
            newPoint = CGPoint(x: contentSize.width - collectionView!.frame.width, y: 0)
        }

        return newPoint
    }
}

我在评论中遗漏了一些小细节,并作为对 OP 演示项目的拉取请求,以便任何感兴趣的人都可以研究它。

关键要点是,

  • Use targetContentOffset当内容偏移发生任意变化以响应布局变化时。

  • 注意布局属性查询错误layoutAttributesForElements。调试你的直肠!

  • 请记住清除缓存的布局属性prepare() method.

Notes

  1. 即使在引入比例变换之前,“跳跃”行为就很明显,如下所示your gif https://i.stack.imgur.com/PoYh3.gif.

  2. 如果答案很长,我真诚地道歉。或者,解决方案并不完全是您想要的。这个问题很有趣,这就是为什么我花了一整天的时间试图找到一种提供帮助的方法。

  3. Fork https://github.com/Thisura98/AnimateCollectionViewLayoutAttributes and 拉取请求 https://github.com/aheze/AnimateCollectionViewLayoutAttributes/pull/1.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在使用“layoutAttributesForElements”时对集合视图布局更改进行动画处理? 的相关文章

  • 根据 Swift 中的列表选择在 ViewController 之间传递值

    我试图将 listView 选择的选定索引号从一个 ViewController 传递到另一个 ViewController 但遇到了 tableView didSelectRowAtIndexPath 委托运行时间稍晚于prepareFo
  • 更改目录时 Gitlab CI 运行程序作业失败退出状态 1

    我正在使用我的个人机器作为使用 Fastlane 的 iOS 项目的运行程序 这主要是因为共享运行器没有为 iOS 设置 因为它们没有安装 Xcode 更改目录时我的作业立即失败 它是一个 shell 运行程序 根本没有其他自定义配置 有什
  • 如何在导航栏上添加 UIView?

    我需要覆盖UINavigationBar with UIView像这儿 除了使用带有按钮返回的自定义 UIView 作为导航栏之外 还有其他方法可以做到这一点吗 您可以将子视图添加到应用程序的基本视图 UIApplication share
  • 使用 JSONKit 解析 JSON 文件

    我正在构建一个音叉应用程序 货叉应允许最多 12 个预设节距 此外 我希望允许用户选择一个主题 每个主题都会加载一组预设 不必使用所有预设 我的配置文件看起来像这样 theme A3 comment An octave below conc
  • 与 parse-server 和 auth0 的自定义身份验证集成

    我想将 auth0 com 与开源解析服务器结合使用 我当前的方法是通过 iOS 的 Lock 库使用标准登录从 auth0 获取令牌 使用该令牌 我想在解析服务器上调用自定义身份验证方法 该方法检查令牌是否有效 如果有效则将登录用户 我的
  • 在 macOS 上使用 Swift 3 从剪贴板读取

    我是 Swift 的初学者 我想弄清楚如何在 macOS Swift 3 上读取已复制到剪贴板的内容 我搜索了很多 但似乎找不到任何有效的东西 我从网上尝试过的一些事情 var pasteboardItems NSPasteboardIte
  • iPhone 的翻译器?

    我对为 iPhone 制作一个解释器很感兴趣 这将是一个实验性的想法 但可能会很棒 我喜欢让我自 己的语言适合移动计算和数学的想法 我查阅了很多资料 发现有关 iPhone 上的口译员的信息很复杂 苹果会允许什么 我见过这个应用程序 这是一
  • 在 Swift 中的 For 循环中更改对象的属性

    我创建了一个名为 ShoppingList 的简单结构 struct ShoppingList var shoppingListId NSNumber var title String var groceryItems GroceryIte
  • gestureRecognizer shouldReceiveTouch 持续存在于已释放的视图中导致崩溃

    我有一个相当简单的 UITableView 它在堆栈上推送一个新视图 新视图有一个像这样初始化的gestureRecognizer synthesize swipeGestureLeft void viewDidLoad swipeGest
  • 使用javascript以编程方式触发iOS safari中的复制菜单?

    我正在尝试实现一种用户友好的方式 将一些文本从文本输入字段复制到 iOS Safari 上的剪贴板 我知道无法在这个平台上以编程方式完成此操作 但我希望能够尽可能地指导用户体验 在 iOS Safari 上 当用户手动突出显示某些文本时 会
  • SDK 和 iOS 部署目标。

    我使用最新的 SDK 4 1 构建项目并设置 iOS 部署目标 3 0 如果我使用4 0 sdk的某些方法 我可以在真正的iPhone 3 0上运行我的项目吗 您只能在 OS 3 设备上运行您的项目 如果有条件地编码围绕您想要使用的 OS
  • UIScrollView 与 UITabBarController 切断

    我有一个 UIScrollView 我将其放置在视图中 界面生成器文档 xib m h 但是 UIScrollView 的下半部分被剪切 并且由于我有一个 UITabBarController 而没有显示其下半部分 我在 appdelega
  • Swift PageControl 当前页面上更大的点

    我试图将当前页面的点缩放为大于未 选择 的点 我正在使用滚动视图委托来确定哪个页面是当前的 目前 点的大小没有变化 我将如何实现这一目标 func scrollViewDidEndDecelerating scrollView UIScro
  • UITableViewCell 内嵌套 UIStackView 内的 UILabel 有时会被截断

    我的一个表设置中有一个表视图单元格 其中包含以下视图层次结构 外部水平 stackview 固定到单元格内容视图的尾部 前部 底部和顶部边缘 右侧标签固定到其父 stackViewHackView 的尾部 前部 底部和顶部边缘 在我的控制器
  • 在 UIViewRepresentable CollectionView(包装的 UICollectionView)中使用 UICollectionViewCell 的 SwiftUI 视图

    我必须更换现有的SwiftUI List的视图UICollectionView 因为应用程序设计已更新 并且新设计对于 SwiftUI 来说非常复杂 因此必须作为自定义实现UICollectionViewFlowLayout 所以视图 现在
  • 使用未声明的类型“对象”

    这太奇怪了 通常我可以理解未声明的类 但这是声称 Object 类本身未声明 NSObject 可以工作 但我的项目设置方式我需要它是一个纯 Swift 对象 我的类标题如下所示 import UIKit import Foundation
  • Swift - 保存在 TableView 中选择的复选标记

    我对 Swift 相当陌生 并且在 TableView 多重选择方面遇到问题 我有多个选择 可以用复选标记进行检查 类似于待办事项列表 当我检查项目时 我希望能够返回 ListView 并保存我的选择 我假设将其保持在已保存状态的代码将位于
  • iOS 搜索栏不显示结果

    更新 这实际上有效 我的自定义单元格的样式尚未出现 因此单元格看起来是空白的 那我怎样才能得到searchResultsTableView使用我的自定义单元格 我在表格视图中实现了搜索栏 当我调试时搜索 过滤所有工作 但是当我在搜索栏中输入
  • 如何安全地重命名 iOS 分发配置文件?

    我几个小时前刚刚提交了我的第一个应用程序 现在处于 等待审核 状态 但我犯了一个错误 我已经命名了我的分配配置文件My Company Distribution Profile 我应该做的事情被命名为我的发行版配置文件My GAME Dis
  • 带有自定义字体的 UILabel 错误呈现

    在我的 iPhone 应用程序中 我为所有 UILabel 设置了自定义字体 更准确地说 我对 UILabel 进行了子类化 重写了一个方法 在该方法中设置了自定义字体 然后将 IB 中的所有标签设置为该自定义类 现在的问题是 所有文本都渲

随机推荐

  • 提交按钮在 asp.net mvc 中不起作用

    我有一个模型 public class FormCreateModel public FormModel formInfo get set public FieldModel fieldInfo get set public Institu
  • JQuery Fancybox - 多个内联实例

    我试图调用多个内联项目 当我单击它们时 它只显示第一个项目的内容 我正在拔头发 请帮忙 我的 JavaScript 调用 My HTML div class atrack img src images albumcovers Italian
  • 我可以从旧版 C 回调中抛出 C++ 异常吗?

    我有使用一些 C 库的 C 代码 C 库采用 C 语言回调 我在 C 代码中编写了一个回调 现在我需要以某种方式报告它的错误 但它返回 void 我想知道是否可以从 C 代码中使用的 C 回调抛出异常 这对我来说很难理解 谢谢 博达 西多
  • 如何获取 VBA For Each 循环中的索引(使用 Excel 编程)?

    我正在使用 EXCEL VBA 处理一些数据 这就是我想要做的 在此工作表中 我想创建一个函数 GetDebutDate 它可以自动计算该行具有值的第一个日期 例如 在 Mark 行中 第一次获取值是Aug 05 编号为 4 我对 VBA
  • 如何将 retryWhen 与返回布尔值的函数一起使用?

    这是我的代码 this http post this url get extension headers headers map res gt res body retryWhen errors gt return responseErro
  • Golang:TCP客户端/服务器数据分隔符

    不确定如何提出这个问题 以及它是否真的只与 go 语言相关 但我想做的是拥有一个 tcp 服务器和客户端 在它们之间交换数据 基本上客户端会将大量数据流式传输到较小的数据中当数据块发送到服务器时 服务器将等待读取每个数据块 然后回复一个状态
  • Android 动态壁纸缩放

    我正在学习如何制作动态壁纸 但我有一个困境 我相信所有刚开始的人也有这样的困境 有这么多分辨率的屏幕尺寸 我怎样才能使一组艺术品在代码中为所有版本重新缩放 我知道它已经完成了 因为我看到了很多应用程序中的图像 并且它们被重新缩放 如果只是一
  • Office JS-将自定义属性添加到新文档

    我正在开发一个 Office word 插件 但我遇到了这个问题 我需要将自定义属性分配给将在新窗口 实例中打开的新文档 我已经对已经以这种方式打开的文档使用自定义属性 setProperty propName propValue Word
  • Swift 短执行语法

    我正在寻找编写简短语法的方法 例如 在JS PHP等中 var a 1 function Foo gt void a Foo 如果 a 存在 则运行 Foo a 和 Foo 本身已经意味着存在或不存在 语法看起来更好 然而 在 Swift
  • 在循环列时如何在 pandas 中生成清晰的绘图?

    生成可复制性的数据帧 df pd DataFrame np random randn 50 1000 columns list ABCDABCDEDABCDABCDEDABCDABCDEDABCDABCDEDABCDABCDEDABCDAB
  • CMake:CMAKE_REQUIRED_LIBRARIES 中的库顺序,用于在配置时测试最小程序

    我编写了这段小代码 以确保我的软件在必要时链接到 libatomic 通常只有在 Raspberry Pi 上才需要链接到 libatomic 目前 我使用的是 Raspberry Pi 4 带有 Raspbian Bullseye 64
  • SFINAE 与 std::enable_if 和 std::is_default_constructible 用于 libc++ 中的不完整类型

    当使用 SFINAE 检测模板化类型是否默认可构造时 我刚刚观察到 libc 的一个奇怪问题 以下是我能想到的一个最小示例 include
  • Rails、Puma、Sidekiq 如何计算总数据库连接数?

    我正进入 状态ActiveRecord ConnectionTimeoutError一天一次或两次 有人可以帮助我计算我的应用程序与数据库建立的连接数量吗 以及优化我的连接的建议 这是我的配置 AWS Database Mysql Vers
  • 在循环中创建变量和数据集? (右)

    这是我第一次尝试使用 R 构建函数 基本上我的预期目标如下 使用 RoogleVision 包与 Google Cloud Vision API 进行通信 该函数遍历目录中的图像 从 Google Vision 功能中检索每张图片的所需信息
  • 异步方法在调用或等待时抛出异常吗?

    当我调用异步方法并取回任务时 它会立即抛出还是会等到我等待任务 换句话说 这段代码能工作吗 或者我是否也必须将方法调用包装在 try 块中 Task task ThisMethodWillThrow try await task catch
  • 詹金斯管道作业的 Cobertura 代码覆盖率报告

    我正在使用 jenkins 的管道插件 我想为每次运行生成代码覆盖率报告并将其与管道用户界面一起显示 有没有一个插件可以用来做到这一点 例如 Cobertura 但它似乎不受管道支持 有一种方法可以添加管道步骤来发布覆盖率报告 但它不会显示
  • 多对多关系中的多个级联删除路径 (EF 4.1)

    表格 Shop Product Category 关系 Shop 1 lt gt n Categories Shop 1 lt gt n Products Categories n lt gt n Products 级联删除 Shop gt
  • 如何形成 cfhttp 调用来使用自定义 Web 服务 API

    我已经做了 11 年的 cf 开发人员 但很不好意思地说我在 Web 服务方面没有做过任何实质性的事情 如何形成 cfhttp 调用来使用供应商提供的以下 Web 服务 API 肥皂 1 2 要求 POST Portal internet
  • as.numeric 函数更改我的数据框中的值[重复]

    这个问题在这里已经有答案了 我有一列包含速度测量值 我需要将其更改为数字 以便我可以使用平均值和总和函数 然而 当我转换它们时 值会发生很大的变化 为什么是这样 这是我的数据最初的样子 这是数据框的结构 data frame 1899571
  • 如何在使用“layoutAttributesForElements”时对集合视图布局更改进行动画处理?

    我制作了一个自定义集合视图流布局 可以在 胶片带 和 列表 布局之间切换 带动画 但在向边缘单元添加一些奇特的动画后 切换动画中断了 这是目前的样子 没有进行这些更改 动画很流畅 对吧 这是当前的工作代码 完整的演示项目在这里 https