UIScrollView 动画到 targetContentOffset 不稳定
Posted
技术标签:
【中文标题】UIScrollView 动画到 targetContentOffset 不稳定【英文标题】:UIScrollView animation to targetContentOffset erratic 【发布时间】:2015-09-30 15:21:41 【问题描述】:我在 UITableViewCell 中实现了 UIScrollView,它使用户能够以与 ios 邮件应用程序相同的方式左右滚动以显示按钮。明确设置框架和位置的原始实现运行良好,但我重构了代码以在整个过程中使用自动布局。隐藏/显示左侧按钮(辅助按钮)的“容器”的动画效果很好,但是当右侧容器(编辑按钮)在达到所需偏移量之前减慢时,滚动视图停止的动画在进入最终状态之前位置。
所有计算都使用刚刚转换的相同数学(例如,+ 而不是 - 值,> 而不是
class SwipeyTableViewCell: UITableViewCell
// MARK: Constants
private let thresholdVelocity = CGFloat(0.6)
private let maxClosureDuration = CGFloat(40)
// MARK: Properties
private var buttonContainers = [ButtonContainerType: ButtonContainer]()
private var leftContainerWidth: CGFloat
return buttonContainers[.Accessory]?.containerWidthWhenOpen ?? CGFloat(0)
private var rightContainerWidth: CGFloat
return buttonContainers[.Edit]?.containerWidthWhenOpen ?? CGFloat(0)
private var buttonContainerRightAnchor = NSLayoutConstraint()
private var isOpen = false
// MARK: Subviews
private let scrollView = UIScrollView()
// MARK: Lifecycle methods
override func awakeFromNib()
super.awakeFromNib()
// Initialization code
scrollView.delegate = self
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
contentView.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraintEqualToAnchor(contentView.topAnchor).active = true
scrollView.leftAnchor.constraintEqualToAnchor(contentView.leftAnchor).active = true
scrollView.rightAnchor.constraintEqualToAnchor(contentView.rightAnchor).active = true
scrollView.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor).active = true
let scrollContentView = UIView()
scrollContentView.backgroundColor = UIColor.cyanColor()
scrollView.addSubview(scrollContentView)
scrollContentView.translatesAutoresizingMaskIntoConstraints = false
scrollContentView.topAnchor.constraintEqualToAnchor(scrollView.topAnchor).active = true
scrollContentView.leftAnchor.constraintEqualToAnchor(scrollView.leftAnchor).active = true
scrollContentView.rightAnchor.constraintEqualToAnchor(scrollView.rightAnchor).active = true
scrollContentView.bottomAnchor.constraintEqualToAnchor(scrollView.bottomAnchor).active = true
scrollContentView.widthAnchor.constraintEqualToAnchor(contentView.widthAnchor, constant: 10).active = true
scrollContentView.heightAnchor.constraintEqualToAnchor(contentView.heightAnchor).active = true
buttonContainers[.Accessory] = ButtonContainer(type: .Accessory, scrollContentView: scrollContentView)
buttonContainers[.Edit] = ButtonContainer(type: .Edit, scrollContentView: scrollContentView)
for bc in buttonContainers.values
scrollContentView.addSubview(bc)
bc.widthAnchor.constraintEqualToAnchor(contentView.widthAnchor).active = true
bc.heightAnchor.constraintEqualToAnchor(scrollContentView.heightAnchor).active = true
bc.topAnchor.constraintEqualToAnchor(scrollContentView.topAnchor).active = true
bc.containerToContentConstraint.active = true
scrollView.contentInset = UIEdgeInsetsMake(0, leftContainerWidth, 0, rightContainerWidth)
func closeContainer()
scrollView.contentOffset.x = CGFloat(0)
extension SwipeyTableViewCell: UIScrollViewDelegate
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>)
let xOffset: CGFloat = scrollView.contentOffset.x
isOpen = false
for bc in buttonContainers.values
if bc.isContainerOpen(xOffset, thresholdVelocity: thresholdVelocity, velocity: velocity)
targetContentOffset.memory.x = bc.offsetRequiredToOpenContainer()
NSLog("Target offset \(targetContentOffset.memory.x)")
isOpen = true
break /// only one container can be open at a time so cn exit here
if !isOpen
NSLog("Closing container")
targetContentOffset.memory.x = CGFloat(0)
let ms: CGFloat = xOffset / velocity.x /// if the scroll isn't on a fast path to zero, animate it closed
if (velocity.x == 0 || ms < 0 || ms > maxClosureDuration)
NSLog("Animating closed")
dispatch_async(dispatch_get_main_queue())
scrollView.setContentOffset(CGPointZero, animated: true)
/**
Defines the position of the container view for buttons assosicated with a SwipeyTableViewCell
- Edit: Identifier for a UIView that acts as a container for buttons to the right of the cell
- Accessory: Identifier for a UIView that acts as a container for buttons to the left of the vell
*/
enum ButtonContainerType
case Edit, Accessory
extension ButtonContainerType
func getConstraints(scrollContentView: UIView, buttonContainer: UIView) -> NSLayoutConstraint
switch self
case Edit:
return buttonContainer.leftAnchor.constraintEqualToAnchor(scrollContentView.rightAnchor)
case Accessory:
return buttonContainer.rightAnchor.constraintGreaterThanOrEqualToAnchor(scrollContentView.leftAnchor)
func containerOpenedTest() -> ((scrollViewOffset: CGFloat, containerFullyOpenWidth: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool)
switch self
case Edit:
return (scrollViewOffset: CGFloat, containerFullyOpenWidth: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool in
(scrollViewOffset > containerFullyOpenWidth || (scrollViewOffset > 0 && velocity.x > thresholdVelocity))
case Accessory:
return (scrollViewOffset: CGFloat, containerFullyOpenWidth: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool in
(scrollViewOffset < -containerFullyOpenWidth || (scrollViewOffset < 0 && velocity.x < -thresholdVelocity))
func transformOffsetForContainerSide(containerWidthWhenOpen: CGFloat) -> CGFloat
switch self
case Edit:
return containerWidthWhenOpen
case Accessory:
return -containerWidthWhenOpen
/// A UIView subclass that acts as a container for buttongs associated with a SwipeyTableCellView
class ButtonContainer: UIView
private let scrollContentView: UIView
private let type: ButtonContainerType
private let maxNumberOfButtons = 3
let buttonWidth = CGFloat(65)
private var buttons = [UIButton]()
var containerWidthWhenOpen: CGFloat
// return CGFloat(buttons.count) * buttonWidth
return buttonWidth // TODO: Multiple buttons not yet implements - this will cause a bug!!
var containerToContentConstraint: NSLayoutConstraint
return type.getConstraints(scrollContentView, buttonContainer: self)
var offsetFromContainer = CGFloat(0)
didSet
let delta = abs(oldValue - offsetFromContainer)
containerToContentConstraint.constant = offsetFromContainer
if delta > (containerWidthWhenOpen * 0.5) /// this number is arbitary - can it be more formal?
animateConstraintWithDuration(0.1, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, completion: nil) /// ensure large changes are animated rather than snapped
// MARK: Initialisers
init(type: ButtonContainerType, scrollContentView: UIView)
self.type = type
self.scrollContentView = scrollContentView
super.init(frame: CGRectZero)
backgroundColor = UIColor.blueColor()
translatesAutoresizingMaskIntoConstraints = false
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
// MARK: Public methods
func isContainerOpen(scrollViewOffset: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool
let closure = type.containerOpenedTest()
return closure(scrollViewOffset: scrollViewOffset, containerFullyOpenWidth: containerWidthWhenOpen, thresholdVelocity: thresholdVelocity, velocity: velocity)
func offsetRequiredToOpenContainer() -> CGFloat
return type.transformOffsetForContainerSide(containerWidthWhenOpen)
【问题讨论】:
更新 - 只有在 contentOffset 比 targetContentOffset 大 10pt 或更多时,才会显示到最终偏移的“捕捉”(而不是平滑减速)。低于 10pt 动画很流畅。我会继续寻找,看看是否有一些虚假的添加发生在某个地方,但看起来这可能是 iOS 中的一个错误 【参考方案1】:好的 - 发现错误,这是早期使用 UIScrollView 实验留下的错字。线索是在我之前关于在所需 targetContentOffset 的 10pt 内发生的“捕捉”的评论中......
scrollContentView 宽度约束设置错误如下:
scrollContentView.widthAnchor.constraintEqualToAnchor(contentView.widthAnchor, constant: 10).active = true
在我发现我可以通过设置它的 contentInset 来强制 UIScrollView 滚动之前,我只是让子视图比 UIScrollView 固定到的单元格的 contentView 更大。因为我一直在逐字重构代码以使用新的锚属性,所以旧代码传播了,我得到了我的错误!
所以,不是 iOS 有错……只是我没有注意。学习到教训了!我现在对如何实现一些可能更整洁的东西有一些其他想法。
【讨论】:
以上是关于UIScrollView 动画到 targetContentOffset 不稳定的主要内容,如果未能解决你的问题,请参考以下文章
UIScrollView 在模态视图控制器的动画演示中滚动到不同的位置
如何在 UIScrollView 中的对象(UIViews)上执行翻转动画