Dragging views on a scroll view: touchesBegan is received, but no touchesEnded or touchesCancelled

自古美人都是妖i 提交于 2019-12-04 07:28:10

The problem here is that removing a view from the view hierarchy confuses the system, the touch is lost. It is the same issue (internally gesture recognizers use the same touchesBegan: API).

https://github.com/LeoNatan/ios-newbie/commit/4cb13ea405d9f959f4d438d08638e1703d6c0c1e (I created a pull request.)

What I changed was to not remove the tile from the content view when touches begin, but only move on touches end or cancel. But this creates a problem - when dragging to the bottom, the tile is hidden below the view (due to scrollview clipping to its bounds). So I created a cloned tile, add it as a subview of the view controller's view and move that together with the original tile. When touches end, I remove the cloned tile and place the original where it should go.

This is because the bottom bar is not part of the scrollview hierarchy. If it was, the entire tile cloning would not be necessary.

I also streamlined the positioning of tiles quite a bit.

you could set the userInteractionEnabled of the scrollview to NO while you are dragging the tile, and set it back to YES when the tile dragging ended.

You should really try using a gesture recognizer instead of the raw touchesBegan/touchesMoved. I say this because UIScrollView is using gesture recognizers and by default these will cede to any higher-level gesture recognizer that is running.

I put together a sample that has a UIScrollView with an embedded UIImageView. As with your screenshot, below the scrollView I have some UIButton "Tiles", which I subclassed as TSTile objects. The only reason I did this was to expose some NSLayoutConstraints to access/alter their height/width (since you're using auto layout vs. frame manipulation). The user can drag tiles from their starting place into the scroll view.

This seems to work well; I didn't hook up the ability to drag a tile once it is re-parented in the scrollview. But that shouldn't be too hard. For that you might consider placing a long-tap gesture recognizer in each tile, then when it fires you would turn off scrolling in the scrollview, such that the top-level pan gesture recognizer would kick in.

Or, you might be able to subclass the UIScrollView and intercept the UIScrollView's pan-gesture-recognizer delegate callbacks to hinder panning when the user starts from a tile.

@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

I also think you should use a UIGestureRecognizer, and more precisely a UILongPressGestureRecognizer on each tile that once recognized will handle pan.

For fine grained control you can still use the recognizers' delegate.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!