如何使用动画展开和折叠NSSplitView子视图?

时间:2011-06-11 09:08:47

标签: cocoa macos animation nssplitview

是否可以为NSSplitView子视图的折叠和展开设置动画? (我知道替代类的可用性,但更喜欢使用NSSplitView而不是动画。)

我使用- (void)setPosition:(CGFloat)position ofDividerAtIndex:(NSInteger)dividerIndex方法执行折叠和展开。

5 个答案:

答案 0 :(得分:13)

经过多次尝试后,我找到了答案:是的,这是可能的。

下面的代码展示了如何完成。 splitView是NSSplitView,它垂直分为mainView(左侧)和inspectorView(右侧)。 inspectorView是崩溃的那个。

- (IBAction)toggleInspector:(id)sender {
   if ([self.splitView isSubviewCollapsed:self.inspectorView]) {
        // NSSplitView hides the collapsed subview
        self.inspectorView.hidden = NO;

        NSMutableDictionary *expandMainAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
        [expandMainAnimationDict setObject:self.mainView forKey:NSViewAnimationTargetKey];
        NSRect newMainFrame = self.mainView.frame;
        newMainFrame.size.width =  self.splitView.frame.size.width-lastInspectorWidth;
        [expandMainAnimationDict setObject:[NSValue valueWithRect:newMainFrame] forKey:NSViewAnimationEndFrameKey];

        NSMutableDictionary *expandInspectorAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
        [expandInspectorAnimationDict setObject:self.inspectorView forKey:NSViewAnimationTargetKey];
        NSRect newInspectorFrame = self.inspectorView.frame;
        newInspectorFrame.size.width = lastInspectorWidth;
        newInspectorFrame.origin.x = self.splitView.frame.size.width-lastInspectorWidth;
        [expandInspectorAnimationDict setObject:[NSValue valueWithRect:newInspectorFrame] forKey:NSViewAnimationEndFrameKey];

        NSViewAnimation *expandAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects:expandMainAnimationDict, expandInspectorAnimationDict, nil]];
        [expandAnimation setDuration:0.25f];
        [expandAnimation startAnimation];
    } else {
        // Store last width so we can jump back
        lastInspectorWidth = self.inspectorView.frame.size.width;

        NSMutableDictionary *collapseMainAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
        [collapseMainAnimationDict setObject:self.mainView forKey:NSViewAnimationTargetKey];
        NSRect newMainFrame = self.mainView.frame;
        newMainFrame.size.width =  self.splitView.frame.size.width;
        [collapseMainAnimationDict setObject:[NSValue valueWithRect:newMainFrame] forKey:NSViewAnimationEndFrameKey];

        NSMutableDictionary *collapseInspectorAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
        [collapseInspectorAnimationDict setObject:self.inspectorView forKey:NSViewAnimationTargetKey];
        NSRect newInspectorFrame = self.inspectorView.frame;
        newInspectorFrame.size.width = 0.0f;
        newInspectorFrame.origin.x = self.splitView.frame.size.width;
        [collapseInspectorAnimationDict setObject:[NSValue valueWithRect:newInspectorFrame] forKey:NSViewAnimationEndFrameKey];

        NSViewAnimation *collapseAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects:collapseMainAnimationDict, collapseInspectorAnimationDict, nil]];
        [collapseAnimation setDuration:0.25f];
        [collapseAnimation startAnimation];
    }
}

- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview {
    BOOL result = NO;
    if (splitView == self.splitView && subview == self.inspectorView) {
        result = YES;
    }
    return result;
}

- (BOOL)splitView:(NSSplitView *)splitView shouldCollapseSubview:(NSView *)subview forDoubleClickOnDividerAtIndex:(NSInteger)dividerIndex {
    BOOL result = NO;
    if (splitView == self.splitView && subview == self.inspectorView) {
        result = YES;
    }
    return result;
}

答案 1 :(得分:8)

这是一个更简单的方法:

http://www.cocoabuilder.com/archive/cocoa/304317-animating-nssplitpane-position.html

(死亡链接,新链接here。)

其中说如下在NSSplitView上创建一个类别,然后使用

进行动画处理
[[splitView animator] setSplitPosition:pos];

