Как развернуть и свернуть подпредставления NSSplitView с анимацией? - PullRequest
14 голосов
/ 11 июня 2011

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

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

Ответы [ 5 ]

13 голосов
/ 11 июня 2011

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

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

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

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
2 голосов
/ 17 марта 2015

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

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

Анимация .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];
    } 
}
1 голос
/ 12 марта 2019

На это есть куча ответов. В 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, если вы устанавливаете кадры вручную, вы боретесь с системой. Вы устанавливаете ограничения; механизм автоматического размещения устанавливает кадры.
  3. Подход -setPosition:ofDividerAtIndex: не работает, когда splitView недостаточно велик, чтобы установить делитель там, где вы хотите его видеть. Например, если вы хотите, чтобы ООН свернула правую панель и присвоила ей 500 ширину, но все ваше окно в настоящее время имеет ширину 300. Это также может привести к путанице, если вам нужно изменить размер нескольких панелей одновременно.
  4. Вы можете использовать этот подход, чтобы делать больше. Например, может быть, вы хотите установить минимальную и максимальную ширину для различных панелей в splitView. Сделайте это с ограничениями, затем измените константы ограничения ширины min и max по мере необходимости (возможно, когда на каждой панели появляются разные представления и т. Д.).

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

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

0 голосов
/ 04 февраля 2018

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

enter image description here

...