I'm having a lot of trouble figuring out how best to reposition my UIScrollView
's image view (I have a gallery kind of app going right now, similar to Photos.app, specifically when you're viewing a single image) when the orientation switches from portrait to landscape or vice-versa.
我很难找到如何最好地重新定位我的UIScrollView的图像视图(我现在有一个类似于图库的应用程序,类似于Photos.app,特别是当你查看单个图像时)方向切换从肖像到风景,反之亦然。
I know my best bet is to manipulate the contentOffset
property, but I'm not sure what it should be changed to.
我知道我最好的办法是操纵contentOffset属性,但我不确定它应该改成什么。
I've played around a lot, and it seems like for whatever reason 128
works really well. In my viewWillLayoutSubviews
method for my view controller I have:
我玩过很多次,似乎无论出于何种原因,128的效果都非常好。在我的视图控制器的viewWillLayoutSubviews方法中,我有:
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.x >= 128) {
newContentOffset.x -= 128.0;
}
else {
newContentOffset.x = 0.0;
}
newContentOffset.y += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
else {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.y >= 128) {
newContentOffset.y -= 128.0;
}
else {
newContentOffset.y = 0.0;
}
newContentOffset.x += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
And it works pretty well. I hate how it's using a magic number though, and I have no idea where this would come from.
而且效果很好。我讨厌它如何使用一个神奇的数字,我不知道它会来自哪里。
Also, whenever I zoom the image I have it set to stay centred (just like Photos.app does):
此外,每当我缩放图像时,我都将其设置为保持居中(就像Photos.app一样):
- (void)centerScrollViewContent {
// Keep image view centered as user zooms
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(self.scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(self.scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(self.scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(self.scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
}
So I need it to keep it positioned properly so it doesn't show black borders around the image when repositioned. (That's what the checks in the first block of code are for.)
因此我需要它以使其正确定位,以便在重新定位时不会在图像周围显示黑色边框。 (这就是第一个代码块中的检查。)
Basically, I'm curious how to implement functionality like in Photos.app, where on rotate the scrollview intelligently repositions the content so that the middle of the visible content before the rotation is the same post-rotation, so it feels continuous.
基本上,我很好奇如何实现像Photos.app这样的功能,其中旋转滚动视图智能地重新定位内容,以便在旋转之前可见内容的中间是相同的后旋转,因此感觉连续。
1 个解决方案
#1
6
You should change the UIScrollView
's contentOffset
property whenever the scrollView is layouting its subviews after its bounds
value has been changed. Then when the interface orientation will be changed, UIScrollView
's bounds
will be changed accordingly updating the contentOffset
.
每当scrollView在其边界值更改后布局其子视图时,您应该更改UIScrollView的contentOffset属性。然后,当界面方向改变时,UIScrollView的边界将相应地更改,更新contentOffset。
To make things "right" you should subclass UIScrollView
and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
为了使事情“正确”,您应该将UIScrollView子类化并在那里进行所有调整。这也可以让您轻松重用“特殊”scrollView。
The contentOffset
calculation function should be placed inside UIScrollView
's layoutSubviews
method. The problem is that this method is called not only when the bounds
value is changed but also when srollView is zoomed or scrolled. So the bounds
value should be tracked to hint if the layoutSubviews
method is called due to a change in bounds
as a consequence of the orientation change, or due to a pan or pinch gesture.
contentOffset计算函数应该放在UIScrollView的layoutSubviews方法中。问题是不仅在更改边界值时调用此方法,而且在调整或滚动srollView时调用此方法。因此,如果由于方向更改导致的边界更改,或者由于平移或捏合手势而调用layoutSubviews方法,则应跟踪边界值以提示。
So the first part of the UIScrollView
subclass should look like this:
所以UIScrollView子类的第一部分应如下所示:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews
method is called every time the UIScrollView
is panned, zoomed or its bounds
are changed. The _centerScrollViewContent
method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView
class and therefore instead of using self.imageView
property to reference the zoomed view, which may not be available in the context of UIScrollView
class, the viewForZoomingInScrollView:
delegate method is used.
这里,每次UIScrollView被平移,缩放或其边界被更改时,都会调用layoutSubviews方法。 _centerScrollViewContent方法负责在缩放视图的大小小于scrollView边界的大小时使缩放视图居中。并且,每次用户平移或缩放scrollView或旋转设备时都会调用它。它的实现与您在问题中提供的实现非常相似。不同之处在于此方法是在UIScrollView类的上下文中编写的,因此不使用self.imageView属性来引用缩放视图(可能在UIScrollView类的上下文中不可用),而是使用viewForZoomingInScrollView:delegate方法。
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset
method. This method is responsible for adjusting the contentOffset
. Such that when UIScrollView
's bounds
value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView
's bounds
is changed (e.g.: orientation change).
但更重要的是_adjustContentOffset方法。此方法负责调整contentOffset。这样当UIScrollView的边界值改变时,更改前的中心点将保持在中心位置。并且由于条件语句,只有在更改UIScrollView的边界时才会调用它(例如:方向更改)。
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView
class (here is link to GitHub) implementing the above behavior and additional bonuses:
我已经创建了一个工作的SMScrollView类(这里是GitHub的链接),实现了上述行为和额外的奖励:
- You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
- In addition to adjusting
contentOffset
you may find that you also want to adjust the scrollView'szoomScale
. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.
您可以注意到,在“照片”应用中,缩放照片,然后将其滚动到其中一个边界,然后旋转设备不会将中心点保持在原位。相反,它将scrollView粘贴到该边界。如果你滚动到其中一个角然后旋转,scrollView也会粘在那个角上。
除了调整contentOffset,您可能还会发现还要调整scrollView的zoomScale。例如,假设您正在以纵向模式查看照片,该照片已缩放以适合屏幕尺寸。然后,当您将设备旋转到横向模式时,您可能希望升级照片以利用可用空间。
#1
6
You should change the UIScrollView
's contentOffset
property whenever the scrollView is layouting its subviews after its bounds
value has been changed. Then when the interface orientation will be changed, UIScrollView
's bounds
will be changed accordingly updating the contentOffset
.
每当scrollView在其边界值更改后布局其子视图时,您应该更改UIScrollView的contentOffset属性。然后,当界面方向改变时,UIScrollView的边界将相应地更改,更新contentOffset。
To make things "right" you should subclass UIScrollView
and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
为了使事情“正确”,您应该将UIScrollView子类化并在那里进行所有调整。这也可以让您轻松重用“特殊”scrollView。
The contentOffset
calculation function should be placed inside UIScrollView
's layoutSubviews
method. The problem is that this method is called not only when the bounds
value is changed but also when srollView is zoomed or scrolled. So the bounds
value should be tracked to hint if the layoutSubviews
method is called due to a change in bounds
as a consequence of the orientation change, or due to a pan or pinch gesture.
contentOffset计算函数应该放在UIScrollView的layoutSubviews方法中。问题是不仅在更改边界值时调用此方法,而且在调整或滚动srollView时调用此方法。因此,如果由于方向更改导致的边界更改,或者由于平移或捏合手势而调用layoutSubviews方法,则应跟踪边界值以提示。
So the first part of the UIScrollView
subclass should look like this:
所以UIScrollView子类的第一部分应如下所示:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews
method is called every time the UIScrollView
is panned, zoomed or its bounds
are changed. The _centerScrollViewContent
method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView
class and therefore instead of using self.imageView
property to reference the zoomed view, which may not be available in the context of UIScrollView
class, the viewForZoomingInScrollView:
delegate method is used.
这里,每次UIScrollView被平移,缩放或其边界被更改时,都会调用layoutSubviews方法。 _centerScrollViewContent方法负责在缩放视图的大小小于scrollView边界的大小时使缩放视图居中。并且,每次用户平移或缩放scrollView或旋转设备时都会调用它。它的实现与您在问题中提供的实现非常相似。不同之处在于此方法是在UIScrollView类的上下文中编写的,因此不使用self.imageView属性来引用缩放视图(可能在UIScrollView类的上下文中不可用),而是使用viewForZoomingInScrollView:delegate方法。
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset
method. This method is responsible for adjusting the contentOffset
. Such that when UIScrollView
's bounds
value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView
's bounds
is changed (e.g.: orientation change).
但更重要的是_adjustContentOffset方法。此方法负责调整contentOffset。这样当UIScrollView的边界值改变时,更改前的中心点将保持在中心位置。并且由于条件语句,只有在更改UIScrollView的边界时才会调用它(例如:方向更改)。
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView
class (here is link to GitHub) implementing the above behavior and additional bonuses:
我已经创建了一个工作的SMScrollView类(这里是GitHub的链接),实现了上述行为和额外的奖励:
- You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
- In addition to adjusting
contentOffset
you may find that you also want to adjust the scrollView'szoomScale
. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.
您可以注意到,在“照片”应用中,缩放照片,然后将其滚动到其中一个边界,然后旋转设备不会将中心点保持在原位。相反,它将scrollView粘贴到该边界。如果你滚动到其中一个角然后旋转,scrollView也会粘在那个角上。
除了调整contentOffset,您可能还会发现还要调整scrollView的zoomScale。例如,假设您正在以纵向模式查看照片,该照片已缩放以适合屏幕尺寸。然后,当您将设备旋转到横向模式时,您可能希望升级照片以利用可用空间。