在滚动视图上拖动视图:已收到 touchesBegan,但没有 touchesEnded 或 touchesCancelled

Posted

技术标签:

【中文标题】在滚动视图上拖动视图:已收到 touchesBegan,但没有 touchesEnded 或 touchesCancelled【英文标题】:Dragging views on a scroll view: touchesBegan is received, but no touchesEnded or touchesCancelled 【发布时间】:2014-03-22 19:45:34 【问题描述】:

作为一名 ios 编程新手,我正在为a word game for iPhone 苦苦挣扎。

应用结构为:scrollView -> contentView -> imageView -> image 1000 x 1000(此处为fullscreen):

我想我终于明白了如何在 Xcode 5.1 中使用启用自动布局的UIScrollView

我只是为 contentView 指定了足够的约束(尺寸 1000 x 1000 和 0 到父级),这定义了 _scrollView.contentSize(我不必显式设置) - 之后我的游戏板滚动和缩放就好了。

但是我在Tile.m 中实现的可拖动字母图块遇到了问题。

我使用touchesBegan, touchesMoved, touchesEnded, touchesCancelled 而不是手势识别器(*** 用户经常建议),因为我在touchesBegan 上显示带有阴影的较大字母平铺图像(bigImage)。

我的拖拽是这样实现的:

    touchesBegan 中,我从contentView 中删除磁贴(并将其添加到主应用程序视图)并显示bigImage 和阴影。 在touchesMoved我移动磁贴 在touchesEndedtouchesCancelled 中,我再次显示带有阴影的smallImage,然后 - 将磁贴添加到contentView 或将其保留在主视图中(如果磁贴位于应用程序的底部)。

我的问题:

大部分情况下这有效,但有时(经常)我看到只有 touchesBegan 被调用,但其他 touchesXXXX 方法从未被调用:

2014-03-22 20:20:20.244 ScrollContent[8075:60b] -[Tile touchesBegan:withEvent:]: Tile J 10 367.15002, 350.98877 57.599998, 57.599998

scrollView 是用手指滚动的,位于大图块下方。

这会导致我的应用屏幕上出现许多带有阴影的大图块,而滚动视图被拖到它们下方:

请问如何解决?

通过查看流行的文字游戏,我确定我的应用程序结构(自定义 UIViews 拖入/拖出 UIScrollView)是可能的。

我使用 tile.exclusiveTouch = YES 和 a custom hitTest method for the contentView - 但这无济于事:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

    UIView* result = [super hitTest:point withEvent:event];

    return result == self ? nil : result;

更新 1:

我尝试将以下代码添加到handleTileTouched

_contentView.userInteractionEnabled = NO;
_scrollView.userInteractionEnabled = NO;
_scrollView.scrollEnabled = NO;

然后在ViewController.m 的handleTileReleased 中将其设置回YES - 但这无济于事,而且对我来说更像是一种黑客攻击。

更新 2:

可能已经阅读了与 UIScrollViewhitTest:withEvent:pointInside:withEvent: 相关的所有内容 - 在网络上(例如 Hacking the responder chain 和 Matt Neuburg 的 Programming iOS 书)、*** 和 Safari,在我看来,一个解决方案是为我的应用程序的主视图实现hitTest:withEvent: 方法:

如果Tile 对象被命中,它应该被返回。否则 - 应返回 scrollView

很遗憾,这不起作用 - 我可能遗漏了一些小问题。

而且我确信存在一个很好的解决方案 - 通过研究适用于 iOS 的流行文字游戏。例如,在 Zynga 的 Words with Friends ® 应用程序中,字母拼贴的拖动和放置非常流畅,在下面的屏幕截图中,您可以看到它们可能使用 UIScrollView(滚动条在角落可见)并显示拼贴阴影(可能在 @ 987654369@方法):

更新 3:

我创建了一个新项目来测试 TomSwift 建议的 gesture recognizer,它显示了我在使用手势识别器时遇到的问题:磁贴大小更改得太晚 - 当用户开始移动磁贴而不是在他触摸它的那一刻:

