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 动画滞后

如何在 UIScrollView 中的对象(UIViews)上执行翻转动画

动画时UIScrollView setContentOffset?

如何在 UIScrollview 和动画上设置优先级

检测用户何时中断 UIScrollView 动画