Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIScrollView within a UIScrollView, how to keep a smooth transition when scrolling between the two

I have the following layout (see below), which for most circumstances works just fine. The user is able to scroll within the blue UIScrollView (which for all intents and purposes is a UITableView, but this question generalises this), and then when they've reached the end of this scroll view, they can start scrolling again (they have to take their finger off, and on again, because the inner scroll view rubberbands otherwise), and the 'super' scroll view starts scrolling, revealing the rest of the image.

It's the whole (they have to take their finger off, and on again, because the inner scroll view rubberbands otherwise) that I don't want. Ideally, once the contained UIScrollView reaches the end of its content, I want the superview to take over scrolling straight away, so that the inner scroll view doesn't rubberband.

The same goes when the user is scrolling back up; when the red scrollview reaches the top of it's content, I want the inner blue scroll view to start scrolling up straight away, instead of the red scroll view rubberbanding at the top.

Screen layout; UIScrollView + UIImageView within a UIScrollView

Any idea how? I am able to determine when the scroll views have reached the ends of their content, but I'm not sure how to apply this knowledge to achieve the effect I'm after. Thanks.

// Inner (blue) scroll view bounds checking
if (scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height) { ... }

// Outer (red) scroll view bounds checking
if (scrollView.contentOffset.y < 0) { ... }
like image 473
WDUK Avatar asked Nov 04 '12 18:11

WDUK


1 Answers

Yeah. Ive got a nifty trick for you.

Instead of having your red outlined view a scrollview make it a normal UIView that fills the screen. In that view lay out your scroll view (table view) and image view as they are in your illustration.

Place a scrollview that fills the bounds of the root view (i.e. also fills the screen) above all the other scrollview and image views. Set the content size of this view to be the total content height of all the views you want to scroll through. In otherwords there is an invisible scrollview sitting on top of all your other views and its content size height is inner scrollview (tableview) content size height + image view size height.

The heierarchy should look like this:

enter image description here

Then this scrollview on top that you have made with the really tall content size make its delegate be your view controller. Implement scrollViewDidScroll and we'll work some magic.

Scrollviews basically scroll by adjusting the bounds origin with funky formulas for momentum and stuff. So in our scrollviewDidScroll method we will simply adjust the bounds of the underlying views:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView{

    //the scroll view underneath (in the container view) will have a max content offset equal to the content height
    //but minus the bounds height
    CGFloat maxYOffsetForUnderScrollView = self.underScrollView.contentSize.height - self.underScrollView.bounds.size.height;

    CGRect scrolledBoundsForContainerView = self.view.bounds;

    if (scrollView.contentOffset.y <= maxYOffsetForUnderScrollView) {
        //in this scenario we are still within the content for the underScrollView
        //so we make sure the container view is scrolled to the top and set the offset for the contained scrollview
        self.containerView.bounds = scrolledBoundsForContainerView;
        self.underScrollview.contentOffset = scrollView.contentOffset;
        return;
    }

    //in this scenario we have scrolled throug the entirety of the contained scrollview
    //set its offset to the max and change the bounds of the container view to scroll everything else.
    self.underScrollView.contentOffset = CGPointMake(0, maxYOffsetForUnderScrollView);

    scrolledBoundsForContainerView.origin.y = scrollView.contentOffset.y - maxYOffsetForUnderScrollView;
    self.containerView.bounds = scrolledBoundsForContainerView;
}

You will find that as scrollViewDidScroll is called every frame of animation that this faux scrolling of the contained views looks really natural.

But wait! I hear you say. That scroll view on top now intercepts ALL touches, and the views underneath it need to be touched as well. I have an interesting solution for that as well.

Set the scrollview on top to be off screen somewhere (i.e. set its frame off screen, but still the same size.) and then in your viewDidLoad method you add the scrollview's panGestureRecogniser to the main view. This will mean that you get all the iOS natural scrolling momentum and stuff without actually having the view on the screen. The contained scroll view will now probably go juddery as its pan gesture recognizer will get called as well (they work differently to UIEvent handling) so you will need to remove it.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self.view addGestureRecognizer:self.scrollview.panGestureRecognizer];

    [self.underScrollview removeGestureRecognizer:self.underScrollView.panGestureRecognizer];

    //further code to set up content sizes and stuff
}

I had fun making this so heres a link to the sample project on github: https://github.com/joelparsons/multipleScrollers

EDIT:

To show the scrollbar for the top scrollview when its off the screen no matter where you put it you can set the scrollIndicatorInsets to an inset created like this:

CGPoint scrollviewOrigin = self.scrollview.frame.origin;
self.scrollview.scrollIndicatorInsets = UIEdgeInsetsMake(-scrollviewOrigin.y,0,scrollviewOrigin.y,scrollviewOrigin.x);

*caveat that the scrollview still has to be the right height but I'm sure you get the idea.

And then to make the bar draw outside the scrollview's visible bounds you have to turn off clips to bounds

self.scrollview.clipsToBounds = NO;
like image 152
jackslash Avatar answered Sep 22 '22 01:09

jackslash