6 Stimmen

zwei NSScrollView synchronisieren

Ich habe das Dokument gelesen Synchronisieren von Bildlaufansichten und tat genau wie das Dokument, aber es gibt ein Problem.

Ich möchte eine NSTableView und eine NSTextView synchronisieren. Lassen Sie zunächst NSTableView NSTextView überwachen, und alles ist in Ordnung, wenn ich die TextView blättern, aber wenn ich versuche, TableView zu blättern, fand ich, dass die TableView an eine andere Stelle springen wird (vielleicht mehrere Zeilen zurück) auf den ersten, dann weiter zu blättern von diesem Ort.

Dieses Problem besteht auch dann noch, wenn ich TextView TableView überwachen lasse.

weiß jemand, was das Problem ist? kann ich nicht synchronisieren ein TableView und ein TextView?

Bearbeitet: OK, jetzt habe ich festgestellt, dass die TableView geht zurück an die Stelle seit dem letzten Scrollen. z.B. TableView's oberste Zeile ist 10. Zeile, dann scrolle ich TextView, jetzt TableView's oberste Zeile ist 20. Zeile, und wenn ich TableView wieder scrollen, wird die TableView zurück zu 10.

3voto

jmk Punkte 1898

Ich bin gerade auf genau dieses Problem gestoßen, als ich eine sehr ähnliche Situation (unter Lion) behoben habe. Ich bemerkte, dass dies nur auftritt, wenn die Bildlaufleisten ausgeblendet sind - aber ich habe überprüft, dass sie immer noch in der Feder existieren und immer noch korrekt instanziiert werden.

Ich habe sogar dafür gesorgt, dass ich die -[NSScrollView reflectScrolledClipView:] aber das hat nichts gebracht. Es scheint wirklich, wie dies ist ein Fehler in NSScrollView.

Wie auch immer, ich konnte das Problem umgehen, indem ich eine benutzerdefinierte Scroller-Klasse erstellt habe. Alles, was ich tun musste, war, die folgenden Klassenmethoden zu überschreiben:

+ (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];
}

Dann habe ich die Bildlaufleisten im Interface Builder "sichtbar" gemacht. Da sie sich jedoch selbst ausblenden, erscheinen sie nicht auf dem Bildschirm und können vom Benutzer nicht angeklickt werden. Es ist überraschend, dass die IB-Einstellung und die hidden nicht gleichwertig sind, aber aus dem Verhalten geht klar hervor, dass sie es nicht sind.

Das ist zwar nicht die beste Lösung, aber die einfachste, die mir (bisher) eingefallen ist.

0voto

uchuugaka Punkte 12486

Ich hatte ein ganz ähnliches Problem. Ich habe 3 Scrollviews zu synchronisieren. Eine davon ist eine Kopfzeile, die nur horizontal gescrollt wird. Eine, die eine Seitenleiste ist, die nur vertikal scrollt. Eine, die ein Inhaltsbereich unterhalb der Kopfzeile und rechts von der Seitenleiste ist. Die Kopfzeile und die Seitenleiste sollten sich mit dem Inhaltsbereich bewegen. Der Inhaltsbereich sollte sich mit der Kopfzeile oder der Seitenleiste mitbewegen, wenn eine von beiden gescrollt wird.

Der horizontale Bildlauf war nie ein Problem. Der vertikale Bildlauf führte immer dazu, dass die beiden Ansichten in entgegengesetzte Richtungen verschoben wurden.

Die merkwürdige Lösung, zu der ich kam, war, eine clipView-Unterklasse zu erstellen (was ich bereits getan habe, da man das so ziemlich immer tun muss, wenn man etwas Nettes will, das nicht aus der Box kommt). In der clipView Unterklasse, füge ich eine Eigenschaft BOOL isInverted und in der Überschreibung von isFlipped ich zurückgeben self.isInverted.

Das Seltsame ist, dass diese BOOL-Werte für die Umkehrbarkeit in allen 3 Ansichten von Anfang an gesetzt sind und übereinstimmen. Es scheint, dass die Scroll-Maschinerie tatsächlich fehlerhaft ist. Meine Abhilfe, die ich stolperte auf war, Sandwich der Scroll-Synchronisierung-Code zwischen Aufrufen, um sowohl die Seitenleiste und Inhalt-Ansicht nicht umgedreht und dann aktualisieren Sie alle vertikalen Scrollen, dann setzen beide umgedreht wieder. Es muss ein veralteter Code in der Scroll-Maschinerie sein, der versucht, invertiertes Scrollen zu unterstützen...

Dies sind die Methoden, die von den NSNotificationCenter addObserver-Methoden aufgerufen werden, um die NSViewBoundsDidChangeNotification für die clipViews zu beobachten.

- (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];
}

Dies funktioniert in 99 % der Fälle, mit nur gelegentlichem Ruckeln. Die horizontale Bildlaufsynchronisation hat keine Probleme.

0voto

Vlad Punkte 5563

Swift 4 Version, die die Dokumentenansicht in Autolayout Umwelt. Basierend auf Apple Artikel Synchronisieren von Bildlaufansichten mit dem Unterschied, dass NSView.boundsDidChangeNotification in der Clip-Ansicht bei der Synchronisierung mit einer anderen Bildlaufansicht vorübergehend ignoriert. Um den vertikalen Scroller auszublenden, geben Sie wiederverwendbar ein InvisibleScroller verwendet wird.

Datei SynchronedScrollViewController.swift - Ansichtssteuerungen mit zwei Bildlaufansichten.

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

Datei: TestView.swift - Testansicht. Zeichnet alle 20 Punkte eine Linie.

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()
   }

}

Datei: NSScrollView.swift - Wiederverwendbare Verlängerung.

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

Datei: UnsichtbarerScroller.swift - Wiederverwendbarer unsichtbarer Scroller.

// 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
   }
}

Datei: ClipView.swift - Angepasste Unterklasse von 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)
         }
      }
   }
}

Datei: NotificationObserver.swift - Wiederverwendbarer Beobachter der Benachrichtigung.

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

Ergebnis:

Synchronised Scroll Views

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X