使用嵌套滚动视图时,滚动会“卡住”

8

问题描述:

我有一个iOS项目,用于浏览带有嵌套UIScrollViews的图像,受到著名的苹果 PhotoScroller的启发。 问题是,当图像被纵向或横向缩放时,有时滚动会“卡住”。以下是在 iPhone 4s 上查看尺寸为 935x1400 的图像时的示例:

(我开始向左拖动,但滚动视图立即放弃此操作,图像被“卡住”)

Scroll problem

解决方法:

我发现一种通过调整内部滚动视图的内容大小到最接近整数来实现缓解的方法:

// Inside ImageScrollView.m

- (void)setZoomScale:(CGFloat)zoomScale
{
    [super setZoomScale:zoomScale];
    [self fixContentSizeForScrollingIfNecessary];
}

- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated
{
    [super zoomToRect:rect animated:animated];
    [self fixContentSizeForScrollingIfNecessary];
}

- (void)fixContentSizeForScrollingIfNecessary
{
    if (SYSTEM_VERSION_LESS_THAN(@"10.2"))
    {
        CGSize content = self.contentSize;
        content.width = rint(content.width);
        content.height = rint(content.height);
        self.contentSize = content;
    }
}

但这个修复并不完美-现在有些图片两侧会显示一像素宽的条纹。例如,在690x14300的图像,在底部显示如下:

iPhone 6

另外,奇怪的是,我能在iOS 7.0 - 10.1上复现这个问题,但在iOS 10.2及以上版本中一切正常。

问题:

那么,我做错了什么?我的修复方法能否改进?

测试项目:

我创建了一个简单的测试项目来说明描述的问题 - NestedScrollingProblems。请注意,我的ImageScrollView版本与苹果的略有不同,因为我应用了另一种缩放规则。此外,默认情况下,工作区被注释掉了。 (项目代码有点乱,请见谅)


在您的演示项目中,普通图像可以正常工作。但是大型图像仅在第一次出现时会卡住。卡在哪里了? - Ajjjjjjjj
当您在图像(大图或普通图)之间滚动时,会出现卡顿情况,请参见附带的gif。由于图像很“大”,所以出现挂起属于正常现象(对于我的项目,我使用了分块技术,但在测试项目中省略了此部分,因为它与问题无关)。 - Anton Malmygin
1个回答

0

目前还没有足够的声望来评论帖子。

但从苹果文档的外观来看,这个项目在滚动时deinits图像,然后在它们即将被加载时re-inits它们(请参见UIScrollView.m中的第350行)。我还注意到ImageScrollView.m内部有一个注释(第346行),明确说明这个类是为了避免缓存而设计的。这对于演示来说是一种实用的方式,但对于像您想要的那样考虑ui加载速度的生产或现实世界应用程序来说,则不是一个实用的方式。

我还注意到您的应用程序必须滚动得更远才能启用分页...这可能是代码中的某些错误,或者它本身就会使主线程停滞,无法流畅地运行分页。如果您打算使用如此宽的分页阈值...我建议您将其减小,以获得更好的用户体验,因为现代智能手机的屏幕比iPhone 4S的屏幕宽得多。

为了解决这个问题,

我在SO上找到了这个post(下面),它似乎有一个相当不错的obj-c方法来缓存和获取应用程序启动后从这样的缓存帖子中的图像数据。您应该能够将其轻松地整合到启动后的方法中,甚至可以使用它与网络一起下载图像。您只需要确保您的UIImage视图正确链接到您使用的url字符串,通过为每个图像视图设置一组自定义字符串变量,或者通过将UImageView子类化为自定义类,并将缓存方法添加到其中以使您的代码看起来更简单。以下是来自iOSfleer的该帖子中的方法和NSCahe类。
NSCache类:
@interface Sample : NSObject

+ (Sample*)sharedInstance;

// set
- (void)cacheImage:(UIImage*)image forKey:(NSString*)key;
// get
- (UIImage*)getCachedImageForKey:(NSString*)key;

@end

#import "Sample.h"

static Sample *sharedInstance;

@interface Sample ()
@property (nonatomic, strong) NSCache *imageCache;
@end

@implementation Sample

+ (Sample*)sharedInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[Sample alloc] init];
    });
    return sharedInstance;
}
- (instancetype)init {
    self = [super init];
    if (self) {
        self.imageCache = [[NSCache alloc] init];
    }
    return self;
}

