【问题标题】:synchronize two NSScrollView同步两个 NSScrollView
【发布时间】:2011-07-06 12:44:48
【问题描述】:

我阅读了文档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


    【解决方案1】:

    我在对非常相似的情况(在 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 属性不等价,但从行为中可以清楚地看出它们不等价。

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

    【讨论】:

    • 谢谢你,我遇到了类似的问题,我试图隐藏覆盖滚动条,但它会破坏滚动视图中的两指手势滚动。使用此方法而不是[scrollView setHasVerticalScroller:NO] 实现了隐藏滚动条的预期结果,但滚动视图仍然正常滚动。
    【解决方案2】:

    我有一个非常相似的问题。 我有 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% 的情况下都有效,只是偶尔会出现抖动。 水平滚动同步没有问题。

    【讨论】:

      【解决方案3】:

      Swift 4 版本,在 auto-layout 环境中使用文档视图。 基于 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)
         }
      }
      

      结果:

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-11-24
        • 1970-01-01
        • 1970-01-01
        • 2023-03-16
        • 2018-03-27
        • 2011-05-05
        • 1970-01-01
        相关资源
        最近更新 更多