How to collapse an NSSplitView pane with animation while using Auto Layout?

僤鯓⒐⒋嵵緔 提交于 2021-01-21 07:27:26

问题


I've tried everything I can think of, including all the suggestions I've found here on SO and on other mailing lists, but I cannot figure out how to programmatically collapse an NSSplitView pane with an animation while Auto Layout is on.

Here's what I have right now (written in Swift for fun), but it falls down in multiple ways:

@IBAction func toggleSourceList(sender: AnyObject?) {
    let isOpen = !splitView.isSubviewCollapsed(sourceList.view.superview!)
    let position = (isOpen ? 0 : self.lastWidth)

    if isOpen {
        self.lastWidth = sourceList.view.frame.size.width
    }

    NSAnimationContext.runAnimationGroup({ context in
        context.allowsImplicitAnimation = true
        context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
        context.duration = self.duration

        self.splitView.setPosition(position, ofDividerAtIndex: 0)
    }, completionHandler: { () -> Void in
    })
}

The desired behavior and appearance is that of Mail.app, which animates really nicely.

I have a full example app available at https://github.com/mdiep/NSSplitViewTest.


回答1:


Objective-C:

[[splitViewItem animator] setCollapse:YES]

Swift:

splitViewItem.animator().collapsed = true

From Apple’s help:

Whether or not the child ViewController corresponding to the SplitViewItem is collapsed in the SplitViewController. The default is NO. This can be set with the animator proxy to animate the collapse or uncollapse. The exact animation used can be customized by setting it in the -animations dictionary with a key of "collapsed". If this is set to YES before it is added to the SplitViewController, it will be initially collapsed and the SplitViewController will not cause the view to be loaded until it is uncollapsed. This is KVC/KVO compliant and will be updated if the value changes from user interaction.




回答2:


I was eventually able to figure this out with some help. I've transformed my test project into a reusable NSSplitView subclass: https://github.com/mdiep/MDPSplitView




回答3:


/// Collapse the sidebar
    func collapsePanel(_ number: Int = 0){
        guard number < self.splitViewItems.count else {
            return
        }
        let panel = self.splitViewItems[number]

        if panel.isCollapsed {
            panel.animator().isCollapsed = false
        } else {
            panel.animator().isCollapsed = true
        }

    }



回答4:


If you're using Auto-Layout and you want to animate some aspect of the view's dimensions/position, you might have more luck animating the constraints themselves. I've had a quick go with an NSSplitView but have so far only met with limited success. I can get a split to expand and collapse following a button push, but I've ended up having to try to hack my way around loads of other problems caused by interfering with the constraints. In case your unfamiliar with it, here's a simple constraint animation:

