JSQMessagesView 动画行高变化的问题
JSQMessagesView problems with animating row height change
我正在为我的应用程序实现一个简单的信使,用户可以在其中相互聊天。 Messenger 基于 UICollectionView (JSQMessagesViewController),其中每条消息由一个 UICollectionView 行表示。每条消息还有一个顶部标签,用于在发送消息时显示。此标签最初是隐藏的(高度 = 0),当用户点击特定消息(行)时,通过相应地设置高度来显示标签。 (身高=25)
我面临的问题是显示标签的实际动画。 (高度变化)。在到达其位置之前,该行的一部分与下面的行重叠了几个像素。此外,当隐藏标签时,动画首先将高度设置为零,然后文本淡出覆盖下面的消息部分,这看起来非常糟糕。
所以基本上我想要实现的是摆脱前面提到的那两个问题。
代码:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAt indexPath: IndexPath!) -> CGFloat {
if indexPath == indexPathTapped {
return 25
}
let messageCurrent = messages[indexPath.item]
let messagePrev: JSQMessage? = indexPath.item - 1 >= 0 ? messages[indexPath.item - 1] : nil
if messageCurrent.senderId == messagePrev?.senderId || messagePrev == nil {
return 0
}
else{
return 25
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
collectionView.reloadItems(at: [indexPath])
// UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveLinear, animations: {
// collectionView.performBatchUpdates({
// collectionView.reloadItems(at: [indexPath])
// }, completion: nil)
// }, completion: nil)
}
演示:(质量请见谅)
如果有人能帮我解决这个问题,我将不胜感激,因为我已经花了几个小时试图解决这个问题,但一无所获。
提前致谢!
编辑:
我尝试了@jamesk 提出的解决方案如下:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
UIView.animate(withDuration: 0.25) {
collectionView.performBatchUpdates(nil)
}
}
并覆盖 JSQMessagesCollectionViewCell
的 apply
:
extension JSQMessagesCollectionViewCell {
override open func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layoutIfNeeded()
}
}
但是这些更改导致:
我还尝试了使布局无效的第二种解决方案:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
var paths = [IndexPath]()
let itemsCount = collectionView.numberOfItems(inSection: 0)
for i in indexPath.item...itemsCount - 1 {
paths.append(IndexPath(item: i, section: 0))
}
let context = JSQMessagesCollectionViewFlowLayoutInvalidationContext()
context.invalidateItems(at: paths)
UIView.animate(withDuration: 0.25) {
self.collectionView?.collectionViewLayout.invalidateLayout(with: context)
self.collectionView?.layoutIfNeeded()
}
}
结果如下:
插入数据后尝试添加这段代码。
collectionView.reloadItems(at: [indexPath])
UIView.animate(withDuration: 0.6) {
self.view.layoutIfNeeded()
}
好像有两个问题。第一个问题是对 reloadItems(at:)
的调用仅限于旧单元格和新单元格之间的 cross-fading——它不会在旧单元格的布局属性和新单元格的布局属性之间进行插值细胞。第二个问题是,似乎没有任何代码指示您选择的单元格根据需要在应用新布局属性时执行布局传递。
JSQMessagesViewController framework uses subclasses of UICollectionViewFlowLayout and UICollectionViewFlowLayoutInvalidationContext,因此我们可以在更新和动画项目时利用流布局的失效行为。所需要做的只是使受单元格高度变化影响的项目的布局属性(即位置)和委托指标(即大小)无效。
下面的代码是为与包含在 JSQMessagesViewController 的 release_7.3
分支中的 Swift 示例项目一起编写的:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
// Determine the lowest item index affected by the change in cell size.
// Lesser of previous tapped item index (if any) and current tapped item index.
let minItem = min(tappedIndexPath?.item ?? indexPath.item, indexPath.item)
// Update tapped index path.
tappedIndexPath = (tappedIndexPath == indexPath ? nil : indexPath)
// Prepare invalidation context spanning all affected items.
let context = JSQMessagesCollectionViewFlowLayoutInvalidationContext()
let maxItem = collectionView.numberOfItems(inSection: 0) - 1
let indexPaths = (minItem ... maxItem).map { IndexPath(item: [=10=], section: 0) }
context.invalidateItems(at: indexPaths) // Must include all affected items.
context.invalidateFlowLayoutAttributes = true // Recompute item positions (for all affected items).
context.invalidateFlowLayoutDelegateMetrics = true // Recompute item sizes (needed for tapped item).
UIView.animate(withDuration: 0.25) {
collectionView.collectionViewLayout.invalidateLayout(with: context)
collectionView.layoutIfNeeded() // Ensure layout pass for visible cells.
}
}
上面的代码应该是合理的性能。
虽然必须始终重新计算受影响项目的位置,但没有必要像上面那样重新计算所有受影响项目的大小。仅重新计算点击项目的大小就足够了。但是由于 invalidateFlowLayoutDelegateMetrics
属性 的效果总是应用于每个无效的项目,要实现这种更窄的方法,您需要使用两个流布局无效上下文并将项目分开(或实现一个具有相应失效行为的自定义失效上下文)。除非 Instruments 另有说明,否则它可能不值得。
我正在为我的应用程序实现一个简单的信使,用户可以在其中相互聊天。 Messenger 基于 UICollectionView (JSQMessagesViewController),其中每条消息由一个 UICollectionView 行表示。每条消息还有一个顶部标签,用于在发送消息时显示。此标签最初是隐藏的(高度 = 0),当用户点击特定消息(行)时,通过相应地设置高度来显示标签。 (身高=25)
我面临的问题是显示标签的实际动画。 (高度变化)。在到达其位置之前,该行的一部分与下面的行重叠了几个像素。此外,当隐藏标签时,动画首先将高度设置为零,然后文本淡出覆盖下面的消息部分,这看起来非常糟糕。
所以基本上我想要实现的是摆脱前面提到的那两个问题。
代码:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAt indexPath: IndexPath!) -> CGFloat {
if indexPath == indexPathTapped {
return 25
}
let messageCurrent = messages[indexPath.item]
let messagePrev: JSQMessage? = indexPath.item - 1 >= 0 ? messages[indexPath.item - 1] : nil
if messageCurrent.senderId == messagePrev?.senderId || messagePrev == nil {
return 0
}
else{
return 25
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
collectionView.reloadItems(at: [indexPath])
// UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveLinear, animations: {
// collectionView.performBatchUpdates({
// collectionView.reloadItems(at: [indexPath])
// }, completion: nil)
// }, completion: nil)
}
演示:(质量请见谅)
如果有人能帮我解决这个问题,我将不胜感激,因为我已经花了几个小时试图解决这个问题,但一无所获。
提前致谢!
编辑:
我尝试了@jamesk 提出的解决方案如下:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
UIView.animate(withDuration: 0.25) {
collectionView.performBatchUpdates(nil)
}
}
并覆盖 JSQMessagesCollectionViewCell
的 apply
:
extension JSQMessagesCollectionViewCell {
override open func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layoutIfNeeded()
}
}
但是这些更改导致:
我还尝试了使布局无效的第二种解决方案:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
var paths = [IndexPath]()
let itemsCount = collectionView.numberOfItems(inSection: 0)
for i in indexPath.item...itemsCount - 1 {
paths.append(IndexPath(item: i, section: 0))
}
let context = JSQMessagesCollectionViewFlowLayoutInvalidationContext()
context.invalidateItems(at: paths)
UIView.animate(withDuration: 0.25) {
self.collectionView?.collectionViewLayout.invalidateLayout(with: context)
self.collectionView?.layoutIfNeeded()
}
}
结果如下:
插入数据后尝试添加这段代码。
collectionView.reloadItems(at: [indexPath])
UIView.animate(withDuration: 0.6) {
self.view.layoutIfNeeded()
}
好像有两个问题。第一个问题是对 reloadItems(at:)
的调用仅限于旧单元格和新单元格之间的 cross-fading——它不会在旧单元格的布局属性和新单元格的布局属性之间进行插值细胞。第二个问题是,似乎没有任何代码指示您选择的单元格根据需要在应用新布局属性时执行布局传递。
JSQMessagesViewController framework uses subclasses of UICollectionViewFlowLayout and UICollectionViewFlowLayoutInvalidationContext,因此我们可以在更新和动画项目时利用流布局的失效行为。所需要做的只是使受单元格高度变化影响的项目的布局属性(即位置)和委托指标(即大小)无效。
下面的代码是为与包含在 JSQMessagesViewController 的 release_7.3
分支中的 Swift 示例项目一起编写的:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
// Determine the lowest item index affected by the change in cell size.
// Lesser of previous tapped item index (if any) and current tapped item index.
let minItem = min(tappedIndexPath?.item ?? indexPath.item, indexPath.item)
// Update tapped index path.
tappedIndexPath = (tappedIndexPath == indexPath ? nil : indexPath)
// Prepare invalidation context spanning all affected items.
let context = JSQMessagesCollectionViewFlowLayoutInvalidationContext()
let maxItem = collectionView.numberOfItems(inSection: 0) - 1
let indexPaths = (minItem ... maxItem).map { IndexPath(item: [=10=], section: 0) }
context.invalidateItems(at: indexPaths) // Must include all affected items.
context.invalidateFlowLayoutAttributes = true // Recompute item positions (for all affected items).
context.invalidateFlowLayoutDelegateMetrics = true // Recompute item sizes (needed for tapped item).
UIView.animate(withDuration: 0.25) {
collectionView.collectionViewLayout.invalidateLayout(with: context)
collectionView.layoutIfNeeded() // Ensure layout pass for visible cells.
}
}
上面的代码应该是合理的性能。
虽然必须始终重新计算受影响项目的位置,但没有必要像上面那样重新计算所有受影响项目的大小。仅重新计算点击项目的大小就足够了。但是由于 invalidateFlowLayoutDelegateMetrics
属性 的效果总是应用于每个无效的项目,要实现这种更窄的方法,您需要使用两个流布局无效上下文并将项目分开(或实现一个具有相应失效行为的自定义失效上下文)。除非 Instruments 另有说明,否则它可能不值得。