I'm attempting to create a very simple parallax effect when the user scrolls a UIScrollView. I'm using the scrollViewDidScroll method on my scrollview, thats all working. I can log an everything from that method so I know it is being fired off when the UIScrollView is scrolled. The issue is any changes I attempt to make to the contents of UIScrollView fails. Possibly a rendering issue? Does a UIScrollView not re-render its contents during scrolling?
I have tried changing the UIImageView(imageView)'s frame and center point, nothing is working. Any ideas on what might be going on, I'm not getting any sort of errors or anything.
-(void)scrollViewDidScroll:(UIScrollView *)body {
float y = body.contentOffset.y;
imageView.center = CGPointMake(imageView.bounds.size.width/2, (imageView.bounds.size.height/2) - y/2);
// Tried this as well
//[imageView setFrame:CGRectMake(0, 0, imageView.bounds.size.width, imageView.bounds.size.height + (y * 2))];
NSLog(#"We are scrolling...");
}
Figured it out, I actually needed to grab the instance of the ImageView within the ScrollView using. Anyone else trying to figure this out, I did it a little hacky but it works.
/* Paralax Scrolling */
-(void)scrollViewDidScroll:(UIScrollView *)body {
float y = body.contentOffset.y;
// When adding (body) image view to scroll view I assigned a tag of 100 to it.
[body viewWithTag:100].center = CGPointMake([body viewWithTag:100].bounds.size.width/2, ([body viewWithTag:100].bounds.size.height/2) + y/2);
}
Related
I'd like to implement a "zoom" effect on a paging UIScrollView that I've created, but I am having a lot of difficulty. My goal is that as a user begins to scroll to the next page, the current page zooms out to become a little bit smaller. As the next page comes into view, it zooms in until it becomes its full size. The closest thing I could find to an example was this...
https://www.pinterest.com/pin/147141112804210631/
Can anyone give me some pointers on how to accomplish this? I've been banging my head against a wall for the last 3 days on this.
I would recommend using the scrollView.contentOffset.y of your paginated UIScrollView to keep track of the scroll and to use that value to animate the transform of your views inside the UIScrollView.
So add your paginated scrollview and make self as delegate.
paginatedScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, [[self view] bounds].size.width, [[self view] bounds].size.height-paginatedScrollViewYOffset)];
[self.view addSubview:paginatedScrollView];
paginatedScrollView.pagingEnabled = YES;
[paginatedScrollView setShowsVerticalScrollIndicator:NO];
[paginatedScrollView setShowsHorizontalScrollIndicator:NO];
[paginatedScrollView setAlwaysBounceHorizontal:NO];
[paginatedScrollView setAlwaysBounceVertical:YES];
paginatedScrollView.backgroundColor = [UIColor clearColor];
paginatedScrollView.contentSize = CGSizeMake([[self view] bounds].size.width, [[self view] bounds].size.height*2); //this must be the appropriate size depending of the number of pages you want to scroll
paginatedScrollView.delegate = self;
Then use the delegate method scrollViewDidScroll to keep track of the scrollView.contentOffset.y
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(#"Scroll Content Offset Y: %f",scrollView.contentOffset.y);
//use here scrollView.contentOffset.y as multiplier with view.transform = CGAffineTransformMakeScale(0,0) or with view.frame to animate the zoom effect
}
Use this Code scrollview its zoom in when scroll next page, the code is given below,
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GridCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectCell" forIndexPath:indexPath];
cell.myscrollview.minimumZoomScale = 5.0;
cell.myscrollview.zoomScale = 5.0;
cell.myscrollview.contentSize = cell.contentView.bounds.size;
return cell;
}
if you change the zoom scale value its automatically zoom in or zoom out to be showed when scroll next or previous page.
hope its helpful.
I actually just posted an answer to a very similar question, where somebody tried to achieve this effect using a UICollectionView. The link to my answer is here: https://stackoverflow.com/a/36710965/3723434
Relevant piece of code I will post here:
So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the pages' size based on their distance from the center of the screen.
For every page, calculate the distance of its center to the center of yourScrollView
The center of yourScrollView can be found using this nifty method: CGPoint point = [self.view convertPoint:yourScrollView.center toView:*yourScrollView];
Now set up a rule, that if the page's center is further than x away, the size of the page is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size, 2.
then you can use the following if/else idea:
if (distance > x) {
page.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
page.transform = CGAffineTransformMakeScale(scale, scale);
}
What happens is that the page's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).
I've done some work on stylized app guide page before.
For Me, I would use CADisplayLink to track the contentOffset.x of the scrollView, associate the value with your animation process. Don't put your views on the scrollView, put them on an overlay view of this scrollView.
This solution follows the philosophy: Fake it before you make it.
Based on CADisplayLink and physics simulation of UIScrollView, you will get smooth animation. Believe me.
What you really want isn't a UIScrollView, it's a UICollectionView with a custom layout. UICollectionViewLayoutAttributes has a transform property that you can set.
Say for example, in layoutAttributesForElementsInRect::
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElementsInRect(rect) else {
return nil
}
return attributes.map { attribute -> UICollectionViewLayoutAttributes in
if attribute.frame.origin.y < 0 {
let scale = -attribute.frame.origin.y / attribute.frame.height
attribute.transform = CGAffineTransformMakeScale(scale, scale)
}
return attribute
}
}
Here, you're filtering by if the element is on the screen (so non-visible elements won't be counted) and checking to see if the y offset is less than 0. If it is, you take the difference between the negated y value and the item's height and turn that into a proportional scale.
You can do it however you want, if you want the scale to be between 1 and 0.5 for example. I like this way of doing things over mucking around with a scroll view.
I have a UITextView in a UIViewController. In front of that UITextView is another UIViewController containing two buttons. The two buttons are static and they obscure the text in the text view.
What I am trying to achieve is an exclusion path on the text that wraps around where the two buttons are and when the text view is scrolled the exclusion path is recalculated so the text is always 100% visible.
To do this I set the initial exclusion path in -viewWillAppear and then use -scrollViewDidScroll: and scrollView.contentOffset to recalculate the exclusion path.
This all works fine while scrolling down however once I start scrolling back up I get stuck in a loop and the processor maxes out.
If I log at -scrollViewDidScroll: I get this:
content offset = {0, 932}
content offset = {0, 584}
content offset = {0, 932}
content offset = {0, 584}
content offset = {0, 932}
content offset = {0, 584}
etc
So as the scrollView scrolls up, it recalculates the exclusion path which pushes the text down and pushes the scrollView down... I think that is what is happening. Although 350pxs seems a big difference when it is just a word or two that gets moved onto the next line.
My question is then, why is this happening and what can I do to fix it?
Here is the code:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self updateExclusionPathWithYOffset:0.0f
width:30.0f];
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(#"content offset = %#",NSStringFromCGPoint(scrollView.contentOffset));
[self updateExclusionPathWithYOffset:scrollView.contentOffset.y
width:30.0f];
}
-(void)updateExclusionPathWithYOffset:(CGFloat)yOffset width:(CGFloat)width{
CGRect rect = CGRectMake(self.textView.bounds.size.width -width,
-10.0f + yOffset,
width,
105.0f);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:rect];
[[self.textView textContainer]setExclusionPaths:#[path]];
}
** Edit **
It seems I was missing the obvious.
-updateExclusionPathWithYOffset: width: changes the textView layout which causes -scrollViewDidScroll: to fire which is what causes the loop.
I'm still interested in a way to avoid this.
Thanks in advance for anyone that will take the time.
Was struggling with this as well. The way I did it was to add my exclusion path/s in UITextViewTextDidChangeNotification listener:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(didReceiveTextViewNotification:)
name:UITextViewTextDidChangeNotification
object:self];
I've a UIScrollView with other subviews inside an UIImageWiew and I need to rotate the whole content, so I take this road:
imageView.transform = CGAffineTransformMakeRotation([self.orientation floatValue]);
Good, works perfectly.
Being the ImageView inside a scroll view, I also need to set zoomScale in order to resize image inside, and I do it in this way:
- (void)updateZoom {
const float minZoom = MIN(self.view.bounds.size.width / self.imageView.image.size.width,
self.view.bounds.size.height / self.imageView.image.size.height);
if (minZoom > 1) {
return;
}
self.scrollView.minimumZoomScale = minZoom;
self.scrollView.zoomScale = minZoom;
}
updateZoom has the effect to "reset" initial transformation, so image come back to original orientation.
Generally, each time I modify "zoomScale" property, orientation is restored.
How can I keep both orientation both zoomScale?
I suppose I need to do something in scrollView delegate:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView;
I just put a repo on GitHub that might help you. It is in Swift but it should do
PhotoSlideShow-Swift
I have a pageable UIScrollView which contains different kind of informations like UITables but also zoomable images. Therefore I set up a pageable main-ScrollView and as subviews I added zoomable image-ScrollViews with the images as content.
Works everything fine, just I fail to set the smaller current zoom scale of the imageScrollViews.
UIImageView *imageView = [[UIImageView alloc] initWithImage:Image];
//storing a link to the imageView
[imagelinkArray addObject:imageView];
CGRect ScrollViewImageRect;
ScrollViewImageRect = CGRectMake((self.scrollView.frame.size.width) * i, 0, 320, self.scrollView.frame.size.height);
float widthfactor = ScrollViewImageRect.size.width / imageView.frame.size.width;
float heightfactor = ScrollViewImageRect.size.height / imageView.frame.size.height;
float zoomscale = MIN(widthfactor, heightfactor);
UIScrollView *imageScrollView = [[UIScrollView alloc] initWithFrame:ScrollViewImageRect];
imageScrollView.contentSize = CGSizeMake(imageView.frame.size.width, imageView.frame.size.height);
imageScrollView.delegate = self;
[imageScrollView setMinimumZoomScale:zoomscale];
[imageScrollView setMaximumZoomScale:1.5];
[imageScrollView addSubview:imageView];
//doesn't work:
[imageScrollView setZoomScale:0.5 animated:YES];
[self.scrollView addSubview:imageScrollView];
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return [imagelinkArray objectAtIndex:page];
}
The main-ScrollView and the image-ScrollViews are drawn perfectly and it's possible to zoom and page properly. The minimum zoom factor is calculated also correct. When I zoom out I can zoom until the image limits are reached. However the first time I page to the image-ScrollView it's current zoom scale is always 1.0 while it should be the minimum scale.
Wherever I set the zoom scale in the code above it doesn't work:
[changeScrollView setZoomScale:changeScrollView.minimumZoomScale animated:YES];
If I log the current zoom scale i always get 1.0;
The only thing which works is changing the zoom scale in the - (void)scrollViewDidScroll: method, which of course doesn't help a lot since zooming also calls it which resets the zoom immediately. But at least I could figure out, that the code somehow works. I have the feeling a UIScrollView doesn't zoom when it's not visible on the screen right now. How can I fix this?
Update:
Okay. In the meantime I figured out that the problem most likely comes from my base layout of "sub-viewing" ScrollViews into another ScrollView. When I zoom one of the images and log the current zoom factor of the ScrollViews they are all the same (main ScrollView as well as ALL sub-ScrollViews).
What could be the reason for it or how could I solve it with a different layout?
I think viewForZoomingInScrollView: may not be getting called by the scrollview. Try to add a breakpoint and check what you're returning there.
Your code doesn't work because you havent set minimumZoomScale ,which has default value of 1.0 . Since you are trying to set 0.5 which is below the default value, it wouldn't work.
Add following line just after the line which sets maximumZoomScale.
[imageScrollView setMinimumZoomScale:0.25];
As user Rivera pointed out the return value of viewForZoomingInScrollView: was off course not right.
I always returned the current visible page shown in the scrollView. Which means setting the zoom in viewDidAppear doesn't change anything.
Here is the correct code: I made a function which will always resets both images left and right of the current visible scroll page:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self updateImageSize];
}
- (void)updateImageSize
{
updatePage = updatePage + 1;
if (updatePage < imagescrolllinkArray.count) {
UIScrollView *changeScrollView = (UIScrollView*)[imagescrolllinkArray objectAtIndex:updatePage];
[changeScrollView setZoomScale:changeScrollView.minimumZoomScale];
}
updatePage = updatePage - 2;
if (updatePage > -1) {
UIScrollView *changeScrollView = (UIScrollView*)[imagescrolllinkArray objectAtIndex:updatePage];
[changeScrollView setZoomScale:changeScrollView.minimumZoomScale];
}
//reset to current view
updatePage = updatePage + 1;
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return [imagelinkArray objectAtIndex:updatePage];
}
and in viewDidAppear I added the following to update the first image before any dragging.
if (i == 1) {
updatePage = 0;
[imageScrollView setZoomScale:imageScrollView.minimumZoomScale];
}
In the meantime I figured out that the code below can under certain circumstances make problems. Sometimes when I zoom in and out from an image the main ScrollView can't be scrolled anymore, so there is no way to get to the next image, except of triggering the pageControl or except of zooming into the image and then scrolling to its borders so the main ScrollView jumps to the next page.
Means the problem only occurs when the image is fully zoomed out (.scale = .minimumscale). I don't understand exactly when and why it happens. Logging viewDidScroll returns nothing in this situations.
Anybody experienced similar problems?
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: git#bitbucket.org: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.
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:
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.
- (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.
I'll post my updates here.
One answerer mentioned - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView and while that doesn't apply here, the similar scrollViewDidEndDecelerating: does. In my struggles yesterday I implemented
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (self.mainScrollView.contentOffset.y < 10)
{
[UIView animateWithDuration:0.2 animations:^{
self.imageScroller.frame = self.initialImageScrollerFrame;
}];
}
}
and this is somewhat of a workaround, but it's ugly and definitely not production-worthy. It just animated the frame back to its original position if the contentOffset is close enough to 0. It's a step in the right direction, but I'm afraid it doesn't deserve merit as a solution.
----
Also added
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
CGPoint point = *targetContentOffset;
CGFloat offset = point.y;
if (0 <= offset && offset < 10)
{
[UIView animateWithDuration:0.2 animations:^{
self.imageScroller.frame = self.initialImageScrollerFrame;
}];
}
}
which brings me closer to the intended effect, but still remains unusable for a production application.
The same issue but for add another functionality, button for left/right listing UIScrollView, after fast tapping on it, scroll view is broke my brain ;(
I think better scrolling without animation, it preventing glitches for UIScrollView content. May be it is not answer, but...
Hey mate use the below method that might help you out, its a delegate method of UIScrollView:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
And within this method you can change the scrollview frame as x and y to 0,0