Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sticky scrollview subview has incorrect frame after scrolling fast. Compilable example

I have spent the past 12 hours futzing with this and I'm going braindead.

view on bitbucket: https://bitbucket.org/burneraccount/scrollviewexample

git https: https://bitbucket.org/burneraccount/scrollviewexample.git

git ssh: [email protected]:burneraccount/scrollviewexample.git

^ That is a condensed, self-contained Xcode project that exemplifies the problem I'm having in my real work.

Summary

I am trying to achieve a static-image effect, where the image (a rock in the example) appears stuck to the screen while the lower content scrolls upwards appearing to go above the scrollview.

There's a screen-sized (UIScrollView*)mainScrollView. This scrollview has a (UIView*)contentView. contentView is ~1200 pts long and has two subviews: (UIScrollView*)imageScroller and (UITextView*)textView.

one

All works well until you scroll up a little bit, then scroll down really fast. The resulting position after movement stops is incorrect. It somehow doesn't update properly in the scrollviewDidScroll delegate method. The fast downward scroll (only after you've dragged up) behaves somewhat correctly, but still results in a misplaced view:

two

gently dragging does almost nothing, and the velocity of the scroll is directly related to the incorrect offset.

Here is the offending code:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (scrollView == self.mainScrollView)
    {

        CGFloat mainOffset = self.mainScrollView.contentOffset.y;
        if (mainOffset > 0 && mainOffset < self.initialImageScrollerFrame.size.height)
        {
            CGRect imageFrame = self.imageScroller.frame;
            CGFloat offset = mainOffset-self.lastOffset;
            self.imageScroller.frame = CGRectMake(imageFrame.origin.x,
                                                  imageFrame.origin.y + offset, // scroll up
                                                  imageFrame.size.width,
                                                  imageFrame.size.height - offset); // hide the bottom of the image
            self.lastOffset = mainOffset;
        }

    }
}

I've restructured this in almost every way I could imagine and it always has similar effects. The worst part is how the very simple and straightforward code fails to do what is seems like it's guaranteed to do. Also odd is that the zoom-effect I use in my real project works fine using the same mechanism to size the same view element. It runs inside (contentOffset < 0) instead of (contentOffset > 0), so it only zoom in on the imageScroller when you're pulling the view below it's normal offset. That leads me to conspire that some data is lost as it crosses the contentOffset 0, but my conspiracies have been shot down all day.

This is example is a lot less complicated than the real project I'm working on, but it properly reproduces the problem. Please let me know if you can't open my project, or can't connect to the repo.

There is likely a largely obvious fix for this, but I am hours past the point of having any new perspective. I will be thoroughly amazed if I ever get this to work.

like image 553
user Avatar asked Nov 11 '22 16:11

user


1 Answers

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (scrollView == self.mainScrollView)
    {

        CGFloat mainOffset = self.mainScrollView.contentOffset.y;
        if (mainOffset >= 0 && mainOffset < self.initialImageScrollerFrame.size.height)
        {
            CGRect imageFrame = self.imageScroller.frame;
            CGFloat offset = self.mainScrollView.contentOffset.y-self.lastOffset;
            if ( imageFrame.origin.y + offset > 0) {  //[2]
                self.imageScroller.frame = CGRectMake(imageFrame.origin.x,
                                                      imageFrame.origin.y + offset, 
                                                      imageFrame.size.width,
                                                      imageFrame.size.height - offset); 
                self.lastOffset = mainOffset;
            }
        }else if (mainOffset < 0) {  //[1]
            self.imageScroller.frame = self.initialImageScrollerFrame;
        }
    }
}

[1] - When your mainOffset goes below zero, you need to reset your imageScroller frame. ScrollViewDidScroll does not register with every pixel's-worth of movement, it is only called every so often (try logging it, you will see). So as you scroll faster, it's offsets are further apart. This can result in a positive scroll of say +15 becoming a negative scroll on the next call to scrollViewDiDScroll. So your imageScroller.frame.y may get stuck on that +15 offset even when you expect it to be zero.Thus as soon as you detect a negative value for mainOffset, you need to reset your imageScroller frame.

[2] - similarly here you need to ensure here that your imageScroller.frame.y will always be +ve.

These fixes do the job, but I would still consider them "ugly and not production-worthy". For a cleaner approach you might want to reconsider the interplay between these views, and what you are trying to achieve.

By the way, your 'image.png' is actually a jpg. While these renders fine on the simulator, it will break (with compiler errors) on a device.

like image 126
foundry Avatar answered Nov 15 '22 05:11

foundry