给UICollectionView添加捏合缩放功能

32

简介

我将描述我想要实现的效果,然后详细说明我目前正在尝试如何实现它以及当前行为存在什么问题。我还会提到另一种我看过但无法完全实现的方法。

最相关的代码是内联在问题底部以便快速访问。您可以下载源代码zip文件或者在BitBucket上获取作为一个Mercurial Repository的项目。该项目现已整合了下面回答中的修复。如果您想要最初提供的错误版本,请使用标记"initial-buggy-version"

该项目是一个相对简单的概念验证/示例,以评估效果是否可行!

期望效果

应用程序将显示大量离散的信息行,形成垂直表格。用户可以通过垂直滚动来滚动表格。这是UITableView的标准行为,您也可以使用UICollectionView。但是,该应用程序还必须支持捏合缩放。当您对表格进行捏合缩放时,所有行将挤在一起。随着您的拉伸,所有的行都会拉开。

在我的概念验证中,单个单元格不会被调整大小,只会靠近或远离重新定位。这是有意为之的:我认为这并不是验证该想法可行性的关键。

下面是屏幕截图,显示当前应用程序缩小和放大的情况:

Zoomed in image Zoomed out image

目前的实现

我正在使用自定义的UICollectionViewLayout子类和UICollectionView。布局将UICollectionViewCells以一个漂亮且弯曲的正弦波形式放置在屏幕中央。每个UICollectionViewCell只是包含一个持有indexPath行的UILabel的容器。

UICollectionViewLayout子类具有设置垂直间距的参数,该参数描述了其所述的每个单元格与UICollectionView之间的垂直间距,并调整此间距可以根据需要垂直压缩或拉伸表格。

我的UICollectionViewController子类具有一个UIPinchGestureRecognizer。当识别器检测到比例变化时,更改UICollectionView布局中的垂直单元格间距。

未经进一步考虑,缩放将发生在内容顶部而非触摸手势中心。为了提供此功能,在收缩期间会调整UICollectionViewcontentOffset属性。

手势识别器还需要适应在收缩时发生的拖动。这也通过更改UICollectionViewcontentOffset来处理。一些额外的代码允许随着手指添加/移除而更改触摸手势的中心点。

请注意,UICollectionView作为UIScrollView的子类具有自己的UIPanGestureRecognizer,它与我添加的UIPinchGestureRecogniser交互。我不确定这是否会导致问题。

我已添加代码以在我的捏合手势期间禁用UICollectionView的内置滚动,但似乎并没有什么区别。我尝试使用gestureRecognizer:shouldRequireFailureOfGestureRecognizer:使我的UIPinchGestureRecognizer失败内置的UIPanGestureRecognizer,但这似乎停止了我的捏合手势识别器完全工作。我不知道这是我傻还是iOS中的一个错误。

如前所述,当前UICollectionViewCell未调整大小。它们只是重新定位。这是有意的。我认为这对验证此概念并不重要。

工作正常

可工作的部分效果相当不错。您可以拖动表格上下。在拖动过程中,您可以添加手指并开始收缩,然后释放手指并继续拖动,然后添加和收缩,等等。一切都非常流畅。在原始iPhone 5上,它可以平稳地支持屏幕上超过200个视图的捏合和滑动。

不起作用的问题1

如果尝试在视图顶部或底部处收缩,则会变得混乱。

  • 在滚动时,允许将视图拖动以使其超出可见内容(这是我希望的,因为这是iOS上数据列表的标准行为)。
  • 然而,在更改比例时,视图被弹回,使内容夹紧到屏幕上(我不希望发生这种情况)。

这两个在捏合手势期间互相斗争,这使内容剧烈上下闪烁(我绝对不想要!)。

不起作用的问题2

UICollectionView的默认滚动具有减速,如果在滚动时放手,则还会平稳地将内容弹回到滚动范围内。目前这些都没有处理。

  • 如果您在滚动过程中放开捏合手势,它会停止滚动。
  • 如果您使用捏合手势滚动超出内容并释放,则它会停留在那里而不弹回。当您再次开始滚动时,它会跳回到内容。

我尝试过但无法使其工作的事情

