6

我阅读了文档Synchronizing Scroll Views,并且完全按照文档进行操作,但是存在一个问题。

我想同步一个 NSTableView 和一个 NSTextView。先让NSTableView监控NSTextView,当我滚动TextView的时候一切正常,但是当我尝试滚动TableView的时候,我发现TableView一开始会跳转到另一个地方(可能向后几行),然后从那个地方继续滚动.

即使在我让 TextView 监控 TableView 之后,这个问题仍然存在。

有谁知道有什么问题?我不能同步 TableView 和 TextView 吗?

编辑: 好的,现在我发现 TableView 会回到上次滚动后的位置。比如TableView的顶行是第10行,然后我滚动TextView,现在TableView的顶行是第20行,如果我再次滚动TableView,TableView会先回到第10行,然后开始滚动。

4

3 回答 3

3

我在对非常相似的情况(在 Lion 上)进行故障排除时遇到了这个确切的问题。我注意到这只发生在滚动条被隐藏时——但我证实它们仍然存在于笔尖中,并且仍然被正确实例化。

我什至确保打电话给-[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属性不等价,但从行为中可以清楚地看出它们不等价。

这不是最好的解决方案,但它是我想出的最简单的解决方法(到目前为止)。

于 2012-10-18T17:57:42.157 回答
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% 的时间里都有效,只有偶尔的抖动。水平滚动同步没有问题。

于 2015-02-03T01:17:37.253 回答
0

在自动布局环境中使用文档视图的 Swift 4 版本。基于 Apple 文章Synchronizing Scroll ViewsNSView.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)
   }
}

结果:

同步滚动视图

于 2019-02-24T16:50:58.320 回答