14

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

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

4

5 回答 5

14

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

下面的代码显示了它是如何完成的。是splitViewNSSplitView,它垂直分为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;
}
于 2011-06-11T10:52:02.400 回答
8

这是一个更简单的方法:

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

(上面的链接死了,这里有新的链接。)

其中说在 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
于 2014-04-24T18:02:57.380 回答
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];
    } 
}
于 2015-03-17T20:07:29.037 回答
3

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

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

在 IB 中,我为三个窗格中的每一个设置了 WIDTH 约束。leftPane并将rightPane宽度设置为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宽度约束的常量(也许当不同的视图进入每个窗格时等)。

重要说明:

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

于 2019-03-11T23:41:28.033 回答
1

macOS 10.11 的解决方案。

要点

  1. NSSplitViewItem.minimumThicknessNSSplitViewItem .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)")
   }
}

在此处输入图像描述

于 2018-02-04T12:39:55.787 回答