синхронизировать два NSScrollView - PullRequest
6 голосов
/ 06 июля 2011

Я прочитал документ Синхронизация представлений прокрутки , и сделал точно так же, как документ, но есть вопрос.

Я хочу синхронизировать NSTableView и NSTextView.сначала пусть NSTableView отслеживает NSTextView, и все в порядке, когда я прокручиваю TextView, но когда я пытаюсь прокрутить TableView, я обнаружил, что TableView сначала перейдет в другое место (возможно, назад на несколько строк), а затем продолжит прокрутку с этого места.

Эта проблема все еще существует даже после того, как я разрешаю TextView контролировать TableView.

Кто-нибудь знает, в чем проблема?я не могу синхронизировать TableView и TextView?

Отредактировано: ОК, теперь я обнаружил, что TableView вернется на место после последней прокрутки.Например, верхняя строка TableView - 10-я строка, затем я прокручиваю TextView, теперь верхняя строка TableView - 20-я строка, и если я снова прокручиваю TableView, TableView сначала вернется к 10-й строке, а затем начнет прокручиваться.

Ответы [ 3 ]

3 голосов
/ 18 октября 2012

Я только что столкнулся с этой точной проблемой при устранении проблем в очень похожей ситуации (на 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];
}

Затем я позволил скроллерам быть "видимыми" в Интерфейсном Разработчике. Однако, поскольку они прячутся, они не отображаются на экране и не могут быть нажаты пользователем. Удивительно, что настройка IB и свойство hidden не эквивалентны, но из поведения видно, что они не являются.

Это не лучшее решение, но это самый простой обходной путь, который я нашел (пока).

0 голосов
/ 24 февраля 2019

Swift 4 версия, которая использует вид документа в среде auto-layout . Основано на статье Apple Синхронизация видов прокрутки с той разницей, что 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: /6849417/skryt-skrollery-ostaviv-prokrutku-vklychennoi-v-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)
   }
}

Результат:

Synchronised Scroll Views

0 голосов
/ 03 февраля 2015

У меня была довольно похожая проблема. У меня есть 3 прокрутки для синхронизации. Тот, который является заголовком, который прокручивается только горизонтально. Тот, который является боковой панелью, которая прокручивается только вертикально. Тот, который является областью содержимого ниже заголовка и справа от боковой панели. Заголовок и боковая панель должны двигаться вместе с областью содержимого. Область содержимого должна перемещаться вместе с заголовком или боковой панелью при прокрутке.

Горизонтальная прокрутка никогда не была проблемой. При вертикальной прокрутке два вида прокручивались в противоположных направлениях.

Странное решение, к которому я пришел, - это создание подкласса clipView (что я уже сделал, как вам всегда нужно, если вы хотите что-то хорошее, не выходящее из коробки). В подкласс clipView я добавляю свойство BOOL isInverted, а в переопределении isFlipped возвращаю self.isInverted.

Странная вещь в том, что эти значения BOOL для переворачивания установлены и совпадают во всех 3 видах с начала. Кажется, что прокрутка машины действительно глючит. Мой обходной путь, на который я наткнулся, заключался в том, чтобы поместить код синхронизации прокрутки между вызовами, чтобы отключить боковую панель и представление содержимого, а затем обновить любую вертикальную прокрутку, а затем снова установить оба перевернутых. Должен быть какой-то устаревший код в прокручивающем оборудовании, пытающемся поддерживать перевернутую прокрутку ...

Это методы, вызываемые методами NSNotificationCenter addObserver для наблюдения NSViewBoundsDidChangeNotification для clipViews.

- (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% случаев, с редким дрожанием. Синхронизация с горизонтальной прокруткой не вызывает проблем.

...