Soft scroll animation NSScrollView scrollToPoint: - cocoa

I want to create soft animation between transitions in simply UI:
view that moved
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 !!!

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];
}

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()
}

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):
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:

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()
}
}

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/

Related

OSX CustomView Doesn't Work After Window Resize

I have a simple view that displays an NSBezierpath. On mouseDown inside the path, the path's fill color sets to yellow and the view redraws. On mouseDown outside the path, the path's fill color sets to blue and the view redraws.
In my storyboard, I have a single window controller with a window content segue to a view controller. The view, customview class HeartView (below) fills the entire view controller.
Everything works fine until the user resizes the window vertically. After that, the view exhibits bizarre behavior: mouseDown no longer works everywhere inside the path, the recolor sometimes happens on mouseDown outside the path, and the path sometimes (but not always) doesn't completely fill. I think something is going on in the superview, but I don't know what.
import Cocoa
class HeartView: NSView {
var mouseLocation : NSPoint = NSZeroPoint
func drawObject(){
//Create an empty Bezier path
let aBezier : NSBezierPath = NSBezierPath()
aBezier.moveToPoint(CGPoint(x: 176.95,y: 44.90))
aBezier.curveToPoint(CGPoint(x: 166.71,y: 145.89),
controlPoint1: CGPoint(x: 76.63,y: 76.78),
controlPoint2: CGPoint(x: 82.59,y: 206.70))
aBezier.curveToPoint(CGPoint(x: 176.95,y: 44.90),
controlPoint1: CGPoint(x: 237.55,y: 224.76),
controlPoint2: CGPoint(x: 276.83,y: 95.98))
aBezier.closePath()
if (aBezier.containsPoint(NSMakePoint(mouseLocation.x, mouseLocation.y))){
NSColor.yellowColor().setFill()
NSColor.greenColor().setStroke()
} else {
NSColor.blueColor().setFill()
NSColor.orangeColor().setStroke()
}
aBezier.fill()
aBezier.lineWidth = 2.0
aBezier.stroke()
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
drawObject()
}
override func mouseDown(theEvent: NSEvent) {
mouseLocation.x = theEvent.locationInWindow.x
mouseLocation.y = theEvent.locationInWindow.y
self.setNeedsDisplayInRect(self.frame)
}
}
I found the answer in Lucas Derraugh's video on Mouse Events (Cocoa Programming L27). Turns out, I was capturing the mouseDown event in the superview's coordinate system. In the mouseDown event, I used "locationInWindow," which is what caused the strange behavior. I changed the method to:
override func mouseDown(theEvent: NSEvent) {
var viewPoint:NSPoint = self.convertPoint(theEvent.locationInWindow, fromView: nil)
mouseLocation.x = viewPoint.x
mouseLocation.y = viewPoint.y
self.needsDisplay = true
}
to convert from the window's coordinate system to the view's. Things now work well after any window resize event.

NSAnimation removes button background colour

I am working on a Mac app. I am trying to do a simple animation which makes an NSButton move down. The animation works really nicely, but when i do it, the background colour of my NSButton disappears for some reason. Here is my code:
// Tell the view to create a backing layer.
additionButton.wantsLayer = YES;
// Set the layer redraw policy. This would be better done in
// the initialization method of a NSView subclass instead of here.
additionButton.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.duration = 1.0f;
additionButton.animator.frame = CGRectOffset(additionButton.frame, 0.0, -20.0);
//additionButton.frame = CGRectOffset(additionButton.frame, 0.0, -20.0);
} completionHandler:nil];
Button move down animation:
Button after move down animation:
Update 1
Just to make it clear, I am not using a background image in my buttons. I am using a background NSColor which I set in the viewDidLoad method like so:
[[additionButton cell] setBackgroundColor:[NSColor colorWithRed:(100/255.0) green:(43/255.0) blue:(22/255.0) alpha:1.0]];
I presume this is an AppKit bug. There are a couple of ways you can work around it.
Workaround 1:
Don't use layers. The button you're animating seems to be small, and you might be able to get away with using a non layer-backed animation and still have it look decent. The button will redraw during each step of the animation, but it will animate correctly. That means this is really all you have to do:
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
additionButton.animator.frame = CGRectOffset(additionButton.frame, 0, -20);
} completionHandler:nil];
Workaround 2:
Set the background color on the layer.
additionButton.wantsLayer = YES;
additionButton.layer.backgroundColor = NSColor.redColor.CGColor;
additionButton.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
additionButton.animator.frame = CGRectOffset(additionButton.frame, 0, -20);
} completionHandler:nil];
Workaround 3:
Subclass NSButtonCell, and implement -drawBezelWithFrame:inView:, drawing your background color there. Keep in mind that the parent view containing the button should be layer-backed, otherwise the button will still redraw on every step.

