Как развернуть и свернуть подпредставления NSSplitView с анимацией?

Можно ли анимировать сворачивание и расширение подпредставлений NSSplitView? (Я знаю о наличии альтернативных классов, но предпочел бы использовать NSSplitView, а не анимации.)

Я использую метод - (void)setPosition:(CGFloat)position ofDividerAtIndex:(NSInteger)dividerIndex выполнить разрушение и расширение.

4 ответа

Решение

После еще нескольких попыток я нашел ответ: да, это возможно.

Код ниже показывает, как это можно сделать. 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;
}

Вот более простой метод:

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

Есть куча ответов на это. В 2019 году лучший способ сделать это - установить ограничения на панели SplitView, а затем анимировать ограничения.

Предположим, у меня есть SplitView с тремя панелями: leftPane, middlePane, rightPane, Я хочу не просто сворачивать две панели сбоку, я также хочу динамически изменять размеры ширины различных панелей при входе или выходе определенных видов.

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

Вы можете усложнить этот анимационный блок. Например, вы можете решить, что хотите свернуть правую панель, но одновременно увеличить среднюю до 500px.

Преимущество этого подхода перед другими, перечисленными здесь, заключается в том, что он будет автоматически обрабатывать случаи, когда рамка окна в настоящее время недостаточно велика, чтобы вместить "расширение" свернутой панели. Кроме того, вы можете использовать это, чтобы изменять размеры панелей ЛЮБЫМ способом, а не просто расширять и сворачивать их. Вы также можете сделать так, чтобы все эти изменения происходили одновременно в плавной комбинированной анимации.


Заметки:

  1. Очевидно, что мнения, которые составляют leftPane, middlePane, а также rightPane никогда не меняется. Это "контейнеры", в которые вы добавляете / удаляете другие представления по мере необходимости. Если вы удалите виды панели из SplitView, вы уничтожите ограничения, установленные в IB.
  2. При использовании AutoLayout, если вы устанавливаете кадры вручную, вы боретесь с системой. Вы устанавливаете ограничения; движок autolayout устанавливает кадры.
  3. -setPosition:ofDividerAtIndex: Подход не работает хорошо, когда splitView не достаточно велик, чтобы установить делитель там, где вы хотите его видеть. Например, если вы хотите, чтобы ООН свернула правую панель и дала ей 500 ширина, но все ваше окно в настоящее время просто 300 широкий. Это также запутывает, если вам нужно изменить размер нескольких панелей одновременно.
  4. Вы можете использовать этот подход, чтобы сделать больше. Например, может быть, вы хотите установить минимальную и максимальную ширину для различных панелей в splitView. Сделайте это с ограничениями, затем измените константы min а также max ограничение ширины по мере необходимости (возможно, когда разные виды появляются на каждой панели и т. д.).

КРИТИЧЕСКОЕ ПРИМЕЧАНИЕ:

Этот подход потерпит неудачу, если какое-либо подпредставление в одной из панелей имеет width или же minimumWidth ограничение, которое имеет приоритет 1000, Вы получите уведомление "Не удается удовлетворить ограничения" в журнале. Вам нужно убедиться, что ваши подпредставления (и их дочерние представления, вплоть до иерархии) не имеют ограничения ширины, установленного в 1000 приоритет. Используйте 999 или меньше для таких ограничений, чтобы splitView всегда мог их переопределить, чтобы свернуть представление.

По какой-то причине ни один из методов анимации кадров не работал для моего прокрутки.

В итоге я создал собственную анимацию для анимации положения делителя. Это заняло меньше времени, чем я ожидал. Если кому-то интересно, вот мое решение:

Анимация.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

Анимация

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

Решение для 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)")
   }
}

введите описание изображения здесь

Другие вопросы по тегам