UICollectionView 作为 UIScrollView 应该具有内置的 UIPinchGestureRecogniser ,如果正确设置支持缩放,我想知道是否可以利用这一点,而不是拥有自己的 UIPinchGestureRecogniser 。我尝试通过设置最小和最大比例,并添加我的控制器的捏合处理程序来设置此项。然而,我不太理解应该从我的实现中返回什么 viewForZoomingInScrollView:,所以我只是用 [[UIView alloc] initWithFrame:[[self collectionView] bounds]] 创建了一个虚拟视图。它使滚动视图“崩溃”成一行,这不是我想要的!

最后(代码之前)

这是一个很长的问题,感谢您阅读。如果您能给出答案,更加感谢。如果我说或添加的很多内容与问题无关,我很抱歉!

视图控制器的代码

//  STViewController.m
#import "STViewController.h"
#import "STDataColumnsCollectionViewLayout.h"
#import "STCollectionViewLabelCell.h"

@interface STViewController () <UIGestureRecognizerDelegate>
@property (nonatomic, assign) CGFloat pinchStartVerticalPeriod;
@property (nonatomic, assign) CGFloat pinchNormalisedVerticalPosition;
@property (nonatomic, assign) NSInteger pinchTouchCount;
-(void) handlePinch: (UIPinchGestureRecognizer *) pinchRecogniser;
@end

@implementation STViewController

-(void) viewDidLoad
{
  [[self collectionView] registerClass: [STCollectionViewLabelCell class] forCellWithReuseIdentifier: [STCollectionViewLabelCell className]];

  UICollectionView *const collectionView = [self collectionView];
  [collectionView setAllowsSelection: NO];

  [_pinchRecogniser addTarget: self action: @selector(handlePinch:)];
  [_pinchRecogniser setDelegate: self];
  [_pinchRecogniser setCancelsTouchesInView:YES];
  [[self view] addGestureRecognizer: _pinchRecogniser];
}

#pragma mark -

-(NSInteger) collectionView: (UICollectionView *)collectionView numberOfItemsInSection: (NSInteger)section
{
  return 800;
}

-(UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
  STCollectionViewLabelCell *const cell = [[self collectionView] dequeueReusableCellWithReuseIdentifier: [STCollectionViewLabelCell className] forIndexPath: indexPath];
  [[cell label] setText: [NSString stringWithFormat: @"%d", [indexPath row]]];
  return cell;
}

#pragma mark -

-(BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
  return YES;
}

#pragma mark -

-(void) handlePinch: (UIPinchGestureRecognizer *) pinchRecogniser
{
  UICollectionView *const collectionView = [self collectionView];
  STDataColumnsCollectionViewLayout *const layout = (STDataColumnsCollectionViewLayout *)[self collectionViewLayout];

  if(([pinchRecogniser state] == UIGestureRecognizerStateBegan) || ([pinchRecogniser numberOfTouches] != _pinchTouchCount))
  {
    const CGFloat normalisedY = [pinchRecogniser locationInView: collectionView].y / [layout collectionViewContentSize].height;
    _pinchNormalisedVerticalPosition = normalisedY;
    _pinchTouchCount = [pinchRecogniser numberOfTouches];
  }

  switch ([pinchRecogniser state])
  {
    case UIGestureRecognizerStateBegan:
    {
      NSLog(@"Began");
      _pinchStartVerticalPeriod = [layout verticalPeriod];
      [collectionView setScrollEnabled: NO];
      break;
    }

    case UIGestureRecognizerStateChanged:
    {
      NSLog(@"Changed");
      STDataColumnsCollectionViewLayout *const layout = (STDataColumnsCollectionViewLayout *)[self collectionViewLayout];
      const CGFloat newVerticalPeriod = _pinchStartVerticalPeriod * [pinchRecogniser scale];
      [layout setVerticalPeriod: newVerticalPeriod];
      [[self collectionViewLayout] invalidateLayout];

      const CGPoint dragCenter = [pinchRecogniser locationInView: [collectionView superview]];
      const CGFloat currentY = _pinchNormalisedVerticalPosition * [layout collectionViewContentSize].height;
      [collectionView setContentOffset: CGPointMake(0, currentY - dragCenter.y) animated: NO];
    }

    case UIGestureRecognizerStateEnded:
    case UIGestureRecognizerStateCancelled:
    {
      [collectionView setScrollEnabled: YES];
    }

    default:
      break;
  }
}