【问题讨论】:

啊,法伯先生和永无止境的文字游戏问题。 :D :-) 我的(移动和桌面)纸牌游戏处于相同阶段,现在(从 4 岁开始)它可以正常工作:facebook.com/appcenter/video-preferans 我只需要通过大量愚蠢的问题,然后那我就好了。请耐心等待我的 iOS 新手。 我想我知道问题出在哪里。如果我注释掉通知帖子,它工作“正常”,没有触摸进入滚动视图 【参考方案1】:

这里的问题是从视图层次结构中删除视图会使系统感到困惑,失去了触摸。这是同一个问题(内部手势识别器使用相同的touchesBegan: API)。

https://github.com/LeoNatan/ios-newbie/commit/4cb13ea405d9f959f4d438d08638e1703d6c0c1e (我创建了一个拉取请求。)

我所做的更改是在触摸开始时不从内容视图中删除磁贴,而仅在触摸结束或取消时移动。但这会产生一个问题 - 当拖到底部时,图块隐藏在视图下方(由于滚动视图剪切到其边界)。所以我创建了一个克隆图块,将其添加为视图控制器视图的子视图,并将其与原始图块一起移动。当触摸结束时,我移除克隆的图块并将原图放在它应该去的地方。

这是因为底部栏不是滚动视图层次结构的一部分。如果是,则不需要整个磁贴克隆。

我还大大简化了图块的定位。

【讨论】:

@AlexanderFarber 我不确定。根据我的经验,alpha 0 仍然会导致事情正常运行,而 hidden 会导致他们跳过某些东西——这就是我选择 alpha 的原因。 @AlexanderFarber 缩小并将磁贴拖到内容视图之外。这可以确保如果 tile 落在内容视图之外,它会返回到堆栈中。 @AlexanderFarber 我想在您开始对 iOS 感到更舒服并开始制作真正的应用程序时成为一名 beta 测试员。 :-) 太棒了,我计划 iOS + android + Facebook(用于桌面,在 Flex + jQuery 中)应用程序...通过 Urban Airship + 套接字通知...您认为移动从Tile.mViewController.mtouchesXXX 方法? 是的,是的。控制器应该处理视图逻辑。【参考方案2】:

您可以在拖动磁贴时将滚动视图的userInteractionEnabled 设置为NO,并在磁贴拖动结束时将其设置回YES

【讨论】:

我尝试在handleTileTouched 中将_scrollView.userInteractionEnabled 设置为NO 并在handleTileReleased 中设置YES - 它没有帮助(所以我再次评论它:@987654321 @ ) 另外,我怀疑这是一种解决方法...必须有更好的解决方案(例如仅将触摸事件传递给拖动的字母图块)。 您可以尝试使用 UIPanGestureRecognizer 代替。您可以使用this 快速实现 使用 UIPanGestureRecognizer 我无法显示带有阴影的 bigImage(当前显示在 touchesBegan - 请参阅我的原始问题)。 您可以使用 TapGesture 添加阴影,并使用 PanGesture 移动磁贴。这样你会立即看到阴影。 应该,你需要将你的视图控制器设置为委托并实现this。在 gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: 中返回 YES 将使您的两个手势都被识别。【参考方案3】:

您真的应该尝试使用手势识别器而不是原始的 touchesBegan/touchesMoved。我这样说是因为 UIScrollView 正在使用手势识别器,默认情况下这些将让给正在运行的任何更高级别的手势识别器。

我整理了一个包含 UIScrollView 和嵌入式 UIImageView 的示例。与您的屏幕截图一样,在 scrollView 下方我有一些 UIButton“Tiles”,我将其子类化为 TSTile 对象。我这样做的唯一原因是公开一些 NSLayoutConstraints 以访问/更改它们的高度/宽度(因为您使用的是自动布局与框架操作)。用户可以将磁贴从它们的起始位置拖到滚动视图中。

