【问题标题】:How to expand and collapse NSSplitView subviews with animation?如何用动画展开和折叠 NSSplitView 子视图?
【发布时间】:2011-06-11 09:08:47
【问题描述】:

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

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

【问题讨论】:

    标签: cocoa macos animation nssplitview


    【解决方案1】:

    经过一番尝试,我找到了答案:是的,有可能。

    下面的代码显示了它是如何完成的。 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;
    }
    

    【讨论】:

    • 在非内存管理上下文中使用此代码的人请注意:NSViewAnimation 实例正在泄漏。
    • 在这个解决方案中扩展inspectorView分隔符后消失了。在 mainView 和 inspectorView 之间单击后,它变得可见。我认为它并没有随着观点而变化。你知道如何解决这个问题吗?
    【解决方案2】:

    这里有一个更简单的方法:

    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
    

    【讨论】:

      【解决方案3】:

      由于某种原因,我的滚动视图没有任何动画帧的方法。

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

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

      【讨论】:

        【解决方案4】:

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

        假设我有一个带有三个窗格的SplitViewleftPanemiddlePanerightPane。我不仅想折叠侧面的两个窗格,还想在某些视图进入或退出时动态调整各个窗格的宽度。

        在 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
            }
        }
        

        你可以在这个动画块中变得更复杂。例如,您可以决定要折叠右侧窗格,同时还要将中间的窗格放大到 500 像素。

        与此处列出的其他方法相比,这种方法的优势在于它会自动处理窗口框架当前不够大以容纳“展开”折叠窗格的情况。另外,您可以使用它以任何方式更改窗格的大小,而不仅仅是展开和折叠它们。您还可以让所有这些变化同时发生在一个流畅的组合动画中。


        注意事项:

        1. 显然构成leftPanemiddlePanerightPane 的视图永远不会改变。这些是您根据需要添加/删除其他视图的“容器”。如果您从 SplitView 中删除窗格视图,您将破坏您在 IB 中设置的约束。
        2. 使用 AutoLayout 时,如果您发现自己手动设置框架,那么您就是在与系统作斗争。你设置约束;自动布局引擎设置框架。
        3. -setPosition:ofDividerAtIndex: 方法在 splitView 不够大而无法将分隔线设置在您想要的位置时效果不佳。例如,如果您想取消折叠右侧窗格并为其提供500 宽度,但您的整个窗口当前只有300 宽度。如果您需要一次调整多个窗格的大小,这也会变得很麻烦。
        4. 您可以在此方法的基础上做更多事情。例如,也许您想为 splitView 中的各种窗格设置最小和最大宽度。使用约束执行此操作,然后根据需要更改 minmax 宽度约束的常量(可能当不同的视图进入每个窗格时等)。

        重要说明:

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

        【讨论】:

        • 处理一个稍微不同的问题,这个答案解决了它。谢谢。 (刚刚尝试过,它似乎也适用于堆栈视图。)
        【解决方案5】:

        macOS 10.11 的解决方案。

        要点

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

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

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

        因此,我们只需要在动画之前停用适当的约束并允许视图达到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)")
           }
        }
        

        【讨论】:

        • NSAnimationContext.runImplicitAnimations() 是什么?找不到任何关于它的信息。
        猜你喜欢
        • 2015-07-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-29
        • 1970-01-01
        • 2011-09-12
        • 1970-01-01
        • 2011-05-31
        相关资源
        最近更新 更多