@end

这个问题是关于在另一个视图中模拟滚动视图的感觉。其中一个答案建议使用来自WWDC演示文稿中标题为“增强滚动视图用户体验”的技术,该演示文稿从25:00开始展示。它使用一个没有任何内容的虚拟滚动视图,用户与之交互,以控制您真正的内容视图的位置和比例。我可能会尝试这个。 - Benjohn
我一直在研究虚拟的UIScrollView方法,看起来很有希望。但令人不安的是,在iOS 7上,苹果自己的照片应用程序在缩放和平移方面存在与内容边界相关的错误,因此我不确定它是否是一个完美无缺的解决方案。 - Benjohn
苹果的iOS照片应用在iOS 6上也存在bug。如果你将图像拉出其自然边界并开始捏合手势,这个问题会在6和7上发生。图像会立即跳回一部分距离,回到它被拉出的边缘。 - Benjohn
我在 What Doesn't Work 1 方面取得了一些进展:当我的 UIPinchGestureRecognizer 减少集合视图项间距时,集合视图在视觉上被夹紧,以至于内容被拉回到屏幕顶部,而不是像拖动时那样“弹跳”(愚蠢的名字)。我还不理解它,但我认为它与 UICollectionView 继承的缩放机制有关。如果我提供一个 viewForZoomingInScrollView 并设置最小和最大缩放比例,问题就会消失。 - Benjohn
1
@departamento 哇哦 - 可能吧。我得看看我是否有这个存储库,如果有的话,就把它放到 GitHub 上。但是,这是非常古老的代码,而且问题也很旧了。虽然可能可以构建,但我可能只会将其原样发布。 - Benjohn
显示剩余2条评论
1个回答

18

好的部分 - 如何使其运作

对上述代码进行一些非常小的调整,解决了问题中的不起作用1不起作用2

我已经在我的UICollectionViewControllerviewDidLoad方法中添加了以下行:

[collectionView setMinimumZoomScale: 0.25];
[collectionView setMaximumZoomScale: 4];

我还更新了示例项目,使视图由小圆圈组成而不是文本标签。当您缩放时,它们会被重新调整大小。以下是现在的样子(缩小和放大):

Image zoomed out Image zoomed in

在缩放过程中,圆圈的视图并没有重新绘制,而只是从它们的预缩放大小插值插值计算出来。重绘被推迟到缩放完成后。这是缩小多次后的截图:

During zoom

最好在后台线程中进行缩放期间的重绘,以使伪影不太明显,但这超出了本问题的范围,而且我也没有解决它。

您可以在 Bit Bucket 上找到整个已修复项目,因此可以从那里获取文件

糟糕的部分-我不知道它为什么能工作

我希望通过这个问题的解答,我对UIScrollView缩放有更多的确定性。但我没有。

根据我对UIScrollView的了解,这个“修复”不应该有任何影响,而且它本应该在第一次就能正常工作。

一个UIScrollView只有在您提供一个实现viewForZoomingInScrollView:的代理之后才能启用滚动,而我并没有这样做。


1
除了“不好”的部分,我对结果也不完全满意。有时在滚动和缩放期间,视图会突然跳过一帧。我可能会调查一个略微不同的方法,在其中UICollectionView的用户交互被完全关闭,并在其前面放置一个虚拟的UIView以及UIPanGestureRecogniserUIPinchGestureRecogniser处理用户交互。这将需要重新创建UIScrollView的减速和反弹行为,但我不确定这是否是个大问题,因为它值得解决所有问题。 - Benjohn
这是一个很棒的项目,帮助我解决了关于 UICollectionView 的问题。 - Alex Cio
我也不知道为什么它能工作,因为UICollectionView没有这样的方法... - James Bush
@JamesBush viewForZoomingInScrollView: 是一个 UIScrollViewDelegate 方法。UICollectionViewUIScrollView 的子类,因此继承了 delegate 属性。但是您的意思是指其他什么吗? - Benjohn
要使用其中一半的方法,滚动视图和内容视图必须是两个不同的视图;集合视图是一个滚动和内容视图合并在一起的视图。UICollectionView 可以从 UIScrollView 继承属性和方法,但并非所有方法都适用。 - James Bush

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