如何在使用“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”时对集合视图布局更改进行动画处理? 的相关文章

  • 如何在iOS中处理1到3个手指的滑动手势

    我使用以下代码来处理代码中的 1 根手指滑动 UISwipeGestureRecognizer swipe UISwipeGestureRecognizer alloc initWithTarget self action selector
  • locationOfTouch 和 numberOfTouches

    你好 我有这个识别器 设置为 2 次触摸 但它只返回一个 而不是两个 CGPoint void gestureLoad UIGestureRecognizer recognizer recognizer UITapGestureRecogn
  • 将自定义数据包含到 iOS 故障转储中

    你好 堆栈溢出 有一个简单的问题要问您 当我的应用程序在用户的设备上崩溃时 是否可以将自定义错误数据嵌入到自动生成的 iOS 故障转储中 例如 我的 SQlite 数据库由于某种原因无法运行 例如 数据库文件已损坏 我无法从这个错误中恢复
  • 如何保护 iOS 应用程序免受任何操作系统攻击(在越狱设备上)

    我希望保护我的应用程序数据 以防任何操作系统攻击或越狱 iOS 设备上的未经授权的访问 在这种情况下 有什么方法可以检测此类威胁并保护应用程序数据 虽然我同意 jrturton 的说法 但如果您有想要保护的关键数据免受流氓应用程序 而不是用
  • 无法在 Swift 中对闭包进行弱引用

    Update 我试着不弱化地写一下 好像也没有漏的情况 所以也许这个问题已经没有必要了 在 Objective C ARC 中 当你想让一个闭包能够在闭包内部使用它自己时 该块不能捕获对自身的强引用 否则它将是一个保留循环 因此您可以使闭包
  • iOS 中 NSDecimalNumber 的小数分隔符错误

    我尝试通过以下方式输出具有正确的小数分隔符的十进制数的描述 NSString strValue 9 94300 NSDecimalNumber decimalNumber NSDecimalNumber decimalNumberWithS
  • 为什么 iOS 启动屏幕很慢?

    我的 iOS 应用程序启动屏幕大约需要 3 5 秒 我有一张将在启动屏幕后加载的地图 我的用户必须等待启动屏幕加载 然后再等待 3 秒才能加载地图 有没有办法最大限度地减少启动屏幕时间 基本上这种延迟意味着you在启动过程中做了一些非常错误
  • 如何让UITextView背景线与文字对齐?

    我正在尝试绘制 UITextView 的背景线 这是我用来画这些线的代码 CGContextBeginPath context CGContextSetStrokeColorWithColor context self horizontal
  • 在 iPhone 3GS 与 iPhone 4 上为 Mobile Safari 嵌入 HTML5 视频

    我在服务器上的 mp4 文件中有 H 264 AAC 编码的视频 mime 类型的视频 mp4 添加到 Web 服务器 IIS 7 并且我有一个带有视频标签的页面
  • 从命令行添加 Xcode 开发者帐户

    我正在尝试使用xcodebuild allowProvisioningUpdates在我只能通过命令行访问的计算机 Azure Devops macOS 托管计算机 上 不幸的是 根据man xcodebuild为了使用 allowProv
  • 会话重新启动后 AVcapture 会话启动缓慢

    我有一个主视图控制器 它连接到具有 avcapturesession 的第二个视图控制器 我第一次从主视图控制器转向捕获会话控制器 大约需要 50 毫秒 使用 仪器 检查 然后我从捕获会话返回到主视图控制器 然后从主控制器返回到 avcap
  • UItextView 背景颜色 Linespacing 区域太

    我正在尝试在 UITextView 中复制文本突出显示 不是搜索文本突出显示 但我也被行间距的颜色所困扰 我该如何纠正这个问题 现在的情况 期望的结果 我已将以下属性添加到我的 UiTextview 的属性文本中 对于段落行间距 我使用了以
  • 访问目标 c 中的类方法。使用 self 还是类名?

    我正在学习 iOS 编程 并且对以下有关关键字 self 的使用的代码感到困惑 据我了解 self就像Java的this 它指的是当前实例 当我想调用类方法时 通常的方式应该是这样 PlayingCard validSuits 但是侵入实例
  • 如何使用 RX 应用宽限时间?

    我有一个Observable
  • 当直接从 usdz 文件而不是 Reality Composer 文件加载模型时,如何在 RealityKit 中渲染阴影?

    将模型直接从 usdz 文件添加到场景锚点时 模型下方不会出现接地阴影 如果您将模型导入 RealityComposer 并添加 RealityComposer 场景 则会出现这些 但是 您可以在不使用 RC 的情况下启用接地阴影吗 我尝试
  • 如何更改 SwiftUI 列表中分隔符的颜色?

    我在 SwiftUI 中创建了一个列表 我想更改颜色或删除分隔符 因为在 UIKit 中 我们可以轻松更改 TableView 中分隔符的颜色 下面是 SwiftUI 中列表的代码和 UI 图片 State private var user
  • CoreBluetooth:检测设备超出范围/连接超时

    我正在设计一个 iOS 框架来处理多个 BLE 设备 均为同一类型 目前一切都运行良好 除了一件事 客户想要一个包含可用设备的列表 但是 我如何检测过去发现的设备何时不再可用 当我尝试连接到不再可用的设备时 会出现另一个问题 文档说 连接尝
  • 模态转场需要点击 2 次而不是 1 次

    我的 UITableView 需要点击 2 次才能显示所选单元格的详细信息页面 一次用于选择 另一次用于显示详细信息视图 我希望有一个 CLI 直接显示所单击单元格的详细视图 我在 UITableViewManager m 中使用此方法的模
  • SpriteKit的更新函数:时间与帧率

    一般来说 我对编程和 Spritekit 很陌生 并且有兴趣探索毫秒和帧率之间的关系 以及如何使用更新函数作为两者之间的中介 帧率与毫秒 从本质上讲 帧速率和时间之间的主要区别在于时间始终一致 而帧速率则不然 由于密集的图形程序 它可能会下
  • 是否可以跨 2 个不同的 iOS 应用程序访问数据?

    假设我在 App1 中存储了一些 ID 数据 并希望在同一设备上的 App2 中访问它 平台上可以这样吗 如果没有的话有什么解决方法吗 您可以使用iOS 钥匙扣 http developer apple com library ios do

随机推荐

  • 提交按钮在 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