这似乎运作良好;一旦它在滚动视图中重新设置为父级,我就没有连接拖动磁贴的能力。但这不应该太难。为此,您可能会考虑在每个图块中放置一个长按手势识别器,然后当它触发时,您将关闭滚动视图中的滚动,这样***平移手势识别器就会启动。

或者,您也许可以继承 UIScrollView 并拦截 UIScrollView 的 pan-gesture-recognizer 委托回调,以在用户从磁贴开始时阻止平移。

@interface TSTile : UIButton
//$hook these up to width/height constraints in your storyboard!
@property (nonatomic, readonly) IBOutlet NSLayoutConstraint* widthConstraint;
@property (nonatomic, readonly) IBOutlet NSLayoutConstraint* heightConstraint;
@end

@implementation TSTile
@synthesize widthConstraint,heightConstraint;
@end

@interface TSViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
@end

@implementation TSViewController

    IBOutlet UIImageView*   _imageView;

    TSTile*                 _dragTile;


- (void)viewDidLoad

    [super viewDidLoad];

    UIPanGestureRecognizer* pgr = [[UIPanGestureRecognizer alloc] initWithTarget: self action: @selector( pan: )];
    pgr.delegate = self;

    [self.view addGestureRecognizer: pgr];


- (UIView*) viewForZoomingInScrollView:(UIScrollView *)scrollView

    return _imageView;


- (BOOL) gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer

    CGPoint pt = [gestureRecognizer locationInView: self.view];

    UIView* v = [self.view hitTest: pt withEvent: nil];

    return [v isKindOfClass: [TSTile class]];


- (void) pan: (UIGestureRecognizer*) gestureRecognizer

    CGPoint pt = [gestureRecognizer locationInView: self.view];

    switch ( gestureRecognizer.state )
    
        case UIGestureRecognizerStateBegan:
        
            NSLog( @"pan start!" );

            _dragTile = (TSTile*)[self.view hitTest: pt withEvent: nil];

            [UIView transitionWithView: self.view
                              duration: 0.4
                               options: UIViewAnimationOptionAllowAnimatedContent
                            animations:^

                                _dragTile.widthConstraint.constant = 70;
                                _dragTile.heightConstraint.constant = 70;
                                [self.view layoutIfNeeded];
                            
                            completion: nil];
        
            break;

        case UIGestureRecognizerStateChanged:
        
            NSLog( @"pan!" );

            _dragTile.center = pt;
        
            break;

        case UIGestureRecognizerStateEnded:
        
            NSLog( @"pan ended!" );

            pt = [gestureRecognizer locationInView: _imageView];

            // reparent:
            [_dragTile removeFromSuperview];
            [_imageView addSubview: _dragTile];

            // animate:
            [UIView transitionWithView: self.view
                              duration: 0.25
                               options: UIViewAnimationOptionAllowAnimatedContent
                            animations:^

                                _dragTile.widthConstraint.constant = 40;
                                _dragTile.heightConstraint.constant = 40;
                                _dragTile.center = pt;
                                [self.view layoutIfNeeded];
                            
                            completion:^(BOOL finished) 

                                _dragTile = nil;
                            ];
        
            break;

        default:
            NSLog( @"pan other!" );
            break;
    


@end

【讨论】:

【参考方案4】:

我还认为您应该在每个一旦识别将处理平移的图块上使用UIGestureRecognizer,更准确地说是UILongPressGestureRecognizer

对于细粒度控制,您仍然可以使用识别器的delegate

【讨论】:

以上是关于在滚动视图上拖动视图:已收到 touchesBegan,但没有 touchesEnded 或 touchesCancelled的主要内容,如果未能解决你的问题,请参考以下文章

拖动项目时在列表视图上创建自动滚动

滚动视图和图像拖动问题

如何将 UIImage 从滚动视图拖动到视图

将图像从滚动视图拖动到视图而不会影响其位置

如何通过拖动滑块来控制我的 Kivy 滚动视图?

**防止在水平滚动视图内拖动两个 CollectionView **