适合我。

类别:

@implementation NSSplitView (Animation)

+ (id)defaultAnimationForKey:(NSString *)key
{
    if ([key isEqualToString:@"splitPosition"])
    {
        CAAnimation* anim = [CABasicAnimation animation];
        anim.duration = 0.3;
        return anim;
    }
    else
    {
        return [super defaultAnimationForKey:key];
    }
}

- (void)setSplitPosition:(CGFloat)position
{
    [self setPosition:position ofDividerAtIndex:0];
}

- (CGFloat)splitPosition
{
    NSRect frame = [[[self subviews] objectAtIndex:0] frame];

    if([self isVertical])
        return NSMaxX(frame);
    else
        return NSMaxY(frame);
}

@end

答案 2 :(得分:2)

由于某种原因,没有任何动画帧的方法适用于我的scrollview。

我最终创建了一个自定义动画来为分隔符位置设置动画。最终花费的时间比我预期的少。如果有人有兴趣,这是我的解决方案:

动画.h:

@interface MySplitViewAnimation : NSAnimation

@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

动画.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];
    }
    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

我这样使用它 - 我有一个3窗格分割器视图,并且正在以固定数量(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];
    } 
}

答案 3 :(得分:1)

对此有很多答案。在2019年,最好的方法是在SplitView窗格上建立约束,然后为约束设置动画。

假设我有一个SplitView,其中包含三个窗格:leftPanemiddlePanerightPane。我不仅要折叠侧面的两个窗格,还想在某些视图进入或离开时动态调整各个窗格的宽度。

在IB中,我为三个窗格中的每个窗格设置了WIDTH约束。 leftPanerightPane的宽度设置为250,优先级为1000 (required)

在代码中,它看起来像这样:

@class MyController: NSViewController
{
    @IBOutlet var splitView: NSSplitView!

    @IBOutlet var leftPane: NSView!
    @IBOutlet var middlePane: NSView!
    @IBOutlet var rightPane: NSView!

    @IBOutlet var leftWidthConstraint: NSLayoutConstraint!
    @IBOutlet var middleWidthConstraint: NSLayoutConstraint!
    @IBOutlet var rightWidthConstraint: NSLayoutConstraint!


    override func awakeFromNib() {
        // We use these in our animation, but want them off normally so the panes
        // can be resized as normal via user drags, window changes, etc.
        leftWidthConstraint.isActive = false
        middleWidthConstraint.isActive = false
        rightWidthConstraint.isActive = false
    }


    func collapseRightPane() 
    {
        NSAnimationContext.runAnimationGroup({ (context) in

            context.allowsImplicitAnimation = true
            context.duration = 0.15

            rightWidthConstraint.constant = 0
            rightWidthConstraint.isActive = true

            // Critical! Call this in the animation block or you don't get animated changes:
            splitView.layoutSubtreeIfNeeded()

        }) { [unowned self] in

            // We need to tell the splitView to re-layout itself before we can
            // remove the constraint, or it jumps back to how it was before animating.
            // This process tells the layout engine to recalculate and update
            // the frames of everything based on current constraints:
            self.splitView.needsLayout = true
            self.splitView.needsUpdateConstraints = true
            self.splitView.needsDisplay = true

            self.splitView.layoutSubtreeIfNeeded()
            self.splitView.displayIfNeeded()

            // Now, disable the width constraint so we can resize the splitView
            // via mouse, etc:
            self.middleWidthConstraint.isActive = false
        }
    }
}


extension MyController: NSSplitViewDelegate
{
    final func splitView(_ splitView: NSSplitView, canCollapseSubview subview: NSView) -> Bool
    {
        // Allow collapsing. You might set an iVar that you can control
        // if you don't want the user to be able to drag-collapse. Set the
        // ivar to false usually, but set it to TRUE in the animation block
        // block, before changing the constraints, then back to false in
        // in the animation completion handler.
        return true
    }


    final func splitView(_ splitView: NSSplitView, shouldHideDividerAt dividerIndex: Int) -> Bool {
        // Definitely do this. Nobody wants a crappy divider hanging out
        // on the side of a collapsed pane.
        return true
    }
}

