同步两个NSScrollView

问题描述 投票:6回答:3

我阅读了文件Synchronizing Scroll Views,并完全按照文件进行,但有一个问题。

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

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

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

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

macos nsscrollview
3个回答
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属性并不相同,但从行为中可以清楚地看出它们不是。

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


0
投票

我有一个非常相似的问题。我有3个滚动视图来同步。一个只是水平滚动的标题。一个只能垂直滚动的侧栏。一个是标题下方和侧栏右侧的内容区域。标题和侧栏应随内容区域移动。如果滚动任一内容区域,内容区域应随标题或侧栏移动。

水平滚动从来都不是问题。垂直滚动总是导致两个视图滚动相反的方向。

我得到的奇怪的解决方案是创建一个clipView子类(我已经做了,因为你几乎总是需要如果你想要一些不开箱即用的好东西。)在clipView子类中,我添加了一个属性BOOL是inverted并且在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%的时间内工作,只会偶尔出现抖动。水平滚动同步没有问题。


0
投票

Swift 4版本在自动布局环境中使用文档视图。基于Apple文章Synchronizing Scroll Views,区别在于NSView.boundsDidChangeNotification在与其他滚动视图同步时临时忽略剪辑视图。隐藏垂直滚动条可重复使用类型InvisibleScroller

File 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.
   }
}

File: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
   }
}

File: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)
         }
      }
   }
}

File: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)
   }
}

结果:

Synchronised Scroll Views

© www.soinside.com 2019 - 2024. All rights reserved.