ScrollView 和 ImageView - 多次设备旋转后图像未居中

Posted

技术标签:

【中文标题】ScrollView 和 ImageView - 多次设备旋转后图像未居中【英文标题】:ScrollView & ImageView - Image not centered after multiple device rotations 【发布时间】:2014-09-18 19:29:12 【问题描述】:

我的应用中有一个包含三个视图的 segmentedControl,其中一个是 scrollView,它的工作原理类似于一种没有缩放的画廊,pageControl 和一个 imageView 在中心。

层次结构是这样的

--> 分段控制(3 个视图):descriptionView、imageTabView、shareView ----> imagesTabView (UIView) ------> 滚动视图 ------> 图像视图 ----> 页面控制

当设备为纵向或横向时,imageView 图像正确显示、居中且滚动效果非常好。

唯一的问题是,当您再次转动设备时,如果图像位于“中间”(例如,是 3 个中的第 2 个或 6 个中的第 3 个),它会显示为偏心、最左边或最右边,并且轻轻滑动它会回到中心,而如果图像是第一个或最后一个,它可以正常工作。

我在 S.O. 上看过这里。在各种线程上,尝试将contentView 设置为scrollView 的子视图并将imageView 添加为contentView 的子视图,但没有奏效,尝试将imageView 附加到底部或右侧scrollView 但也不起作用。

我觉得我离实现我想做的事情还有一步之遥,唯一的问题是我不明白为什么它没有居中。

viewWillLayoutSubviews 中我指定了contentSize,以便在它旋转时正确设置它的大小,比如

-(void)viewWillLayoutSubviews
    [super viewWillLayoutSubviews];
    self.scrollView.contentSize = CGSizeMake (self.scrollView.frame.size.width * photosArray.count, 1);

下面是我如何初始化 pageControl、scrollView 和 imageView:

-(void)configureImageTab
    pageControl = [UIPageControl new];
    [pageControl addTarget:self action:@selector(changePage) forControlEvents:UIControlEventValueChanged];
    pageControl.translatesAutoresizingMaskIntoConstraints = NO;

    //Don't show pageControl when there are no photos
    if (photosURL.count == 0)
        pageControl.hidden = YES;

    //Configuring scrollView
    self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.imageSegmentView.frame.size.width, self.imageSegmentView.frame.size.height-pageControl.frame.size.height)];
    self.scrollView.pagingEnabled = YES;
    self.scrollView.delegate = self;
    self.scrollView.userInteractionEnabled = YES;
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;

    //... Code cut - adding remote images to fetch to array

    //Actual setup -> scrollView adding imageView as subview with all the images
    for (int i =0; i< photosArray.count; i++)
        CGRect frame;
        frame.origin.x = self.scrollView.frame.size.width * i;
        frame.origin.y = 0;
        frame.size = self.scrollView.frame.size;

        //imageView setup
        imageView = [[UIImageView alloc]initWithFrame:frame];
        imageView.backgroundColor = [UIColor clearColor];
        imageView.clipsToBounds = YES;
        imageView.userInteractionEnabled = YES;
        imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
        imageView.translatesAutoresizingMaskIntoConstraints = YES;
        imageView.contentMode = UIViewContentModeScaleAspectFit;

        //Setting images urls
        [imageView setImageWithURL:[NSURL URLWithString:[photosArray objectAtIndex:i]] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) 
        //Error handling 
            
        usingActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];

        //Adding gesture recognizer to scrollView and imageView as subview
        [self.scrollView addGestureRecognizer:singleTap];
        [self.scrollView addSubview:imageView];
    

    //Setting the contentSize
    pageControl.numberOfPages = [photosURL count];

    [self.imageSegmentView addSubview:self.scrollView];
    [self.imageSegmentView addSubview:pageControl];

    //Constraints
    NSDictionary *views = @@"pageControl" : pageControl, @"scrollView" : self.scrollView;

    [self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[pageControl]-0-|" options:0 metrics:nil views:views]];
    [self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]-1-[pageControl]-1-|" options:0 metrics:nil views:views]];
    [self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:views]];

    [pageControl addConstraint:[NSLayoutConstraint constraintWithItem:pageControl attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.imageSegmentView attribute:NSLayoutAttributeHeight multiplier:0 constant:30]];


