在调整 window 大小时保持缩放 NSScrollView 的内容居中并可见
Keeping the contents of a scaled NSScrollView centered and visible when resizing the window
我正在尝试放大一个包含 NSTextView
的 NSScrollView
并使其始终以其内容为中心。 NSTextView
有 left/right 内嵌以保持自动换行的一致性并使段落很好地位于视图的中心。
[NSScrollView scaleUnitSquareToSize:...]
和 setMagnification:...
都有自己的怪癖和问题,但目前 setMagnification 似乎是更好的选择,因为它不是相对的。
下面是发生的事情(以及其他奇怪的事情):
调整大小时,我更新插图:
CGFloat inset = self.textScrollView.frame.size.width / 2 - _documentWidth / 2;
self.textView.textContainerInset = NSMakeSize(inset, TEXT_INSET_TOP);
self.textView.textContainer.size = NSMakeSize(_documentWidth, self.textView.textContainer.size.height);
正在放大:
CGFloat magnification = [self.textScrollView magnification];
NSPoint center = NSMakePoint(self.textScrollView.frame.size.width / 2, self.textScrollView.frame.size.height / 2);
if (zoomIn) magnification += .05; else magnification -= .05;
[self.textScrollView setMagnification:magnification centeredAtPoint:center];
一切都在一段时间内有效。有时,根据 window 从哪个 window 角调整大小,ScrollView 会失去中心,我还没有找到 re-centering 放大视图的解决方案 NSScrollView
.
放大后,在调整 window 大小时布局约束也会被破坏,尤其是当 textContainer
被剪裁到视图之外时,应用程序崩溃并出现以下错误:
*** Assertion failure in -[NSISLinearExpression addVariable:coefficient:], /Library/Caches/com.apple.xbs/Sources/Foundation/Foundation-1349.91/Layout.subproj/IncrementalSimplex/NSISLinearExpression.m:716
一个问题可能是我根据UIScrollView
帧大小设置insets,因为包含的NSTextView的坐标在放大后似乎不是相对的而是绝对的。
有没有什么安全的方法可以放大这种视图并使其始终以其内容为中心?为什么我的限制会被打破?
我 运行 遇到过类似的问题,不幸的是我最终自己做了居中。以下是我的解决方案的一些亮点。
- 需要防止递归! (否则 Whosebug :)
- 创建一个不可绘制的NSView作为documentView,然后将你的可绘制视图添加为手动居中的子视图,并手动将框架设置为父级的visibleRect。
- 覆盖visibleRect,如果无效则再次调用它,并调试以确保其有效!
- 缩放分层支持视图 sux。您可以尝试使用 NSTiledLayer,但我已经多次尝试并放弃了该解决方案。
代码如下:
@interface FlippedParentView : NSView
@end
@implementation FlippedParentView
- (BOOL) isFlipped { return YES; }
@end
- (void)awakeFromNib
{
[self resetMouseInfo];
[[self window] setAcceptsMouseMovedEvents:YES];
needsFullRedraw = YES;
[self setAcceptsTouchEvents:YES];
// problem: when zoomed-in, CALayer backed NSOpenGLView becomes too large
// and hurts performance.
// solution: create a fullsizeView for the NSScrollView to resize,
// and make NSOpenGLView a subview. Keep NSOpenGLView size the same as visibleRect,
// positioning it as needed on the fullsizeView.
NSScrollView *scrollvw = [self enclosingScrollView];
[scrollvw setBackgroundColor:[NSColor darkStrokeColor]];
fullsizeView = [[FlippedParentView alloc] initWithFrame: [self frame]];
[scrollvw setDocumentView:fullsizeView];
[fullsizeView setAutoresizesSubviews:NO];
//printf("mask %d\n", [self autoresizingMask]);
[fullsizeView setAutoresizingMask: NSViewHeightSizable | NSViewWidthSizable | NSViewMinYMargin | NSViewMaxYMargin | NSViewMaxXMargin | NSViewMinXMargin];
[self setAutoresizingMask: NSViewNotSizable];
[fullsizeView addSubview:self];
}
- (NSRect) visibleRect
{
NSRect visRect = [super visibleRect];
if ( visRect.size.width == 0 )
{
visRect = [[self superview] visibleRect];
if ( visRect.size.width == 0 )
{
// this jacks up everything
DUMP( @"bad visibleRect" );
}
visRect.origin = NSZeroPoint;
}
return visRect;
}
- (void) _my_zoom: (double)newZoom
{
mouseFocusPt = [self focusPt];
NSRect oldVisRect = [[self superview] visibleRect];
if ( newZoom < 1.0 )
newZoom = 1.0;
if ( newZoom > kZoomFactorMax ) newZoom = kZoomFactorMax;
float xpct = (mouseFocusPt.x - oldVisRect.origin.x) /
( NSMaxX(oldVisRect) - oldVisRect.origin.x );
float ypct = (mouseFocusPt.y - oldVisRect.origin.y) /
( NSMaxY(oldVisRect) - oldVisRect.origin.y );
float oldZoom = zoomFactor;
zoomFactor = newZoom;
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Stay locked on users' relative mouse location, so user can zoom in and back out without
// the view scrolling out from under the mouse location.
NSPoint newFocusPt = NSMakePoint (mouseFocusPt.x * newZoom/oldZoom,
mouseFocusPt.y * newZoom/oldZoom) ;
NSRect myFrame = fullsizeFrame; // [self frame];
float marginPercent = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
[self updateContext];
NSRect newVisRect;
newVisRect.size = [self visibleRect].size;
newVisRect.origin.x = (newFocusPt.x) - (xpct * newVisRect.size.width);
//DLog( @"xpct %0.2f, zoomFactor %0.2f, newVisRect.origin.x %0.2f", xpct, zoomFactor, newVisRect.origin.x);
myFrame = fullsizeFrame; // [self frame];
float marginPercent2 = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
float marginDiff = (marginPercent - marginPercent2) * drawableSizeWithMargins.height;
newVisRect.origin.y = (newFocusPt.y ) - (ypct * newVisRect.size.height) - marginDiff;
//DLog( @"ypct %0.2f, zoomFactor %0.2f, newVisRect.origin.y %0.2f", ypct, zoomFactor, newVisRect.origin.y);
//DLog( @"marginPercent %0.2f newVisRect %@", marginPercent, NSStringFromRect(newVisRect) );
if ( newVisRect.origin.x < 1 ) newVisRect.origin.x = 1;
if ( newVisRect.origin.y < 1 ) newVisRect.origin.y = 1;
// NSLog( @"zoom scrollRectToVisible %@ bounds %@", NSStringFromRect(newVisRect), NSStringFromRect([[self superview] bounds]) );
// if ( iUseMousePt || isSlider )
[[self superview] scrollRectToVisible:newVisRect];
}
// - zoomFactor of 1.0 is defined as the zoomFactor needed to show entire selected context within visibleRect,
// including margins of 5% of the context size
// - zoomFactor > 1.0 will make pixels look bigger (view a subsection of a larger total drawableSize)
// - zoomFactor < 1.0 will make pixels look smaller (selectedContext size will be less than drawableSize)
-(void)updateContext
{
static BOOL sRecursing = NO;
if ( sRecursing ) return; // prevent recursion
sRecursing = YES;
//NSRect scrollRect = [[self superview] frame];
NSRect clipViewRect = [[[self enclosingScrollView] contentView] frame];
NSRect visRect = [[self superview] visibleRect]; // careful... visibleRect is sometimes NSZeroRect
float layoutWidth = clipViewRect.size.width;
float layoutHeight = clipViewRect.size.height;
marginPct = layoutHeight / (layoutHeight - (overlayViewMargin*2) );
// Satisfy the constraints fully-zoomed-out case:
// 1) the drawable rect is centered in the view with at margins.
// Allow for 5% margins (1.025 = 2.5% left, right, top, bottom)
// 2) guarantee the drawable rect does not overlap the mini-map in upper right corner.
NSRect baseRect = NSZeroRect;
baseRect.size = visRect.size;
NSRect drawableBaseRect = getCenteredRectFloat(baseRect, metaUnionRect.size );
//drawableSizeWithMargins = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor ) );
drawableSizeWithMargins = nsScaleSize( drawableBaseRect.size, zoomFactor );
// drawableSize will NOT include the margins. We loop until we've satisfied
// the constraints above.
drawableSize = drawableSizeWithMargins;
do
{
NSSize shrunkSize;
shrunkSize.width = layoutWidth / marginPct;
shrunkSize.height = layoutHeight / marginPct;
//drawableSize = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct ));
drawableSize = nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct );
[self calculateMiniMapRect]; // get approx. size. Will calculate once more below.
NSRect shrunkRect = getCenteredRectNoScaling(baseRect, shrunkSize );
// DLog( @"rough miniMapRect %@ shrunk %@", NSStringFromRect(miniMapRect), NSStringFromRect(shrunkRect));
// make sure minimap doesn't overlap drawable when you scroll to top-left
NSRect topMiniMapRect = miniMapRect;
topMiniMapRect.origin.x -= visRect.origin.x;
topMiniMapRect.origin.y = 0;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
float topMarginOffset = shrunkRect.size.height + (baseRect.size.height * 0.025);
shrunkRect.origin.y = NSMaxY(baseRect) - topMarginOffset;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
marginPct *= 1.025;
} while (1);
fullsizeFrame.origin = NSZeroPoint;
fullsizeFrame.size.width = fmax(drawableSizeWithMargins.width, layoutWidth);
fullsizeFrame.size.height = fmax(drawableSizeWithMargins.height, layoutHeight);
[fullsizeView setFrame:fullsizeFrame];
NSRect myNewFrame = [fullsizeView visibleRect];
if (myNewFrame.size.width > 0)
[self setFrame: myNewFrame]; //NSView
sRecursing = NO;
}
我正在尝试放大一个包含 NSTextView
的 NSScrollView
并使其始终以其内容为中心。 NSTextView
有 left/right 内嵌以保持自动换行的一致性并使段落很好地位于视图的中心。
[NSScrollView scaleUnitSquareToSize:...]
和 setMagnification:...
都有自己的怪癖和问题,但目前 setMagnification 似乎是更好的选择,因为它不是相对的。
下面是发生的事情(以及其他奇怪的事情):
调整大小时,我更新插图:
CGFloat inset = self.textScrollView.frame.size.width / 2 - _documentWidth / 2;
self.textView.textContainerInset = NSMakeSize(inset, TEXT_INSET_TOP);
self.textView.textContainer.size = NSMakeSize(_documentWidth, self.textView.textContainer.size.height);
正在放大:
CGFloat magnification = [self.textScrollView magnification];
NSPoint center = NSMakePoint(self.textScrollView.frame.size.width / 2, self.textScrollView.frame.size.height / 2);
if (zoomIn) magnification += .05; else magnification -= .05;
[self.textScrollView setMagnification:magnification centeredAtPoint:center];
一切都在一段时间内有效。有时,根据 window 从哪个 window 角调整大小,ScrollView 会失去中心,我还没有找到 re-centering 放大视图的解决方案 NSScrollView
.
放大后,在调整 window 大小时布局约束也会被破坏,尤其是当 textContainer
被剪裁到视图之外时,应用程序崩溃并出现以下错误:
*** Assertion failure in -[NSISLinearExpression addVariable:coefficient:], /Library/Caches/com.apple.xbs/Sources/Foundation/Foundation-1349.91/Layout.subproj/IncrementalSimplex/NSISLinearExpression.m:716
一个问题可能是我根据UIScrollView
帧大小设置insets,因为包含的NSTextView的坐标在放大后似乎不是相对的而是绝对的。
有没有什么安全的方法可以放大这种视图并使其始终以其内容为中心?为什么我的限制会被打破?
我 运行 遇到过类似的问题,不幸的是我最终自己做了居中。以下是我的解决方案的一些亮点。
- 需要防止递归! (否则 Whosebug :)
- 创建一个不可绘制的NSView作为documentView,然后将你的可绘制视图添加为手动居中的子视图,并手动将框架设置为父级的visibleRect。
- 覆盖visibleRect,如果无效则再次调用它,并调试以确保其有效!
- 缩放分层支持视图 sux。您可以尝试使用 NSTiledLayer,但我已经多次尝试并放弃了该解决方案。
代码如下:
@interface FlippedParentView : NSView
@end
@implementation FlippedParentView
- (BOOL) isFlipped { return YES; }
@end
- (void)awakeFromNib
{
[self resetMouseInfo];
[[self window] setAcceptsMouseMovedEvents:YES];
needsFullRedraw = YES;
[self setAcceptsTouchEvents:YES];
// problem: when zoomed-in, CALayer backed NSOpenGLView becomes too large
// and hurts performance.
// solution: create a fullsizeView for the NSScrollView to resize,
// and make NSOpenGLView a subview. Keep NSOpenGLView size the same as visibleRect,
// positioning it as needed on the fullsizeView.
NSScrollView *scrollvw = [self enclosingScrollView];
[scrollvw setBackgroundColor:[NSColor darkStrokeColor]];
fullsizeView = [[FlippedParentView alloc] initWithFrame: [self frame]];
[scrollvw setDocumentView:fullsizeView];
[fullsizeView setAutoresizesSubviews:NO];
//printf("mask %d\n", [self autoresizingMask]);
[fullsizeView setAutoresizingMask: NSViewHeightSizable | NSViewWidthSizable | NSViewMinYMargin | NSViewMaxYMargin | NSViewMaxXMargin | NSViewMinXMargin];
[self setAutoresizingMask: NSViewNotSizable];
[fullsizeView addSubview:self];
}
- (NSRect) visibleRect
{
NSRect visRect = [super visibleRect];
if ( visRect.size.width == 0 )
{
visRect = [[self superview] visibleRect];
if ( visRect.size.width == 0 )
{
// this jacks up everything
DUMP( @"bad visibleRect" );
}
visRect.origin = NSZeroPoint;
}
return visRect;
}
- (void) _my_zoom: (double)newZoom
{
mouseFocusPt = [self focusPt];
NSRect oldVisRect = [[self superview] visibleRect];
if ( newZoom < 1.0 )
newZoom = 1.0;
if ( newZoom > kZoomFactorMax ) newZoom = kZoomFactorMax;
float xpct = (mouseFocusPt.x - oldVisRect.origin.x) /
( NSMaxX(oldVisRect) - oldVisRect.origin.x );
float ypct = (mouseFocusPt.y - oldVisRect.origin.y) /
( NSMaxY(oldVisRect) - oldVisRect.origin.y );
float oldZoom = zoomFactor;
zoomFactor = newZoom;
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Stay locked on users' relative mouse location, so user can zoom in and back out without
// the view scrolling out from under the mouse location.
NSPoint newFocusPt = NSMakePoint (mouseFocusPt.x * newZoom/oldZoom,
mouseFocusPt.y * newZoom/oldZoom) ;
NSRect myFrame = fullsizeFrame; // [self frame];
float marginPercent = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
[self updateContext];
NSRect newVisRect;
newVisRect.size = [self visibleRect].size;
newVisRect.origin.x = (newFocusPt.x) - (xpct * newVisRect.size.width);
//DLog( @"xpct %0.2f, zoomFactor %0.2f, newVisRect.origin.x %0.2f", xpct, zoomFactor, newVisRect.origin.x);
myFrame = fullsizeFrame; // [self frame];
float marginPercent2 = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
float marginDiff = (marginPercent - marginPercent2) * drawableSizeWithMargins.height;
newVisRect.origin.y = (newFocusPt.y ) - (ypct * newVisRect.size.height) - marginDiff;
//DLog( @"ypct %0.2f, zoomFactor %0.2f, newVisRect.origin.y %0.2f", ypct, zoomFactor, newVisRect.origin.y);
//DLog( @"marginPercent %0.2f newVisRect %@", marginPercent, NSStringFromRect(newVisRect) );
if ( newVisRect.origin.x < 1 ) newVisRect.origin.x = 1;
if ( newVisRect.origin.y < 1 ) newVisRect.origin.y = 1;
// NSLog( @"zoom scrollRectToVisible %@ bounds %@", NSStringFromRect(newVisRect), NSStringFromRect([[self superview] bounds]) );
// if ( iUseMousePt || isSlider )
[[self superview] scrollRectToVisible:newVisRect];
}
// - zoomFactor of 1.0 is defined as the zoomFactor needed to show entire selected context within visibleRect,
// including margins of 5% of the context size
// - zoomFactor > 1.0 will make pixels look bigger (view a subsection of a larger total drawableSize)
// - zoomFactor < 1.0 will make pixels look smaller (selectedContext size will be less than drawableSize)
-(void)updateContext
{
static BOOL sRecursing = NO;
if ( sRecursing ) return; // prevent recursion
sRecursing = YES;
//NSRect scrollRect = [[self superview] frame];
NSRect clipViewRect = [[[self enclosingScrollView] contentView] frame];
NSRect visRect = [[self superview] visibleRect]; // careful... visibleRect is sometimes NSZeroRect
float layoutWidth = clipViewRect.size.width;
float layoutHeight = clipViewRect.size.height;
marginPct = layoutHeight / (layoutHeight - (overlayViewMargin*2) );
// Satisfy the constraints fully-zoomed-out case:
// 1) the drawable rect is centered in the view with at margins.
// Allow for 5% margins (1.025 = 2.5% left, right, top, bottom)
// 2) guarantee the drawable rect does not overlap the mini-map in upper right corner.
NSRect baseRect = NSZeroRect;
baseRect.size = visRect.size;
NSRect drawableBaseRect = getCenteredRectFloat(baseRect, metaUnionRect.size );
//drawableSizeWithMargins = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor ) );
drawableSizeWithMargins = nsScaleSize( drawableBaseRect.size, zoomFactor );
// drawableSize will NOT include the margins. We loop until we've satisfied
// the constraints above.
drawableSize = drawableSizeWithMargins;
do
{
NSSize shrunkSize;
shrunkSize.width = layoutWidth / marginPct;
shrunkSize.height = layoutHeight / marginPct;
//drawableSize = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct ));
drawableSize = nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct );
[self calculateMiniMapRect]; // get approx. size. Will calculate once more below.
NSRect shrunkRect = getCenteredRectNoScaling(baseRect, shrunkSize );
// DLog( @"rough miniMapRect %@ shrunk %@", NSStringFromRect(miniMapRect), NSStringFromRect(shrunkRect));
// make sure minimap doesn't overlap drawable when you scroll to top-left
NSRect topMiniMapRect = miniMapRect;
topMiniMapRect.origin.x -= visRect.origin.x;
topMiniMapRect.origin.y = 0;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
float topMarginOffset = shrunkRect.size.height + (baseRect.size.height * 0.025);
shrunkRect.origin.y = NSMaxY(baseRect) - topMarginOffset;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
marginPct *= 1.025;
} while (1);
fullsizeFrame.origin = NSZeroPoint;
fullsizeFrame.size.width = fmax(drawableSizeWithMargins.width, layoutWidth);
fullsizeFrame.size.height = fmax(drawableSizeWithMargins.height, layoutHeight);
[fullsizeView setFrame:fullsizeFrame];
NSRect myNewFrame = [fullsizeView visibleRect];
if (myNewFrame.size.width > 0)
[self setFrame: myNewFrame]; //NSView
sRecursing = NO;
}