- (void)cacheImage:(UIImage*)image forKey:(NSString*)key {
    [self.imageCache setObject:image forKey:key];
}

- (UIImage*)getCachedImageForKey:(NSString*)key {
    return [self.imageCache objectForKey:key];
}

为了不改变您已经做的太多,似乎通过更改ImageScrollview.m中的displayImageWithInfo方法为以下方法(使用缓存方法),在初始加载后它似乎工作得更好。如果我是您,我还会进一步实现一个循环式方法,在控制器的viewDidLoad方法中立即缓存这些图像,以便在启动时更快地加载。但这取决于您。
- (void)displayImageWithInfo:(ImageItem*)imageInfo
{
    CGSize imageSize = (CGSize){.width = imageInfo.width, .height = imageInfo.height};

    // clear the previous imageView
    [self.imageView removeFromSuperview];
    self.imageView = nil;

    // reset our zoomScale to 1.0 before doing any further calculations
    self.zoomScale = 1.0;

    self.imageView = [[UIImageView alloc] initWithFrame:(CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size = imageSize}];

    UIImage *image = [[Sample sharedInstance] getCachedImageForKey:imageInfo.path];
    if(image)
    {
        NSLog(@"This is cached");
        ((UIImageView*)self.imageView).image = image;
    }
    else{

        NSURL *imageURL = [NSURL URLWithString:imageInfo.path];
        UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:imageURL]];

        if(image)
        {
            NSLog(@"Caching ....");
            [[Sample sharedInstance] cacheImage:image forKey:imageInfo.path];
            ((UIImageView*)self.imageView).image = image;
        }

    }


    [self addSubview:self.imageView];

    [self configureForImageSize:imageSize];
}

我建议在滚动时不要从其父视图中删除视图,而是绕过此问题。添加视图是非常繁重的任务。再加上图像加载,对于像智能手机这样的小型CPU来说可能会非常沉重(因为它们还没有GPU)。为了强调这一点,甚至苹果也提到了一旦显示UIImages就不会重新渲染它们,措辞很微妙here,但它显然没有提到优化删除、重新添加和渲染视图后它们被显示一次(例如在这种情况下)。我认为这里的预期用途是显示ImageView,并在控制器显示后简单地更改其image元素。

虽然图像对象支持所有平台本机图像格式,但建议您在应用程序中使用PNG或JPEG文件来处理大多数图像。图像对象针对读取和显示这两种格式进行了优化,并且这些格式比大多数其他图像格式具有更好的性能。

这就是为什么视图通常在它们的父视图上添加/初始化,然后再加载任何可见的加载方法,如viewWillAppearviewDidAppear,或者如果是在初始加载后完成,则它们很少被取消初始化,它们的内容通常是唯一被更改的东西,即使这样做,也通常是异步完成(如果从网络下载),或者是从缓存中完成的,某些初始化程序还可以自动完成此操作(您可以将其添加到我推荐的内容中):

使用imageNamed:inBundle:compatibleWithTraitCollection:方法(或 imageNamed:方法)从应用程序的主包(或其他已知包)中创建图像。由于这些方法会自动缓存图像数据,因此特别推荐经常使用的图像。

就个人而言,我会尝试采用UICollectionViews的方法。值得注意的是,它们有代理来自动处理内容缓存,当视图滚出窗口时(这正是此演示的情况)。您也可以在这些方法中添加自定义代码,以更好地控制这些视图上的滚动效果。它们可能一开始有点棘手,但我可以证明,您想要实现的目标可以使用比此演示使用的代码少得多的代码来复制。我还会把这个演示是在2012年构建的事实作为提示...它是一个非常古老的演示,而UICollectionViews则出现在此演示最后更新的时间。因此,我认为这正是苹果一直以来的目标,因为所有面向内容的UIView子类都从UIScrollView继承某种方式(UICollectionView、UITableView、UITextView等)。值得一看!UICollectionViews


谢谢您的反馈,但不幸的是,您完全错过了我的问题的重点。我没有任何缓存问题,重新创建图像是必要的,以便在内存中仅保留3个图像进行优化。因此,正如我在问题中已经描述的那样,我的问题是关于在图像之间滚动时出现的特定“卡住”的问题。 - Anton Malmygin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接