xcode UITapGestureRecognizer on scrollview not calling until second tap

I have the following code to dismiss the keyboard if the user taps the background. It works fine if the scrollview is in the PointZero position, but if the user scrolls the view and then selects the textview, it doesn't call the "dismissKeyboard' method until the 2nd background tap.
On the first tap (for some reason) moves the scrollview offset to align with the scrollview frame to the screen bottom. The second tap will dismiss the keyboard and run the code below. I know it has to do with the scrollview. Any help would be appreciated.
Thanks
- (void)viewDidLoad {
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(dismissKeyboard)];
tapGesture.cancelsTouchesInView = NO;
[_scrollView addGestureRecognizer:tapGesture];
}
-(void)dismissKeyboard {
[self.view endEditing:YES];
}
- (void)keyboardWasShown:(NSNotification *)notification {
scrollViewRect = _scrollView.contentOffset.y;
NSDictionary* info = [notification userInfo];
CGSize keyboardSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
keyboardSize.height += 10;
CGFloat viewBottom = CGRectGetMaxY(self.scrollView.frame);
if ([_itemNotes isFirstResponder]) {
CGFloat notesBottom = CGRectGetMaxY(_itemNotes.frame);
viewBottom -= notesBottom;
if (viewBottom < keyboardSize.height) {
keyboardSize.height -= viewBottom;
CGPoint scrollPoint = CGPointMake(0.0, keyboardSize.height);
[self.scrollView setContentOffset:scrollPoint animated:YES];
}
else {
[self.scrollView setContentOffset:CGPointZero animated:YES];
}
}
else {
[self.scrollView setContentOffset:CGPointZero animated:YES];
}
}
- (void)keyboardWillBeHidden:(NSNotification *)notification {
CGPoint scrollPoint = CGPointMake(0.0, scrollViewRect);
[self.scrollView setContentOffset:scrollPoint animated:YES];
}
EDIT:
So I figured out a solution but it seems like there must be a better way to handle this. The problem was because I was setting the contentOffset of the scrollView so that the contentSize was beyond the screen boundaries. Thus the first tap was moving the scrollView contentOffset back within the screen boundaries and the second was performing the tap gesture. I will post my solution below hoping that someone has a better answer.
I would recommend setting
_scrollView.layer.borderColor = [UIColor redColor].CGColor;
_scrollView.layer.borderWidth = 1;
This will show you exactly where your scrollview boundaries are, which may not be where you think they are, or may be covered by something else. Also, when I open the keyboard, I generally set the scrollview frame bottom to the top of the keyboard. Otherwise, you may have content below the keyboard you can't get to. Not sure if this is exactly related to your issues.
I am assuming there must be a better solution to this but I was able to solve the issue by extending the contentSize when the keyboard is displayed and then shrinking it back down when the keyboard is hidden.
Set a float (scrollViewHeight) to hold the original content size for the reset.
//add this right before setting the content offset
scrollViewHeight = _scrollView.contentSize.height;
_scrollView.contentSize = CGSizeMake(_scrollView.frame.size.width , scrollViewHeight + keyboardSize.height);
//add this right before reseting the content offset
_scrollView.contentSize = CGSizeMake(_scrollView.frame.size.width , scrollViewHeight);
It really seems like there must be a better way that I'm not aware of. I will have to go back through the documentation to see if there is another way.

UIView.animateWithDuration on UITextfield's contentoffset: It clips the text (Swift)