在此动画块中,您可以变得更加复杂。例如,您可以决定要折叠右窗格,但同时将中间的窗格放大到500px。

此方法相对于此处列出的其他方法的优点在于,它将自动处理当前窗口框不足以容纳“展开”折叠窗格的情况。另外,您可以使用此方法以任何方式更改窗格的大小,而不仅仅是扩展和折叠它们。您还可以使用平滑的组合动画一次完成所有这些更改。


注意:

  1. 显然,组成leftPanemiddlePanerightPane的视图永远不会改变。这些是“容器”,您可以根据需要在其中添加/删除其他视图。如果从SplitView中删除窗格视图,则将破坏在IB中设置的约束。
  2. 使用“自动版式”时,如果发现自己手动设置框架,则说明您正在使用该系统。您设置约束;自动布局引擎设置框架。
  3. 当splitView的大小不足以将分隔符设置在所需的位置时,-setPosition:ofDividerAtIndex:方法将无法正常工作。例如,如果要取消折叠右侧窗格并将其宽度设为500,但当前整个窗口的宽度仅为300。如果您需要一次调整多个窗格的大小,这也会变得很混乱。
  4. 您可以在此方法的基础上做更多的事情。例如,也许您想为splitView中的各个窗格设置最小和最大宽度。使用约束来执行此操作,然后根据需要更改minmax宽度约束的常量(也许当不同的视图进入每个窗格时,等等)。

重要提示:

如果窗格之一中的任何子视图具有width优先级的minimumWidth1000约束,则此方法将失败。您将在日志中收到“无法满足约束”的通知。您需要确保您的子视图(及其子视图,一直到层次结构的下半部)没有将宽度限制设置为1000优先级。对于此类约束,请使用999或更少的值,以便splitView始终可以覆盖它们以折叠视图。

答案 4 :(得分:0)

macOS 10.11的解决方案。