- (IBAction)animate:(NSButton *)sender {
    /* Shrink view to invisible */

    NSLayoutConstraint *constraint = self.viewWidthConstraint;
    [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {

        [[NSAnimationContext currentContext] setDuration:0.33];
        [[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];
        [[constraint animator] setConstant:0];

    } completionHandler:^{
        /* Do Some clean-up, if required */

    }];

Bear in mind you can only animate a constraints constant, you can't animate its priority.




回答5:


For some reason none of the methods of animating frames worked for my scrollview. I didn't try animating the constraints though.

I ended up creating a custom animation to animate the divider position. If anyone is interested, here is my solution:

Animation .h:

@interface MySplitViewAnimation : NSAnimation <NSAnimationDelegate>

@property (nonatomic, strong) NSSplitView* splitView;
@property (nonatomic) NSInteger dividerIndex;
@property (nonatomic) float startPosition;
@property (nonatomic) float endPosition;
@property (nonatomic, strong) void (^completionBlock)();

- (instancetype)initWithSplitView:(NSSplitView*)splitView
                   dividerAtIndex:(NSInteger)dividerIndex
                             from:(float)startPosition
                               to:(float)endPosition
                  completionBlock:(void (^)())completionBlock;
@end

Animation .m

@implementation MySplitViewAnimation

- (instancetype)initWithSplitView:(NSSplitView*)splitView
                   dividerAtIndex:(NSInteger)dividerIndex
                             from:(float)startPosition
                               to:(float)endPosition
                  completionBlock:(void (^)())completionBlock;
{
    if (self = [super init]) {
        self.splitView = splitView;
        self.dividerIndex = dividerIndex;
        self.startPosition = startPosition;
        self.endPosition = endPosition;
        self.completionBlock = completionBlock;

        [self setDuration:0.333333];
        [self setAnimationBlockingMode:NSAnimationNonblocking];
        [self setAnimationCurve:NSAnimationEaseIn];
        [self setFrameRate:30.0];
        [self setDelegate:self];
    }
    return self;
}

- (void)setCurrentProgress:(NSAnimationProgress)progress
{
    [super setCurrentProgress:progress];

    float newPosition = self.startPosition + ((self.endPosition - self.startPosition) * progress);

    [self.splitView setPosition:newPosition
               ofDividerAtIndex:self.dividerIndex];

    if (progress == 1.0) {
        self.completionBlock();
    }
}

@end

I'm using it like this - I have a 3 pane splitter view, and am moving the right pane in/out by a fixed amount (235).

- (IBAction)togglePropertiesPane:(id)sender
{
    if (self.rightPane.isHidden) {

        self.rightPane.hidden = NO;

        [[[MySplitViewAnimation alloc] initWithSplitView:_splitView
                                          dividerAtIndex:1
                                                  from:_splitView.frame.size.width  
                                                   to:_splitView.frame.size.width - 235                                                                                                             
                         completionBlock:^{
              ;
                                 }] startAnimation];
}
else {
    [[[MySplitViewAnimation alloc] initWithSplitView:_splitView
                                      dividerAtIndex:1                                                          
                                               from:_splitView.frame.size.width - 235
                                                 to:_splitView.frame.size.width
                                     completionBlock:^{          
        self.rightPane.hidden = YES;
                                     }] startAnimation];
    } 
}



回答6:


NSSplitViewItem (i.e. arranged subview of NSSplitView) can be fully collapsed, if it can reach Zero dimension (width or height). So, we just need to deactivate appropriate constrains before animation and allow view to reach Zero dimension. After animation we can activate needed constraints again.

See my comment for SO question How to expand and collapse NSSplitView subviews with animation?.




回答7:


This is a solution that doesn't require any subclasses or categories, works without NSSplitViewController (which requires macOS 10.10+), supports auto layout, animates the views, and works on macOS 10.8+.

As others have suggested, the solution is to use an NSAnimationContext, but the trick is to set context.allowsImplicitAnimation = YES (Apple docs). Then just set the divider position as one would normally.

#import <Quartz/Quartz.h>
#import <QuartzCore/QuartzCore.h>

- (IBAction)toggleLeftPane:(id)sender
{
    [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
        context.allowsImplicitAnimation = YES;
        context.duration = 0.25; // seconds
        context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];

        if ([self.splitView isSubviewCollapsed:self.leftPane]) {
            // -> expand
            [self.splitView setPosition:self.leftPane.frame.size.width ofDividerAtIndex:0];
        } else {
            // <- collapse
            _lastLeftPaneWidth = self.leftPane.frame.size.width;
            // optional: remember current width to restore to same size
            [self.splitView setPosition:0 ofDividerAtIndex:0];
        }

        [self.splitView layoutSubtreeIfNeeded];
    }];
}

Use auto layout to constrain the subviews (width, min/max sizes, etc.). Make sure to check "Core Animation Layer" in Interface Builder (i.e. set views to be layer backed) for the split view and all subviews — this is required for the transitions to be animated. (It will still work, but without animation.)

A full working project is available here: https://github.com/demitri/SplitViewAutoLayout.




回答8:


I will also add, because it took me quite a while to figure this out, that setting collapseBehavior = .useConstraints on your NSSplitViewItem (or items) may help immensely if you have lots of constraints defining the layouts of your subviews. My split view animations didn't look right until I did this. YMMV.



来源:https://stackoverflow.com/questions/26664759/how-to-collapse-an-nssplitview-pane-with-animation-while-using-auto-layout

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!