Parallax effect with UIScrollView subviews
Asked Answered
L

2

13

I'm trying to create a Parallax effect on a UIView inside a UIScrollView. The effect seems to work, but not so well.

  1. First i add two UIView sub-views to a UIScrollView and set the UIScrollViews contentSize.
  2. The Views sum up and create a contentSize of {320, 1000}.
  3. Then I implemented the following in scrollViewDidScroll:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
        CGFloat offsetY = scrollView.contentOffset.y;
    
        CGFloat percentage = offsetY / scrollView.contentSize.height;
    
        NSLog(@"percent = %f", percentage);
    
        if (offsetY < 0) {
    
            firstView.center = CGPointMake(firstView.center.x, firstView.center.y - percentage * 10);
    
        } else if (offsetY > 0){
    
            firstView.center = CGPointMake(firstView.center.x, firstView.center.y + percentage * 10);
    
        }
    }
    

These lines of code do create a parallax effect, but as the scrolling continues, the view does not return to it's original position if i scroll to the original starting position.

I have tried manipulating the views layers and frame, all with the same results.

Any Help will be much appreciated.

Lighthouse answered 5/3, 2014 at 15:16 Comment(4)
The conditional amounts to percentage = fabs(percentage);, and that seems wrong. How does it look when you replace the conditional with just the else block?Ramp
I haven't tried this, so not an answer, but this code looks more promising to me: github.com/ralfbernert/RBParallaxScrolling/blob/master/…Ramp
@danh, if i replace the "if" with just the "else", when ever i scroll down (offset = -x) the view will slowly bounce up.Lighthouse
I added an edit to my answer to explain numerically why your problem exists.Grampositive
G
16

The problem you have is that you are basing your secondary scrolling on a ratio of offset to size, not just on the current offset. So when you increase from an offset of 99 to 100 (out of say 100) your secondary scroll increases by 10, but when you go back down to 99 your secondary scroll only decreases by 9.9, and is thereby no longer in the same spot as it was last time you were at 99. Non-linear scrolling is possible, but not the way you are doing it.

A possible easier way to deal with this is to create a second scrollview and place it below your actual scrollview. Make it non intractable (setUserInteractionEnabled:false) and modify it's contentOffset during the main scrolling delegate instead of trying to move a UIImageView manually.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    [scrollView2 setContentOffset:CGPointMake(scrollView.contentOffset.x,scrollView.contentOffset.y * someScalingFactor) animated:NO];
}

But make sure not to set a delegate for the scrollView2, otherwise you may get a circular delegate method call that will not end well for you.

Grampositive answered 5/3, 2014 at 15:31 Comment(4)
Like the github ref but pleasingly simpler +1. Be sure that scrollView2's content encompasses the bounce regions of the foreground view.Ramp
@Ramp The bouncing is an exercise to the creator, but definitely yes. I found that the amount of bounce possible on the iPad is 37% of the screen width or height, just for reference in this event.Grampositive
@Grampositive thanks for the answer. when you say place scrollView2 below the actual scrollview, you mean giving it a Z index?Lighthouse
@Lighthouse No, just place it in your view hierarchy in whatever position you want it to be. You can use bringSubviewToFront or sendSubviewToBack in UIView's to get the end result you would like.Grampositive
B
4

Scaling Factor being the key element...

...let me offer a 1:1 calculation:

Assuming 2 UIScrollView, one in the foreground and on in the rear, assuming the foreground controls the rear, and further assuming that a full width in the foreground corresponds to a full width in the background, you then need to apply the fore ratio, not the fore offset.

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let foreSpan = foreScrolView.bounds.width - foreScrolView.contentSize.width
    let foreRatio = scrollView.contentOffset.x / foreSpan
    let rearSpan = rearScrollView.bounds.width - rearScrollView.contentSize.width
    rearScrollView.setContentOffset(
        CGPoint(x: foreRatio * rearSpan, y: 0),
        animated: false)
}

Final effect

Parallax effect

The two scrollers, fore and rear, each contain a UIImageView displayed at its full width:

let foreImg = UIImageView.init(image: UIImage(named: "fore"))
foreImg.frame = CGRect(x: 0, y: 0,
                       width: foreImg.frame.width,
                       height: foreScrolView.bounds.height)
foreScrolView.contentSize = foreImg.frame.size
foreScrolView.addSubview(foreImg)

let rearImg = UIImageView.init(image: UIImage(named: "rear"))
rearImg.frame = CGRect(x: 0, y: 0,
                       width: rearImg.frame.width,
                       height: rearScrollView.bounds.height)
rearScrollView.contentSize = rearImg.frame.size
rearScrollView.addSubview(rearImg)

This will scroll both images at a different speed, covering each image in full from edge to edge.


► Find this solution on GitHub and additional details on Swift Recipes.

Buschi answered 15/11, 2015 at 19:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.