I use the following code:
UIView.animateWithDuration(30, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: {
self.textView.contentOffset = CGPointMake(0, 700)
}, completion: nil)
If the contentOffset is up to about 100, the animation works correctly and all text is visible. Everything higher than that leads to the disappearance of text at the beginning of the textlabel during the animation. The higher the contentOffset, the more text disappears during the animation. So I see white space for a while and then the remaining text comes into animation. Also: Once the animation is done, all text is visible again when I scroll up.
I have tried multiple UITextviews in various superviews. The general idea is a kind of Credits like animation where all text moves slowly upwards for about 30 seconds.
You can try looping small animations together like the following.
func animate(count:Int)
{
if (count > 100)
{
return
}
UIView.animateWithDuration(1, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: {
self.textView.contentOffset = CGPointMake(0, self.textView.contentOffset.y + 10)
}, completion: { finished in
dispatch_async(dispatch_get_main_queue(),{
self.animate(count+1)
})
})
}
This still appears to be an issue even in iOS 10 SDK.
To work around it, I manually animated the scrolling using setContentOffset:, delaying each step using performSelector: withObject: afterDelay: and implementing a method that scrolls one increment at a time (in my case 1/2 point).
To prevent users from manually overriding the scroll animation, you have to manually disable user interaction (animateWithDuration: automatically disables user scrolling while the animation is playing), you also must have scrolling enabled. I also disabled selection and editing, because that fit my use:
note: Objective-C code follows
textBox.editable = NO;
textBox.selectable = NO;
textBox.scrollEnabled = YES;
textBox.userInteractionEnabled = NO;
float contentLength = textBox.contentSize.height - textBox.frame.size.height;
float timePerLine = audioLength / contentLength;
for (float i = 0; i < contentLength; i+=0.5){
[self performSelector:#selector(scrollOneLine) withObject:nil afterDelay:timePerLine * i];
}
And then implemented a method to be used as a selector to scroll one increment at a time
-(void) scrollOneLine {
float currentPosition = textBox.contentOffset.y;
[textBox setContentOffset:CGPointMake(0, currentPosition + 0.5) animated:NO];
}

iCarousels Getting Stuck in iOS 7

I have implemented the iCarousels in my project. But after updating the app for iOS 7, my iCarousels are getting stuck in between. Its working fine in iOS 6 and 5. the problem in iOS 7 is that my scroll view below the iCarousel view is getting called first sometimes when I touch my carousel view. Can anyone help me out here?
Is the solution in the following method's return value:
- (CGFloat)carouselItemWidth:(iCarousel *)carousel
I tried many things here, and it works fine for few times, and again after some time it start getting stuck because the scroll view takes the touch from its subview(iCarousel view) and call its own delegate methods before the iCarousel's delegate method.
I am not using any gesture recognizer. I am using scroll view because i have iCarousel view and another view resting on UIScrollView, so that I can use pull to refresh as well.
The following delegate methods I am using, and changing in carouselItemWidth has decreased the stuck problem, but it still persists
- (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform
{
CGFloat tilt = 0.65f;
CGFloat spacing = 0.28f; // should be ~ 1/scrollSpeed;
CGFloat clampedOffset = fmaxf(-1.0f, fminf(1.5f, offset));
CGFloat itemWidth = 320;
CGFloat x = (clampedOffset * 0.5f * tilt + offset * spacing) * itemWidth;
CGFloat z = fabsf(clampedOffset) * -itemWidth * 0.5f;
transform = CATransform3DTranslate(transform, 0.0f, x, z);
transform = CATransform3DRotate(transform, -clampedOffset * M_PI_2 * tilt, -1.0f, 0.0f, 0.0f);
//DLog(#"offset: %f, %#", offset, [NSValue valueWithCATransform3D:transform]);
return transform;
}
- (NSUInteger)numberOfPlaceholdersInCarousel:(iCarousel *)carousel
{
//note: placeholder views are only displayed on some carousels if wrapping is disabled
return 0;
}
- (CGFloat)carouselItemWidth:(iCarousel *)carousel
{
//usually this should be slightly wider than the item views
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0)
{
return 270;
}
else
{
return 250;
}
}
- (BOOL)carouselShouldWrap:(iCarousel *)carousel
{
return NO;
}
The problem seems to be that when a scrollview receives a touch, it waits a second to see if it should handle it before passing it to the carousel.
You can (mostly) fix this by setting scrollView.delaysContentTouches = NO;
It's still a bit clunky if you try to swipe the carousel when the scrollview is moving/decelerating however. You will have to wait until it stops moving to interact with the carousel.
I'm investigating if there's a better way to fix this.
UPDATE:
I still don't have a proper general-purpose fix for this yet, but as a workaround, you can add this method to your local copy of iCarousel:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gesture shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return [gesture isKindOfClass:[UIPanGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]];
}
This forces iCarousel's pan gesture recogniser to take precedence over the one in the scrollView. If you combine this with the delaysContentTouches fix above, you shouldn't have any issues scrolling the carousel when it's inside a tableview or scrollview.

Resources