#pragma mark - scrollView delegate -

-(void)scrollViewDidScroll:(UIScrollView *)sView
    CGFloat pageWidth = self.scrollView.frame.size.width;
    int page = floor ((self.scrollView.contentOffset.x - pageWidth /2) /pageWidth) +1;
    self.pageControl.currentPage = page;


-(IBAction)changePage 
    CGRect frame;
    frame.origin.x = self.scrollView.frame.size.width * self.pageControl.currentPage;
    frame.origin.y = 0;
    frame.size = self.scrollView.frame.size;
    [self.scrollView scrollRectToVisible:frame animated:YES];


-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
    pageControlBeingUsed = NO;


-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
    pageControlBeingUsed = NO;

需要注意的是:imageView 正在使用 autoresizingMask:否则,它将无法正确显示图像。

我的猜测是,scrollView 委托中可能有一些需要修复的地方,但我不太确定。

任何建议表示赞赏!

编辑

我注意到 Twitter 应用在​​浏览用户图片然后转动设备时会出现同样的错误。

编辑 2 用于 TL;DR

基本上,假设我在带有分页的水平滚动视图中有 3 张图像。

我在第一张照片上将设备从纵向切换到横向,它显示在自己的位置,正确居中。

我移到下一张照片,显示为居中,然后我再次将设备转到纵向。照片未正确对齐,未居中

实际上,当设备旋转多次时,第一张和最后一张图像会居中显示。其他不居中

编辑 3

我提取了一些行并制作了sample project 来演示我遇到的问题。我想 contentSize 肯定有问题。

【问题讨论】:

【参考方案1】:

我们可以通过在界面即将旋转时记录当前页面,然后在此期间适当设置滚动视图的contentOffset来修复您正在谈论的特定错误(旋转后滚动视图未与页面边界对齐)旋转,在系统更新滚动视图的边界大小之后。让我们添加一个pageNumberPriorToRotation 实例变量:

@implementation ViewController 
    CGFloat pageNumberPriorToRotation;

