Soft scroll animation NSScrollView scrollToPoint:
Asked Answered
D

5

12

I want to create soft animation between transitions in simply UI:

first slidesecond slidethird slide

view that moved

view

When a call scrollToPoint: for move view to point that transition doesn't animate. I'm newbe in Cocoa programming (iOS is my background). And I don't know how right use .animator or NSAnimationContext.

Also I was read Core Animation guide but didn't find the solution.

The source can be reach on Git Hub repository

Please help !!!

Darryl answered 16/10, 2013 at 9:6 Comment(0)
P
25

scrollToPoint is not animatable. Only animatable properties like bounds and position in NSAnimatablePropertyContainer are animated. You don't need to do anything with CALayer: remove the wantsLayer and CALayer stuff. Then with following code it is animated.

- (void)scrollToXPosition:(float)xCoord {
    [NSAnimationContext beginGrouping];
    [[NSAnimationContext currentContext] setDuration:5.0];
    NSClipView* clipView = [_scrollView contentView];
    NSPoint newOrigin = [clipView bounds].origin;
    newOrigin.x = xCoord;
    [[clipView animator] setBoundsOrigin:newOrigin];
    [_scrollView reflectScrolledClipView: [_scrollView contentView]]; // may not bee necessary
    [NSAnimationContext endGrouping];
}
Pine answered 17/10, 2013 at 10:48 Comment(1)
Thank you! It works! But how can I know is the property animatable or not?Darryl
C
8

Swift 4 code of this answer

func scroll(toPoint: NSPoint, animationDuration: Double) {
    NSAnimationContext.beginGrouping()
    NSAnimationContext.current.duration = animationDuration
    let clipView = scrollView.contentView
    clipView.animator().setBoundsOrigin(toPoint)
    scrollView.reflectScrolledClipView(scrollView.contentView)
    NSAnimationContext.endGrouping()
}
Ceil answered 5/4, 2018 at 12:21 Comment(2)
thank you for the update but I cannot check it ATM. Did you try it?Darryl
@Darryl yes, I did.Ceil
L
6

The proposed answers have a significant downside: If the user tries to scroll during an ongoing animation, the input will be cause jittering as the animation will forcefully keep on going until completion. If you set a really long animation duration, the issue becomes apparent. Here is my use case, animating a scroll view to snap to a section title (while trying to scroll up at the same time):

enter image description here

I propose the following subclass:

public class AnimatingScrollView: NSScrollView {

    // This will override and cancel any running scroll animations
    override public func scroll(_ clipView: NSClipView, to point: NSPoint) {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        contentView.setBoundsOrigin(point)
        CATransaction.commit()
        super.scroll(clipView, to: point)
    }

    public func scroll(toPoint: NSPoint, animationDuration: Double) {
        NSAnimationContext.beginGrouping()
        NSAnimationContext.current.duration = animationDuration
        contentView.animator().setBoundsOrigin(toPoint)
        reflectScrolledClipView(contentView)
        NSAnimationContext.endGrouping()
    }

}

By overriding the normal scroll(_ clipView: NSClipView, to point: NSPoint) (invoked when the user scrolls) and manually performing the a scroll inside a CATransaction with setDisableActions, we cancel the current animation. However, we don't call reflectScrolledClipView, instead we call super.scroll(clipView, to: point), which will perform other necessary internal procedures and then perform reflectScrolledClipView.

Above class produces better results:

enter image description here

Leith answered 9/10, 2019 at 15:38 Comment(1)
Could you please add an example fo horizontal one?Darryl
H
3

Here is a Swift 4 extension version of Andrew's answer

extension NSScrollView {
    func scroll(to point: NSPoint, animationDuration: Double) {
        NSAnimationContext.beginGrouping()
        NSAnimationContext.current.duration = animationDuration
        contentView.animator().setBoundsOrigin(point)
        reflectScrolledClipView(contentView)
        NSAnimationContext.endGrouping()
    }
}
Hecklau answered 20/3, 2019 at 14:39 Comment(0)
K
0

I know, it's a little bit off topic, but I wanted to have a similar method to scroll to a rectangle with animation like in UIView's scrollRectToVisible(_ rect: CGRect, animated: Bool) for my NSView. I was happy to find this post, but apparently the accepted answer doesn't always work correctly. It turns out that there is a problem with bounds.origin of the clipview. If the view is getting resized (e.g. by resizing the surrounding window) bounds.origin is somehow shifted against the true origin of the visible rectangle in y-direction. I could not figure out why and by how much. Well, there is also this statement in the Apple docs not to manipulate the clipview directly since its main purpose is to function internally as a scrolling machine for views.

But I do know the true origin of the visible area. It’s part of the clipview’s documentVisibleRect. So I take that origin for the calculation of the scrolled origin of the visibleRect and shift the bounds.origin of the clipview by the same amount, and voilà: that works even if the view is getting resized.

Here is my implementation of the new method of my NSView:

   func scroll(toRect rect: CGRect, animationDuration duration: Double) {
        if let scrollView = enclosingScrollView {           // we do have a scroll view
            let clipView = scrollView.contentView           // and thats its clip view
            var newOrigin = clipView.documentVisibleRect.origin // make a copy of the current origin
            if newOrigin.x > rect.origin.x {                // we are too far to the right
                newOrigin.x = rect.origin.x                 // correct that
            }
            if rect.origin.x > newOrigin.x + clipView.documentVisibleRect.width - rect.width {  // we are too far to the left
                newOrigin.x = rect.origin.x - clipView.documentVisibleRect.width + rect.width   // correct that
            }
            if newOrigin.y > rect.origin.y {                // we are too low
                newOrigin.y = rect.origin.y                 // correct that
            }
            if rect.origin.y > newOrigin.y + clipView.documentVisibleRect.height - rect.height {    // we are too high
                newOrigin.y = rect.origin.y - clipView.documentVisibleRect.height + rect.height // correct that
            }
            newOrigin.x += clipView.bounds.origin.x - clipView.documentVisibleRect.origin.x  // match the new origin to bounds.origin
            newOrigin.y += clipView.bounds.origin.y - clipView.documentVisibleRect.origin.y
            NSAnimationContext.beginGrouping()              // create the animation
            NSAnimationContext.current.duration = duration  // set its duration
            clipView.animator().setBoundsOrigin(newOrigin)  // set the new origin with animation
            scrollView.reflectScrolledClipView(clipView)    // and inform the scroll view about that
            NSAnimationContext.endGrouping()                // finaly do the animation
        }
    }

Please note, that I use flipped coordinates in my NSView to make it match the iOS behaviour. BTW: the animation duration in the iOS version scrollRectToVisible is 0.3 seconds.

https://www.fpposchmann.de/animate-nsviews-scrolltovisible/

Kingpin answered 12/8, 2019 at 15:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.