views:

941

answers:

1

There seem to be many questions asked about this subject here on stackoverflow, but none of them touch on the updates made in 3.0. After mucking around for hours on end I finally found out, that nested scroll views (in my case web views inside a scroll view) are fully supported, however the example given at http://developer.apple.com/iphone/library/documentation/WindowsViews/Conceptual/UIScrollView_pg/Introduction/Introduction.html is pretty basic.

I have a main scroll view with paging enabled, with web views laid out as subviews, so that I can page left and right changing different web views, but also scroll up and down inside the subviews.

In essence this seems to work fine, however what I can't figure out is how to stop the parent scroll view from paging left or right once the user has already started scrolling the web view. Essentially I'd like to lock the scrolling to whichever direction it started with. Funnily enough, this works fine if I start paging first, but if I start scrolling up or down first it also lets page at the same time (during the same began-moved-ended cycle).

The stocks app for example locks the scrolling properly.

A: 

Since UIWebView doesn't play very nicely with the automagical nested scrolling introduced in 3.0 and sending touchesBegin/Moved/Ended to a UIScrollView is no longer supported here's what I came up with.

I added a transparent UIView subclass on top of all of the other views and made it catch all touches. When touches begin I forward that to the currently active UIWebView and start a short timer (or rather another thread that uses usleep() to sleep for a little bit - in my experience the main thread can get locked up when a lot of touch events are incoming and timers can go way off).

In touchesMoved I check if the timer I started hasn't expired - if it hasn't and fabs(location.x - lastLocation.x) > fabs(location.y - lastLocation.y) then it looks like the user is trying to page (this could be adjusted with a multiplier, but for now this seems to be just the sweet spot). If it's been determined that the user is trying to page I send the web view touchesCancelled and start adjusting the scroll view's contentOffset.x accordingly.

In touchesEnded, if it's been determined that the user was paging, I apply some Newtonian physics to see if the touch, along with some kinetic continuation of the scrolling would've been enough to cross over to the next page (more than half of the next page is visible). If so I animate the scroll to continue based on the speed of the scroll.

Warning: This code is horrible due to trying a billion different things. I haven't had a chance to clean it up yet. Hope this helps someone.

-(void)timer {
    usleep(250000);
    expired = YES;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    paging = NO;
    expired = NO;
    determined = NO;


    if(navManager == nil) {
        navManager = [[[[UIApplication sharedApplication] delegate] viewController] navManager];
    }

    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self];

    touchStartX = location.x;
    touchStarted = touch.timestamp;

    [NSThread detachNewThreadSelector:@selector(timer) toTarget:self withObject:nil];

    [[navManager.currentNavItem.webView hitTest:CGPointZero withEvent:event] touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    if(!paging) {
        [[navManager.currentNavItem.webView hitTest:CGPointZero withEvent:event] touchesCancelled:touches withEvent:event];
    }

}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if(!paging) {
        [[navManager.currentNavItem.webView hitTest:CGPointZero withEvent:event] touchesEnded:touches withEvent:event];
    }
    else {
        UITouch *touch = [touches anyObject];
        CGPoint location = [touch locationInView:self];
        NSTimeInterval touchLasted = touch.timestamp - touchStarted;
        CGFloat touchLen = location.x - touchStartX;
        float dir = touchLen/fabs(touchLen);
        float touchSpeed = touchLen/touchLasted;
        float deAccelRate = -3000.0;
        float timeToDeAccel = (-touchSpeed) / deAccelRate;
        float averageVelocity = touchSpeed / 2.0;
        float couldTravel = averageVelocity*timeToDeAccel;

        if(couldTravel > navManager.scrollView.frame.size.width/2.0) {
            couldTravel = navManager.scrollView.frame.size.width/2.0;
        }
        couldTravel = dir*couldTravel;

        NSLog(@"could travel: %f, touchSpeed: %f, timeToDeAccel = %f, averageVelocity: %f", couldTravel, touchSpeed, timeToDeAccel, averageVelocity);

        int page = round((navManager.scrollView.contentOffset.x - couldTravel) / navManager.scrollView.frame.size.width);
        if(page < 0) 
            page = 0;
        else if(page > round(navManager.scrollView.contentSize.width / navManager.scrollView.frame.size.width) - 1) 
            page = round(navManager.scrollView.contentSize.width / navManager.scrollView.frame.size.width) - 1;

        CGPoint newOffset = CGPointMake(page*navManager.scrollView.frame.size.width, navManager.scrollView.contentOffset.y);
        float needToMove = fabs(newOffset.x - navManager.scrollView.contentOffset.x);
        float timeToAnimate = needToMove / averageVelocity;

        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDelegate:nil];
        [UIView setAnimationDuration:timeToAnimate];
        [UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
        navManager.scrollView.contentOffset = newOffset;
        [UIView commitAnimations];
    }
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self];
    CGPoint lastLocation = [touch previousLocationInView:self];

    if(!determined && !expired) {

        if(fabs(location.x - lastLocation.x) > fabs(location.y - lastLocation.y)) {
            NSLog(@"PAGE!!");
            paging = YES;
            [[navManager.currentNavItem.webView hitTest:CGPointZero withEvent:event] touchesCancelled:touches withEvent:event];
        }
        else
            [navManager.scrollView touchesCancelled:touches withEvent:event];

        determined = YES;
    }

    if(!paging) 
        [[navManager.currentNavItem.webView hitTest:CGPointZero withEvent:event] touchesMoved:touches withEvent:event];

    else {
        float xScroll = navManager.scrollView.contentOffset.x-(location.x - lastLocation.x);

        CGPoint newOffset = CGPointMake(xScroll, navManager.scrollView.contentOffset.y);
        navManager.scrollView.contentOffset = newOffset;
    }
}

There's a few more things to add to make it feel more native, but this should be a good starting point.

Joonas Trussmann