是否可以动画 NSSplitView 子视图的折叠和展开?(我知道替代类的可用性,但更喜欢使用 NSSplitView 而不是动画。)
我正在使用该方法- (void)setPosition:(CGFloat)position ofDividerAtIndex:(NSInteger)dividerIndex
来执行折叠和展开。
是否可以动画 NSSplitView 子视图的折叠和展开?(我知道替代类的可用性,但更喜欢使用 NSSplitView 而不是动画。)
我正在使用该方法- (void)setPosition:(CGFloat)position ofDividerAtIndex:(NSInteger)dividerIndex
来执行折叠和展开。
经过一番尝试,我找到了答案:是的,有可能。
下面的代码显示了它是如何完成的。是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
由于某种原因,动画帧的方法都不适用于我的滚动视图。
我最终创建了一个自定义动画来为分隔线位置设置动画。这最终花费的时间比我预期的要少。如果有人有兴趣,这是我的解决方案:
动画.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];
}
}
对此有很多答案。在 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
}
}
您可以在此动画块中变得更复杂。例如,您可以决定要折叠右侧窗格,但同时将中间的窗格放大到 500 像素。
与此处列出的其他方法相比,此方法的优势在于,它将自动处理窗口框架当前不够大以容纳“展开”折叠窗格的情况。另外,您可以使用它以任何方式更改窗格的大小,而不仅仅是展开和折叠它们。您还可以让所有这些更改同时发生在平滑的组合动画中。
leftPane
、middlePane
和rightPane
永远不会改变的观点。这些是您根据需要添加/删除其他视图的“容器”。如果您从 SplitView 中删除窗格视图,您将破坏您在 IB 中设置的约束。-setPosition:ofDividerAtIndex:
splitView 不够大以将分隔线设置在您想要的位置时,该方法效果不佳。例如,如果您想取消折叠右侧窗格并为其指定500
宽度,但您的整个窗口当前只是300
宽度。如果您需要一次调整多个窗格的大小,这也会变得混乱。min
和max
宽度约束的常量(也许当不同的视图进入每个窗格时等)。如果其中一个窗格中的任何子视图具有优先级为 的width
orminimumWidth
约束,则此方法将失败1000
。您将在日志中收到“无法满足约束”通知。您需要确保您的子视图(及其子视图,一直沿层次结构向下)没有1000
优先设置宽度约束。对此类约束使用 999 或更少,以便 splitView 始终可以覆盖它们以折叠视图。
macOS 10.11 的解决方案。
要点:
NSSplitViewItem.minimumThickness
NSSplitViewItem .viewController.view
如果未明确设置,则取决于宽度/高度。
NSSplitViewItem .viewController.view
宽度/高度取决于显式添加的约束。
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)")
}
}