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.
Related
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'm working on making a large grid using a UICollectionView, and I want it to be zoomable (the entire UICollectionView, not just a single cell) with the standard pinch-to-zoom gesture. The grid in question has a custom UICollectionViewLayout, because I needed it to scroll both horizontally and vertically.
I got the layout working perfectly with this SO answer, so I had a grid that you could move all around on. The short version is that each row of cells is a section of the view, and all the cells are positioned based on a uniform cellSize of (to start with) 50.
Then I worked out the pinch-to-zoom ability using a modified version of this SO answer, where I basically change the layout's cellSize value when the pinch gesture is received, and then invalidate the layout so it re-draws with the slightly larger or smaller layout. Thus, all the cells get bigger or smaller, and we have zooming.
Here's the code for the pinch gesture method:
-(void)didReceivePinchGesture:(UIPinchGestureRecognizer*)gesture {
double newCellSize = [(IMMapViewLayout *)_mainCollectionView.collectionViewLayout cellSize] * gesture.scale;
newCellSize = MIN(newCellSize, 100);
newCellSize = MAX(newCellSize, 15);
[(IMMapViewLayout *)_mainCollectionView.collectionViewLayout setCellSize:newCellSize];
[_mainCollectionView.collectionViewLayout invalidateLayout];
}
And everything was working (almost) perfectly.
My problem is this: it zooms from the top-left corner, not from where the pinch is located. Makes sense, I suppose, since we're redrawing everything and it's all a little bigger, but it's obviously not the desired effect.
My first thought was simply to detect the cell directly under the pinch and then use scrollToItemAtIndexPath:atScrollPosition:animated: to move back to that cell instantaneously, but it doesn't seem to be working, and the animation gets super-choppy anyway. Also, if you're pinching anywhere other than the center of the screen, it would be hard to move it right back to that exact spot repeatedly during the zoom.
Here's what I've got now:
-(void)didReceivePinchGesture:(UIPinchGestureRecognizer*)gesture {
double newCellSize = [(IMMapViewLayout *)_mainCollectionView.collectionViewLayout cellSize] * gesture.scale;
newCellSize = MIN(newCellSize, 100);
newCellSize = MAX(newCellSize, 15);
[(IMMapViewLayout *)_mainCollectionView.collectionViewLayout setCellSize:newCellSize];
[_mainCollectionView.collectionViewLayout invalidateLayout];
if([gesture numberOfTouches] >= 2) {
CGPoint touch1 = [gesture locationOfTouch:0 inView:_mainCollectionView];
CGPoint touch2 = [gesture locationOfTouch:1 inView:_mainCollectionView];
CGPoint mid;
mid.x = ((touch2.x - touch1.x) / 2) + touch1.x;
mid.y = ((touch2.y - touch1.y) / 2) + touch1.y;
NSIndexPath *currentIndexPath = [_mainCollectionView indexPathForItemAtPoint:mid];
[_mainCollectionView scrollToItemAtIndexPath:currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:NO];
}
}
Can anyone help me make this UICollectionView zoom, in its entirety, but centered on the position of the pinch?
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 was trying to mimic the yahoo weather app screen transition between cities, I couldn't figure out what transition it is. Any clue?
I really appreciate you time.
Thanks
Edit:
I have slideView ctrler which has a subview. The sliderview has an image and the subview has text. When I make a swipe, the text view with text must be moving and dragging way the view ctrler with it at a slower rate and this intern should start dragging in the next view ctrler which is an instance of slider Ctrler.
There is no built-in transition that does this for you (I assume you're talking about the images that are transitioning their frame/center at a different rate than the view itself). You'd probably have to write it yourself. Some basic familiarity with gesture recognizers and view animation is needed.
The basic effect is by simultaneously adjusting the center property for two image views as you change the frame of those views (or their super views). (Or, you can achieve this by having image views whose contentMode is UIViewContentModeCenter and just changing the frame.) I'd suggest you start with some simple tests of the effect and build from there.
For example, I created a scene that has two image views, whose autolayout constraints were defined as follows:
H:|[leftImageView][rightImageView]|
V:|[leftImageView]|
V:|[rightImageView]|
I then defined a width constraint for the leftImageView, and hooked it up to an IBOutlet for that constraint, e.g. leftImageWidthConstraint. I then have a UIPanGestureRecognizer that could handle the gesture, simply changing this leftImageWidthConstraint accordingly (and with auto layout, the rest of the frame is calculated automatically for me from that):
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
CGPoint translate = [gesture translationInView:gesture.view];
static CGFloat width;
if (gesture.state == UIGestureRecognizerStateBegan)
{
width = self.leftImageWidthConstraint.constant;
}
CGFloat newWidth = width + translate.x;
if (newWidth < 0)
newWidth = 0;
else if (newWidth > self.view.bounds.size.width)
newWidth = self.view.bounds.size.width;
self.leftImageWidthConstraint.constant = newWidth;
// if you let go, animate the views to their final position
if (gesture.state == UIGestureRecognizerStateEnded)
{
// if more than half way, set left view's target width to take up full width,
// otherwise set left view's target width to zero
if (newWidth > (self.view.bounds.size.width / 2.0))
newWidth = self.view.bounds.size.width;
else
newWidth = 0;
// animate the changing of the constraint (and thus the `frame`) accordingly
self.leftImageWidthConstraint.constant = newWidth;
[UIView animateWithDuration:0.5 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
}
Thus, as I pan across, the two images are centered within their clipped frames:
This is a very basic implementation of the idea. There are, though, a ton of implementation details (custom container vs subviews, autolayout vs not, etc.), so until you answer some of those questions, it's going to be hard to be more specific.
It is not default but i achieved similar to it in my old app
Register gesture on your view and on detection set isFromLeftSide accordingly
Call following. do fine tune this as per your requirements
[self.view addSubview:mySlidingView];
mySlidingView.frame = // set offscreen frame, in the direction you want it to appear from depending on flag isFromLeftSide
[UIView animateWithDuration:8.0
animations:^{
mySlidingView.frame = // desired end location
}];
Im attempting to replicate the resizing behaviour of a screen seen in the iOS app Rdio. The screen in question provides an overview of a selected album and contains a UIView on the top half and a UITableView on the bottom half. When the tableView is scrolled, it first resizes upwards to fill the screen, then begins to scroll through its content normally once the maximum height is reached.
After some searching I found this question: Dragging UITableView which is basically asking for the same thing, however its accepted method is the same as my initial thoughts & trial, which was to use a UIPanGestureRecognizer and resize the tableviews height according to the translation of the pan.
This does not provide the behaviour i'm looking for. Using this method only allows you to statically drag the tableviews height up or down and it has the added issue of the panGesture overriding that of the tableViews which then prevents scrolling through the content.
The resizing behaviour of the Rdio app functions and feels exactly like a UIScrollView, it has inertia. You can drag it all the way, flick it up or down, and it smoothly resizes. When the tableView has reached its full-size or original half-size, the remaining inertia is seemingly passed on the tableview causing the cells to scroll as they normally would for that amount. I know they must be manipulating UIScrollViews, I just can't figure out how.
As a final note, eventually I will be using AutoLayout on this screen so i'm wondering how that will potentially hinder or help this situation as well.
Update
This approach has gotten me closest to the behaviour i'm looking for so far.
Flicking the tableView upwards behaves exactly like I wanted it to (resize with inertia & continue scrolling when max height is reached), although with less sensitivity than i'd like. Flicking downwards however, provides no inertia and instantly stops.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect scrollViewFrame = scrollView.frame;
CGFloat scrollViewYOffset = scrollView.contentOffset.y;
if (scrollViewFrame.origin.y <= kTableViewMaxYOrigin || scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.origin.y -= scrollViewYOffset;
if(scrollViewFrame.origin.y >= kTableViewMaxYOrigin && scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.size.height += scrollViewYOffset;
scrollView.frame = scrollViewFrame;
scrollView.contentOffset = CGPointZero;
}
}
}
I made a version that uses autolayout instead.
It took me a while of trial and error to get this one right!
Please use the comments and ask me if the answer is unclear.
In viewDidLoad save the initial height of your layout constraint determining the lowest down you want the scrollview to be.
- (void)viewDidLoad
{
...
_initialTopLayoutConstraintHeight = self.topLayoutConstraint.constant;
...
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
BOOL leaveScrollAlone = self.topLayoutConstraint.constant == _initialTopLayoutConstraintHeight && scrollView.contentOffset.y <= 0;
if (leaveScrollAlone)
{
// This allows for bounce when swiping your finger downwards and reaching the stopping point
return;
}
// Do some capping of that layout constraint to keep it from going past the range you want it to be.
// In this case, I use self.topLayoutGuide.length so that my UICollectionView scales all the way until
// it hits the bottom of the navigation bar
CGFloat topLayoutConstraintLength = _initialTopLayoutConstraintHeight - scrollView.contentInset.top;
topLayoutConstraintLength = MAX(topLayoutConstraintLength, self.topLayoutGuide.length);
topLayoutConstraintLength = MIN(topLayoutConstraintLength, _initialTopLayoutConstraintHeight);
self.topLayoutConstraint.constant = topLayoutConstraintLength;
// Keep content seemingly still while the UICollectionView resizes
if (topLayoutConstraintLength > self.topLayoutGuide.length)
{
scrollView.contentInset = UIEdgeInsetsMake(scrollView.contentInset.top + scrollView.contentOffset.y,
scrollView.contentInset.left,
scrollView.contentInset.bottom,
scrollView.contentInset.right);
scrollView.contentOffset = CGPointZero;
}
// This helps get rid of the extraneous contentInset.top we accumulated for keeping
// the content static while the UICollectionView resizes
if (scrollView.contentOffset.y < 0)
{
self.topLayoutConstraint.constant -= scrollView.contentOffset.y;
scrollView.contentInset = UIEdgeInsetsMake(scrollView.contentInset.top + scrollView.contentOffset.y,
scrollView.contentInset.left,
scrollView.contentInset.bottom,
scrollView.contentInset.right);
}
// Prevents strange jittery artifacts
[self.view layoutIfNeeded];
}
It turns out the key component to getting the smooth inertial resizing in both directions was to update the scrollViews contentInset.top by its contentOffset.y.
I believe this makes sense in retrospect as if the content within is already at the top it cannot scroll anymore, hence the sudden stop rather than smooth scroll. At least thats my understanding.
Another key point was to make sure the cells only started scrolling once maximum or original height was achieved. This was done simply by setting the scrollViews contentOffset to CGPointZero each time the view resized until maximum or original height was reached.
Here is the - (void)scrollViewDidScroll:(UIScrollView *)scrollView method demonstrating how to achieve this effect.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect scrollViewFrame = scrollView.frame;
CGFloat scrollViewTopContentInset = scrollView.contentInset.top;
CGFloat scrollViewYOffset = scrollView.contentOffset.y;
if (scrollViewFrame.origin.y <= kTableViewMaxYOrigin || scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.origin.y -= scrollViewYOffset;
if(scrollViewFrame.origin.y >= kTableViewMaxYOrigin && scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.size.height += scrollViewYOffset;
scrollViewTopContentInset += scrollViewYOffset;
scrollView.frame = scrollViewFrame;
scrollView.contentInset = UIEdgeInsetsMake(scrollViewTopContentInset, 0, 0, 0);
scrollView.contentOffset = CGPointZero;
}
}
}
I haven't seen the app in question, but from your description... in your tableView's delegate method -scrollViewDidScroll:, set tableView.frame.origin.y to albumView.frame.height - tableView.contentOffset.y and change its height accordingly.
(If you're using autolayout, you'll have to change the constraints pertaining to the tableView's frame rather than the frame itself.)