要点

    如果没有明确设置,
  1. NSSplitViewItem.minimumThickness取决于NSSplitViewItem .viewController.view宽度/高度。

  2. NSSplitViewItem .viewController.view宽度/高度取决于明确添加的约束。

  3. NSSplitViewItem(即排列为NSSplitView的子视图)可以完全折叠,如果它可以达到Zero尺寸(宽度或高度)。

  4. 因此,我们只需要在动画之前停用适当的约束,并允许视图达到Zero维度。在动画之后,我们只需要激活所需的约束。

    class SplitViewAnimationsController: ViewController {
    
       private lazy var toolbarView = StackView().autolayoutView()
       private lazy var revealLeftViewButton = Button(title: "Left").autolayoutView()
       private lazy var changeSplitOrientationButton = Button(title: "Swap").autolayoutView()
       private lazy var revealRightViewButton = Button(title: "Right").autolayoutView()
    
       private lazy var splitViewController = SplitViewController()
    
       private lazy var viewControllerLeft = ContentViewController()
       private lazy var viewControllerRight = ContentViewController()
       private lazy var splitViewItemLeft = NSSplitViewItem(viewController: viewControllerLeft)
       private lazy var splitViewItemRight = NSSplitViewItem(viewController: viewControllerRight)
    
       private lazy var viewLeftWidth = viewControllerLeft.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
       private lazy var viewRightWidth = viewControllerRight.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
       private lazy var viewLeftHeight = viewControllerLeft.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 40)
       private lazy var viewRightHeight = viewControllerRight.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 40)
       private lazy var equalHeight = viewControllerLeft.view.heightAnchor.constraint(equalTo: viewControllerRight.view.heightAnchor, multiplier: 1)
       private lazy var equalWidth = viewControllerLeft.view.widthAnchor.constraint(equalTo: viewControllerRight.view.widthAnchor, multiplier: 1)
    
       override func loadView() {
          super.loadView()
          splitViewController.addSplitViewItem(splitViewItemLeft)
          splitViewController.addSplitViewItem(splitViewItemRight)
          contentView.addSubviews(toolbarView, splitViewController.view)
          addChildViewController(splitViewController)
    
          toolbarView.addArrangedSubviews(revealLeftViewButton, changeSplitOrientationButton, revealRightViewButton)
       }
    
       override func viewDidAppear() {
          super.viewDidAppear()
          splitViewController.contentView.setPosition(contentView.bounds.width * 0.5, ofDividerAt: 0)
       }
    
       override func setupDefaults() {
          setIsVertical(true)
       }
    
       override func setupHandlers() {
          revealLeftViewButton.setHandler { [weak self] in guard let this = self else { return }
             self?.revealOrCollapse(this.splitViewItemLeft)
          }
          revealRightViewButton.setHandler { [weak self] in guard let this = self else { return }
             self?.revealOrCollapse(this.splitViewItemRight)
          }
          changeSplitOrientationButton.setHandler { [weak self] in guard let this = self else { return }
             self?.setIsVertical(!this.splitViewController.contentView.isVertical)
          }
       }
    
       override func setupUI() {
    
          splitViewController.view.translatesAutoresizingMaskIntoConstraints = false
          splitViewController.contentView.dividerStyle = .thin
          splitViewController.contentView.setDividerThickness(2)
          splitViewController.contentView.setDividerColor(.green)
    
          viewControllerLeft.contentView.backgroundColor = .red
          viewControllerRight.contentView.backgroundColor = .blue
          viewControllerLeft.contentView.wantsLayer = true
          viewControllerRight.contentView.wantsLayer = true
    
          splitViewItemLeft.canCollapse = true
          splitViewItemRight.canCollapse = true
    
          toolbarView.distribution = .equalSpacing
       }
    
       override func setupLayout() {
          var constraints: [NSLayoutConstraint] = []
    
          constraints += LayoutConstraint.Pin.InSuperView.horizontally(toolbarView, splitViewController.view)
          constraints += [
             splitViewController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
             toolbarView.topAnchor.constraint(equalTo: splitViewController.view.bottomAnchor),
             toolbarView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
          ]
    
          constraints += [viewLeftWidth, viewLeftHeight, viewRightWidth, viewRightHeight]
          constraints += [toolbarView.heightAnchor.constraint(equalToConstant: 48)]
    
          NSLayoutConstraint.activate(constraints)
       }
    }
    
    extension SplitViewAnimationsController {
    
       private enum AnimationType: Int {
          case noAnimation, `default`, rightDone
       }
    
       private func setIsVertical(_ isVertical: Bool) {
          splitViewController.contentView.isVertical = isVertical
          equalHeight.isActive = isVertical
          equalWidth.isActive = !isVertical
       }
    
       private func revealOrCollapse(_ item: NSSplitViewItem) {
    
          let constraintToDeactivate: NSLayoutConstraint
          if splitViewController.splitView.isVertical {
             constraintToDeactivate = item.viewController == viewControllerLeft ? viewLeftWidth : viewRightWidth
          } else {
             constraintToDeactivate = item.viewController == viewControllerLeft ? viewLeftHeight : viewRightHeight
          }
    
          let animationType: AnimationType = .rightDone
    
          switch animationType {
          case .noAnimation:
             item.isCollapsed = !item.isCollapsed
          case .default:
             item.animator().isCollapsed = !item.isCollapsed
          case .rightDone:
             let isCollapsedAnimation = CABasicAnimation()
             let duration: TimeInterval = 3 // 0.15
             isCollapsedAnimation.duration = duration
             item.animations = [NSAnimatablePropertyKey("collapsed"): isCollapsedAnimation]
             constraintToDeactivate.isActive = false
             setActionsEnabled(false)
             NSAnimationContext.runImplicitAnimations(duration: duration, animations: {
                item.animator().isCollapsed = !item.isCollapsed
             }, completion: {
                constraintToDeactivate.isActive = true
                self.setActionsEnabled(true)
             })
          }
       }
    
       private func setActionsEnabled(_ isEnabled: Bool) {
          revealLeftViewButton.isEnabled = isEnabled
          revealRightViewButton.isEnabled = isEnabled
          changeSplitOrientationButton.isEnabled = isEnabled
       }
    }
    
    class ContentViewController: ViewController {
    
       override func viewDidLayout() {
          super.viewDidLayout()
          print("frame: \(view.frame)")
       }
    }
    

    enter image description here