然后,我们在界面即将旋转时进行设置:

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration 
    [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
    [self setPageNumberPriorToRotation];


- (void)setPageNumberPriorToRotation 
    CGRect bounds = self.scrollView.bounds;
    static const int kNumberOfImages = 3;
    pageNumberPriorToRotation = fmin(round(bounds.origin.x / bounds.size.width),
        kNumberOfImages - 1);

我们使用它来设置滚动视图在界面旋转期间的contentOffset

-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
    [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
    [self updateScrollViewLayout];


- (void)updateScrollViewLayout 
    CGRect bounds = self.scrollView.bounds;
    bounds.origin.x = bounds.size.width * pageNumberPriorToRotation;
    self.scrollView.bounds = bounds;

这可以解决您的主要问题:滚动视图在旋转后将始终与页面视图边界对齐。

但是……

滚动视图交互还有一些其他问题。在横向中,我无法滚动到第三张图像。旋转到横向并返回纵向后,我可以滚动到空白的第四页。这些问题大概就是您所说的“contentSize 肯定有问题”的意思。

此外,您的代码存在许多问题。它使用了一些过时的风格,比如显式声明属性的实例变量并将实例变量放在头文件中。它还受到Massive View Controller 的影响。它真的可以用现代风格重写,并使用 UITabBarControllerUIPageViewController 等功能。

不管怎样,你可能既没有时间也没有兴趣做这么多的工作,所以我将向你展示如何解决contentSize 的问题,同时让你的 VC 瘦一点。

我将创建一个名为ImageScrollViewUIScrollView 子类。你给我图像数组,我会负责设置它的子视图并在旋转后对齐页面边界。这是我的头文件:

ImageScrollView.h

#import <UIKit/UIKit.h>

@interface ImageScrollView : UIScrollView

@property (nonatomic, copy) NSArray *images;

@end

要实现这一点,我需要一些实例变量:

ImageScrollView.m

#import "ImageScrollView.h"
#import <tgmath.h>

@implementation ImageScrollView 
    NSMutableArray *imageSubviews;
    CGSize priorSize;
    CGFloat pageNumber;
    BOOL needsToSyncSubviewsWithImages : 1;

无论如何,首先我将实现公共 API,即 images 属性:

#pragma mark - Public API

@synthesize images = _images;

- (void)setImages:(NSArray *)images 
    _images = [images copy];
    needsToSyncSubviewsWithImages = YES;

请注意,当您设置images 数组时,我不会立即创建子视图。现在,我只设置了needsToSyncSubviewsWithImages 标志,所以我会知道在布局阶段这样做。

#pragma mark - UIView overrides

接下来,我需要覆盖layoutSubviews,这样我才能在布局阶段完成真正的工作。如果我的subviews 数组已更改,或者我的bounds 已更改,系统会在布局阶段向我发送layoutSubviews

因为我是一个滚动视图,并且因为滚动视图的contentOffset 实际上只是它的bounds.origin 的别名,所以系统向我发送了很多layoutSubviews:每次滚动视图滚动时。所以我要小心,只在layoutSubviews做必要的工作。

- (void)layoutSubviews 

我做的第一件事是调用super,它可以让自动布局工作(如果你正在使用它)并更新我的滚动指示器(如果它们是可见的)。

    [super layoutSubviews];

接下来,如果我有新图像,我会设置显示它们的子视图。

    if (needsToSyncSubviewsWithImages) 
        [self syncSubviewsWithImages];
    

接下来,如果我设置了新的子视图,或者我更改了大小,我会根据新大小布置子视图的框架,并与页面边界对齐。

    if (needsToSyncSubviewsWithImages || !CGSizeEqualToSize(self.bounds.size, priorSize)) 
        [self layoutForNewSize];
    

最后,我更新了我的状态。

    needsToSyncSubviewsWithImages = NO;
    priorSize = self.bounds.size;
    [self updatePageNumber];

当然,我将所有实际工作都委托给了辅助方法,所以现在我需要实现这些。

#pragma mark - Implementation details

要将我的子视图与我的图像同步,我需要做三件事。我需要确保我实际上分配了我的imageSubviews 数组,我需要确保每个图像都在一个子视图中,并且我需要确保我没有任何额外的图像子视图(以防我的images数组变小了)。

- (void)syncSubviewsWithImages 
    [self ensureImageSubviewsArrayExists];
    [self putImagesInSubviews];
    [self removeExtraSubviews];


- (void)ensureImageSubviewsArrayExists 
    if (imageSubviews == nil) 
        imageSubviews = [NSMutableArray arrayWithCapacity:self.images.count];
    


- (void)putImagesInSubviews 
    [self.images enumerateObjectsUsingBlock:^(id obj, NSUInteger i, BOOL *stop) 
        [self putImage:obj inSubviewAtIndex:i];
    ];


- (void)removeExtraSubviews 
    while (imageSubviews.count > self.images.count) 
        [imageSubviews.lastObject removeFromSuperview];
        [imageSubviews removeLastObject];
    


- (void)putImage:(UIImage *)image inSubviewAtIndex:(NSUInteger)i 
    UIImageView *imageView = [self imageViewAtIndex:i];
    imageView.image = image;

当我想获取索引的图像视图时,我可能会发现我实际上还没有创建足够的子视图,所以我按需创建它们:

- (UIImageView *)imageViewAtIndex:(NSUInteger)i 
    while (i >= imageSubviews.count) 
        UIView *view = [[UIImageView alloc] init];
        view.contentMode = UIViewContentModeScaleAspectFit;
        view.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
        [self addSubview:view];
        [imageSubviews addObject:view];
    
    return imageSubviews[i];

请注意,我已设置 autoresizingMask 以便自动调整大小实际上不会修改我的子视图框架。相反,我将“手动”布置它们。

好的,现在我需要实现设置我的子视图的框架并在我的大小发生变化时与页面边界对齐的方法。

- (void)layoutForNewSize 
    [self setSubviewFramesAndContentSize];
    [self alignToNearestPage];

设置子视图框架需要循环它们,从左到右布置它们。在我布置完最后一个之后,我知道我的contentSize。请注意,我只需要遍历imageSubviews,而不是self.subviews,因为self.subviews 还包含滚动指示器。

- (void)setSubviewFramesAndContentSize 
    CGRect frame = self.bounds;
    frame.origin = CGPointZero;
    for (UIView *subview in imageSubviews) 
        subview.frame = frame;
        frame.origin.x += frame.size.width;
    
    self.contentSize = CGSizeMake(frame.origin.x, frame.size.height);

为了与最近的页面对齐,我根据最后已知的页码和我的新尺寸设置了我的contentOffset

- (void)alignToNearestPage 
    self.contentOffset = CGPointMake(pageNumber * self.bounds.size.width, 0);

最后,我需要在每次滚动时更新我的​​页码,所以我会在轮换时使用它:

- (void)updatePageNumber 
    // Note that self.contentOffset == self.bounds.origin.
    CGRect bounds = self.bounds;
    pageNumber = fmin(round(bounds.origin.x / bounds.size.width), self.images.count - 1);


@end

现在您可以更新ViewController 以使用ImageScrollView。这主要涉及撕掉东西:

-(void)configureImageTab
    //Page control
    pageControl = [UIPageControl new];
    pageControl.currentPageIndicatorTintColor = [UIColor blackColor];
    pageControl.pageIndicatorTintColor = [UIColor grayColor];
    [pageControl addTarget:self action:@selector(changePage) forControlEvents:UIControlEventValueChanged];
    pageControl.translatesAutoresizingMaskIntoConstraints = NO;


    //Configuring scrollView
    self.scrollView = [[ImageScrollView alloc] initWithFrame:CGRectMake(0, 0, self.imageSegmentView.frame.size.width, self.imageSegmentView.frame.size.height-pageControl.frame.size.height)];
    self.scrollView.backgroundColor = [UIColor clearColor];
    self.scrollView.pagingEnabled = YES;
    self.scrollView.delegate = self;
    self.scrollView.userInteractionEnabled = YES;
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;

    //Adding imageURLS to array
    photos = @[ [UIImage imageNamed:@"createBootableUSBInstallDrive1"], [UIImage imageNamed:@"createBootableUSBInstallDrive2"], [UIImage imageNamed:@"createBootableUSBInstallDrive3"]];
    self.scrollView.images = photos;

    pageControl.numberOfPages = [photos count];

    [self.imageSegmentView addSubview:self.scrollView];
    [self.imageSegmentView addSubview:pageControl];


    NSDictionary *views = @@"pageControl" : pageControl, @"scrollView" : self.scrollView;

    [self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[pageControl]-0-|" options:0 metrics:nil views:views]];
    [self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:views]];

    [self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]-1-[pageControl]-1-|" options:0 metrics:nil views:views]];

    [pageControl addConstraint:[NSLayoutConstraint constraintWithItem:pageControl attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.imageSegmentView attribute:NSLayoutAttributeHeight multiplier:0 constant:30]];

还需要在头文件中将scrollView的声明类型改为ImageScrollView。您可以完全消除 viewWillLayoutSubviewswillRotateToInterfaceOrientation:duration:willAnimateRotationToInterfaceOrientation:duration: 方法。

我已将您的测试项目的修改版本上传到this github repository。

【讨论】:

天哪,这就像所有答案的母亲!谢谢你!我会检查并尽快尝试。根据过时的代码,这是真的,因为我从我的一个较旧的项目中获取了代码并“重新调整”了一点。 tabBarController,我已经在用了,这个是我主tableView的detailView。我会尽快回复您! 试过了,它确实有效。我只需要弄清楚如何使用 SDWebImage 进行远程下载就可以了。非常感谢您的回答!你太棒了

以上是关于ScrollView 和 ImageView - 多次设备旋转后图像未居中的主要内容,如果未能解决你的问题,请参考以下文章

使用 scrollView 和 imageView 的 bounds.height 属性计算 zoomScaleForHeight

如何将imageView添加到collectionViewCell中的scrollView

如何从 ScrollView 中的 ImageView 中选择项目?

如何使 scrollView 滚动 ImageView 直到中心?

ImageView 到屏幕右下角,上方有大 ScrollView

为啥添加ScrollView时ImageView Image Zoomed?