我阅读了文档Synchronizing Scroll Views,并且与文档完全相同,但有一个问题。
我想同步NSTableView和NSTextView。首先让NSTableView监视NSTextView,当我滚动TextView时一切正常,但是当我尝试滚动TableView时,我发现TableView将首先跳转到另一个地方(可能是向后几行),然后继续从那个地方滚动
即使我让TextView监视TableView,这个问题仍然存在。
任何人都知道这是什么问题?我无法同步TableView和TextView?
编辑: 好的,现在我发现TableView将自上次滚动后返回到该位置。例如,TableView的第一行是第10行,然后我滚动TextView,现在TableView的第一行是第20行,如果我再次滚动TableView,TableView将首先返回第10行,然后开始滚动。
答案 0 :(得分:3)
我在解决非常类似的情况时遇到了这个问题(在Lion上)。我注意到这只会在隐藏滚动条时发生 - 但我确认它们仍然存在于nib中,并且仍然可以正确实例化。
我甚至确保打电话给-[NSScrollView reflectScrolledClipView:]
,但这并没有什么不同。看起来这似乎是NSScrollView中的一个错误。
无论如何,我能够通过创建自定义滚动类来解决这个问题。我所要做的就是覆盖以下类方法:
+ (BOOL)isCompatibleWithOverlayScrollers
{
// Let this scroller sit on top of the content view, rather than next to it.
return YES;
}
- (void)setHidden:(BOOL)flag
{
// Ugly hack: make sure we are always hidden.
[super setHidden:YES];
}
然后,我允许滚动器在Interface Builder中“可见”。但是,由于它们隐藏自己,因此它们不会出现在屏幕上,用户也无法点击它们。令人惊讶的是,IB设置和hidden
属性并不相同,但从行为中可以看出它们并非如此。
这不是最好的解决方案,但这是我提出的最简单的解决方法(到目前为止)。
答案 1 :(得分:0)
我有一个非常相似的问题。 我有3个滚动视图来同步。 一个只是水平滚动的标题。 一个只能垂直滚动的侧栏。 一个是标题下方和侧栏右侧的内容区域。 标题和侧栏应随内容区域移动。 如果滚动了内容区域,内容区域应随标题或侧栏移动。
水平滚动从来都不是问题。 垂直滚动总是导致两个视图滚动相反的方向。
我得到的奇怪的解决方案是创建一个clipView子类(我已经做过了,因为如果你想要一些不开箱即用的东西,你几乎总是需要它。) 在clipView子类中,我添加了一个属性BOOL isInverted,并在重写的isFlipped中返回self.isInverted。
奇怪的是,翻转的这些BOOL值从一开始就在所有3个视图中设置和匹配。 滚动机械似乎确实是马车。 我偶然发现的解决方法是将调用之间的滚动同步代码夹在中,以便将侧边栏和内容视图设置为未折叠,然后更新任何垂直滚动,然后再次设置两次翻转。 必须是滚动机器中的一些老化代码试图支持反向滚动...
这些是NSNotificationCenter addObserver方法调用的方法,用于观察clipViews的NSViewBoundsDidChangeNotification。
- (void)synchWithVerticalControlClipView:(NSNotification *)aNotification
{
NSPoint mouseInWindow = self.view.window.currentEvent.locationInWindow;
NSPoint converted = [self.verticalControl.enclosingScrollView convertPoint:mouseInWindow fromView:nil];
if (!NSPointInRect(converted, self.verticalControl.enclosingScrollView.bounds)) {
return;
}
[self.contentGridClipView setIsInverted:NO];
[self.verticalControlClipView setIsInverted:NO];
// ONLY update the contentGrid view.
NSLog(@"%@", NSStringFromSelector(_cmd));
NSPoint changedBoundsOrigin = self.verticalControlClipView.documentVisibleRect.origin;
NSPoint currentOffset = self.contentGridClipView.bounds.origin;
NSPoint newOffset = currentOffset;
newOffset.y = changedBoundsOrigin.y;
NSLog(@"\n changedBoundsOrigin=%@\n currentOffset=%@\n newOffset=%@", NSStringFromPoint(changedBoundsOrigin), NSStringFromPoint(currentOffset), NSStringFromPoint(newOffset));
[self.contentGridClipView scrollToPoint:newOffset];
[self.contentGridClipView.enclosingScrollView reflectScrolledClipView:self.contentGridClipView];
[self.contentGridClipView setIsInverted:YES];
[self.verticalControlClipView setIsInverted:YES];
}
- (void)synchWithContentGridClipView:(NSNotification *)aNotification
{
NSPoint mouseInWindow = self.view.window.currentEvent.locationInWindow;
NSPoint converted = [self.contentGridView.enclosingScrollView convertPoint:mouseInWindow fromView:nil];
if (!NSPointInRect(converted, self.contentGridView.enclosingScrollView.bounds)) {
return;
}
[self.contentGridClipView setIsInverted:NO];
[self.verticalControlClipView setIsInverted:NO];
// Update BOTH the control views.
NSLog(@"%@", NSStringFromSelector(_cmd));
NSPoint changedBoundsOrigin = self.contentGridClipView.documentVisibleRect.origin;
NSPoint currentHOffset = self.horizontalControlClipView.documentVisibleRect.origin;
NSPoint currentVOffset = self.verticalControlClipView.documentVisibleRect.origin;
NSPoint newHOffset, newVOffset;
newHOffset = currentHOffset;
newVOffset = currentVOffset;
newHOffset.x = changedBoundsOrigin.x;
newVOffset.y = changedBoundsOrigin.y;
[self.horizontalControlClipView scrollToPoint:newHOffset];
[self.verticalControlClipView scrollToPoint:newVOffset];
[self.horizontalControlClipView.enclosingScrollView reflectScrolledClipView:self.horizontalControlClipView];
[self.verticalControlClipView.enclosingScrollView reflectScrolledClipView:self.verticalControlClipView];
[self.contentGridClipView setIsInverted:YES];
[self.verticalControlClipView setIsInverted:YES];
}
这种方法在99%的时间内都有效,偶尔会有抖动。 水平滚动同步没有问题。
答案 2 :(得分:0)
Swift 4版本,该版本在自动布局环境中使用文档视图。
基于Apple文章Synchronizing Scroll Views,不同之处在于在同步到其他滚动视图时,剪辑视图上的NSView.boundsDidChangeNotification
会暂时被忽略。
要隐藏垂直滚动条的可重用类型,请使用InvisibleScroller
。
文件 SynchronedScrollViewController.swift –具有两个滚动视图的视图控制器。
class SynchronedScrollViewController: ViewController {
private lazy var leftView = TestView().autolayoutView()
private lazy var rightView = TestView().autolayoutView()
private lazy var leftScrollView = ScrollView(horizontallyScrolledDocumentView: leftView).autolayoutView()
private lazy var rightScrollView = ScrollView(horizontallyScrolledDocumentView: rightView).autolayoutView()
override func setupUI() {
view.addSubviews(leftScrollView, rightScrollView)
leftView.backgroundColor = .red
rightView.backgroundColor = .blue
contentView.backgroundColor = .green
leftScrollView.verticalScroller = InvisibleScroller()
leftView.setIntrinsicContentSize(CGSize(intrinsicHeight: 720)) // Some fake height
rightView.setIntrinsicContentSize(CGSize(intrinsicHeight: 720)) // Some fake height
}
override func setupHandlers() {
(leftScrollView.contentView as? ClipView)?.onBoundsDidChange = { [weak self] in
print("\(Date().timeIntervalSinceReferenceDate) : Left scroll view changed")
self?.syncScrollViews(origin: $0)
}
(rightScrollView.contentView as? ClipView)?.onBoundsDidChange = { [weak self] in
print("\(Date().timeIntervalSinceReferenceDate) : Right scroll view changed.")
self?.syncScrollViews(origin: $0)
}
}
override func setupLayout() {
LayoutConstraint.pin(to: .vertically, leftScrollView, rightScrollView).activate()
LayoutConstraint.withFormat("|[*(==40)]-[*]|", leftScrollView, rightScrollView).activate()
}
private func syncScrollViews(origin: NSClipView) {
// See also:
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/NSScrollViewGuide/Articles/SynchroScroll.html
let changedBoundsOrigin = origin.documentVisibleRect.origin
let targetScrollView = leftScrollView.contentView == origin ? rightScrollView : leftScrollView
let curOffset = targetScrollView.contentView.bounds.origin
var newOffset = curOffset
newOffset.y = changedBoundsOrigin.y
if curOffset != changedBoundsOrigin {
(targetScrollView.contentView as? ClipView)?.scroll(newOffset, shouldNotifyBoundsChange: false)
targetScrollView.reflectScrolledClipView(targetScrollView.contentView)
}
}
}
文件: TestView.swift –测试视图。每20点画一条线。
class TestView: View {
override init() {
super.init()
setIsFlipped(true)
}
override func setupLayout() {
needsDisplay = true
}
required init?(coder decoder: NSCoder) {
fatalError()
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else {
return
}
context.saveGraphicsState()
let cgContext = context.cgContext
cgContext.setStrokeColor(NSColor.white.cgColor)
for x in stride(from: CGFloat(20), through: bounds.height, by: 20) {
cgContext.addLines(between: [CGPoint(x: 0, y: x), CGPoint(x: bounds.width, y: x)])
NSString(string: "\(Int(x))").draw(at: CGPoint(x: 0, y: x), withAttributes: nil)
}
cgContext.strokePath()
context.restoreGraphicsState()
}
}
文件: NSScrollView.swift -可重用的扩展名。
extension NSScrollView {
public convenience init(documentView view: NSView) {
let frame = CGRect(dimension: 10) // Some dummy non zero value
self.init(frame: frame)
let clipView = ClipView(frame: frame)
clipView.documentView = view
clipView.autoresizingMask = [.height, .width]
contentView = clipView
view.frame = frame
view.translatesAutoresizingMaskIntoConstraints = true
view.autoresizingMask = [.width, .height]
}
public convenience init(horizontallyScrolledDocumentView view: NSView) {
self.init(documentView: view)
contentView.setIsFlipped(true)
view.translatesAutoresizingMaskIntoConstraints = false
LayoutConstraint.pin(in: contentView, to: .horizontally, view).activate()
view.topAnchor.constraint(equalTo: contentView.topAnchor).activate()
hasVerticalScroller = true // Without this scroll might not work properly. Seems Apple bug.
}
}
文件: InvisibleScroller.swift -可重复使用的不可见滚动条。
// Disabling scroll view indicators.
// See: https://stackoverflow.com/questions/9364953/hide-scrollers-while-leaving-scrolling-itself-enabled-in-nsscrollview
public class InvisibleScroller: Scroller {
public override class var isCompatibleWithOverlayScrollers: Bool {
return true
}
public override class func scrollerWidth(for controlSize: NSControl.ControlSize, scrollerStyle: NSScroller.Style) -> CGFloat {
return CGFloat.leastNormalMagnitude // Dimension of scroller is equal to `FLT_MIN`
}
public override func setupUI() {
// Below assignments not really needed, but why not.
scrollerStyle = .overlay
alphaValue = 0
}
}
文件: ClipView.swift -NSClipView的自定义子类。
open class ClipView: NSClipView {
public var onBoundsDidChange: ((NSClipView) -> Void)? {
didSet {
setupBoundsChangeObserver()
}
}
private var boundsChangeObserver: NotificationObserver?
private var mIsFlipped: Bool?
open override var isFlipped: Bool {
return mIsFlipped ?? super.isFlipped
}
// MARK: -
public func setIsFlipped(_ value: Bool?) {
mIsFlipped = value
}
open func scroll(_ point: NSPoint, shouldNotifyBoundsChange: Bool) {
if shouldNotifyBoundsChange {
scroll(to: point)
} else {
boundsChangeObserver?.isActive = false
scroll(to: point)
boundsChangeObserver?.isActive = true
}
}
// MARK: - Private
private func setupBoundsChangeObserver() {
postsBoundsChangedNotifications = onBoundsDidChange != nil
boundsChangeObserver = nil
if postsBoundsChangedNotifications {
boundsChangeObserver = NotificationObserver(name: NSView.boundsDidChangeNotification, object: self) { [weak self] _ in
guard let this = self else { return }
self?.onBoundsDidChange?(this)
}
}
}
}
文件: NotificationObserver.swift –可重用的通知观察器。
public class NotificationObserver: NSObject {
public typealias Handler = ((Foundation.Notification) -> Void)
private var notificationObserver: NSObjectProtocol!
private let notificationObject: Any?
public var handler: Handler?
public var isActive: Bool = true
public private(set) var notificationName: NSNotification.Name
public init(name: NSNotification.Name, object: Any? = nil, queue: OperationQueue = .main, handler: Handler? = nil) {
notificationName = name
notificationObject = object
self.handler = handler
super.init()
notificationObserver = NotificationCenter.default.addObserver(forName: name, object: object, queue: queue) { [weak self] in
guard let this = self else { return }
if this.isActive {
self?.handler?($0)
}
}
}
deinit {
NotificationCenter.default.removeObserver(notificationObserver, name: notificationName, object: notificationObject)
}
}
结果: