I have embedded an image view into a scroll view for zooming purposes. Everything works great but if the user choses to zoom in then pan in any direction the user can slightly pull the ScrollView off the screen bounds exposing the content i have behind the scrollview before bouncing back. I want to disable this bouncing when zoomed in but don't know how. This only happens when zoomed in. At normal zoom scale you can't do it. Heres my code I have gone through the UIScrollView Class Reference and have commented out my attempts at disabling it but it doesn't work!
zoomScrollView.contentSize = CGSizeMake(enlargedPhotoImageView.frame.size.width , enlargedPhotoImageView.frame.size.height);
zoomScrollView.maximumZoomScale = 10;
zoomScrollView.minimumZoomScale = 1;
zoomScrollView.clipsToBounds = YES;
zoomScrollView.delegate = self;
zoomScrollView.zoomScale = 1;
//zoomScrollView.alwaysBounceHorizontal = NO;
//zoomScrollView.alwaysBounceVertical = NO;
//zoomScrollView.bouncesZoom = NO;
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
if(scrollView == zoomScrollView){
return enlargedPhotoImageView;
}else{
return nil;
}
}
zoomScrollView.bounces = NO;
Will prevent overscroll. But...scrollviews that don't bounce usually feel very weird to users; bear in mind that there's probably a better way to accomplish whatever you're trying to do.
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 searched a lot but not able to find anything fruitful.Here is my scenario:
I am making a horizontal Image Slider(UIScrollView) which consist of multiple images.
The slider is working fine.
But the problem arises when I implement Pinch-In Zoom-In-Out`.
I am using this delegate method to zoom a particular Image:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return imgView;
}
Whenever I zoom-In any image the Image next to it changes its position and the image which I am zooming after I leave my finger the image slighly shift to left and top . It not stoping where I want to stop.
What I have Done: Programatically created image and added them to scrollView.
xPointer = 0;
for(i=0;i<[imgArr count];i++)
{
UIImageView *imgToZoom= [[UIImageView alloc]init];
imgToZoom.image = [imgArr objectAtIndex:i];
imgToZoom.tag = i+100;
imgToZoom.frame = CGRectMake(xPointer, _imagesScrollView.frame.origin.y-10, self.view.frame.size.width,_imagesScrollView.frame.size.height/2);
[imgToZoom setUserInteractionEnabled:YES];
[imgToZoom setMultipleTouchEnabled:YES];
[_imagesScrollView addSubview:imgToZoom];
xPointer = xPointer+self.view.frame.size.width;
}
_imagesScrollView.contentSize = CGSizeMake(xPointer, _imagesScrollView.frame.size.height);
How can I implement Pinch In zoom. As When I done it using apple Doc My other images are changing there position.And Zoom in also not to the point.
I want to achieve something very close to what Google Maps (iOS) does and I have some doubts.
But first, a better explanation and things to take into account:
-------------- ANSWER --------------
Thanks to Jugale's contribution, here's a repository so everybody can download and test everything out.
https://github.com/vCrespoP/VCSlidingView
-------------- ORIGINAL QUESTION -----------
You tap in a point inside the map, a view comes in from the bottom but then when you interact with that summary view:
Notice when pulling just a bit, the navigation bar already has set.
When you have scrolled it to the top, you can continue scrolling and the inner scrollview will continue scrolling.
When you 'reverse' the action to dismiss the view, the PanGesture doesn't mess up with the inner scrollView (same for the other way, scrollView VS dismiss)
Here it is in action:
Doubts:
I've tried to do it as an interactive transition (UIPercentDrivenInteractiveTransition) and separating Map from Details in 2 controllers but I'm having troubles with the UIPanGesture interfering with the scrollView.
Maybe it's better to do it as a subview and handle everything there? More or less like MBPullDownController (Although it has some issues with iOS8) -> https://github.com/matej/MBPullDownController
So, anybody knows any framework, has done it, or knows how to do this in a good way?
Thank you for your time :D
Looking through my implementation it seems the following are true:
I have a subclass of UIViewController that is the view controller
I have a subclass of UIView that is the overlay (and henceforth with the known as "the overlay") (actually for me this is a UIScrollView because it needs to go sideways too, but I'll try and filter out the unnecessary code)
I have another subclass of UIView that loads the overlay's content ("the content wrapper")
The content wrapper has a UIScrollView property, in which all other views are loaded ("the content view")
The view controller is responsible for initializing the overlay, setting it's initial frame (where the height is the height of the screen) and passing content to it, nothing more.
From it's -initWithFrame method, the overlay sets itself up with a UIDynamicItemBehavior. It also creates some UICollisionBehavior objects: one at the top of the screen and one below the bottom of the screen at just the right y position for the top of the overlay to be partially visible (as seen in the first frame of your GIF). A UIGravityBehavior is also set up to keep the overlay sitting on the lower collision boundary. Of course, _animator = [[UIDynamicAnimator alloc... is set up too.
Finally:
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan)];
_pan.delegate = self;
_pan.cancelsTouchesInView = FALSE;
The overlay class also has some other helpful methods such as changing the gravity's direction so that the overlay can appear to snap to the top or bottom of the screen.
The _pan handler uses a UISnapBehavior to keep the overlay moving dynamically up and down the screen underneath the user's finger:
- (void)handlePan
{
[self handlePanFromPanGestureRecogniser:_pan];
}
- (void)handlePanFromPanGestureRecogniser:(UIPanGestureRecognizer *)pan
{
CGFloat d = [pan velocityInView:self.superview.superview].y;
CGRect r = self.frame;
r.origin.y = r.origin.y + (d*0.057);
if (r.origin.y < 20)
{
r.origin.y = 20;
}
else if (r.origin.y > [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT)
{
r.origin.y = [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT;
}
if (pan.state == UIGestureRecognizerStateEnded)
{
[self panGestureEnded];
}
else if (pan.state == UIGestureRecognizerStateBegan)
{
[self snapToBottom];
[self removeGestureRecognizer:_tap];
}
else
{
[_animator removeBehavior:_findersnap];
_findersnap = [[UISnapBehavior alloc] initWithItem:self snapToPoint:CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r))];
[_animator addBehavior:_findersnap];
}
}
- (void)panGestureEnded
{
[_animator removeBehavior:_findersnap];
CGPoint vel = [_dynamicSelf linearVelocityForItem:self];
if (fabsf(vel.y) > 250.0)
{
if (vel.y < 0)
{
[self snapToTop];
}
else
{
[self snapToBottom];
}
}
else
{
if (self.frame.origin.y > (self.superview.bounds.size.height/2))
{
[self snapToBottom];
}
else
{
[self snapToTop];
}
}
}
The content wrapper listens for scroll events generated by the content view:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//this is our fancy way of getting the pan to work when the scrollview is in the way
if (scrollView.contentOffset.y <= 0 && _dragging)
{
_shouldForwardScrollEvents = TRUE;
}
if (_shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapper:isForwardingGestureRecogniserTouches:)])
{
[_delegate theContentWrapper:self isForwardingGestureRecogniserTouches:scrollView.panGestureRecognizer];
}
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
_dragging = FALSE;//scrollviewdidscroll must not be called after this
if (scrollView.contentOffset.y <= 0 || _shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapperStoppedBeingDragged:)])
{
[_delegate theContentWrapperStoppedBeingDragged:self];
}
}
_shouldForwardScrollEvents = FALSE;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_dragging = TRUE;
}
As you can see, when the bool shouldForwardScrollEvents is TRUE then we send scrollView.panGestureRecognizer to the content wrapper's delegate (the overlay). The overlay implements the delegate methods like so:
- (void)theContentWrapper:(TheContentWrapper *)contentWrapper isForwardingGestureRecogniserTouches:(UIPanGestureRecognizer *)contentViewPan
{
[self handlePanFromPanGestureRecogniser:contentViewPan];
}
- (void)theContentWrapperStoppedBeingDragged:(TheContentWrapper *)contentWrapper
{
//because the scrollview internal pan doesn't tell use when it's state == ENDED
[self panGestureEnded];
}
Hopefully at least some of this is useful to someone!
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?
Sorry for the long and self explanatory title, but UIScrollView has raised so many questions that I find it difficult to reach the ones that might help in different situations.
I have nested scrollviews in my iPad app. So far so good, everything has its ups and downs but its quite slick and responsive. My outter scroll view is a paged one, that contains fullscreen or bigger content scrollviews in it. Outter scrollview is horizontal and inner vertical. Like the photo gallery one. I found that when I'm zooming and scrolling the inner scrollview, there's a noticeable delay in detecting slow and long swipe gestures ONLY when the scrollview has been scrolled down to the bottom of the content and bouncing is OFF.
the other thing is that the inner scrollview bouncing property goes YES/NO pseudo-randomly. So, this is the code in the constructor that set the inner scrollviews that are acting up:
if (UIInterfaceOrientationIsPortrait(forOrientation)) {
self.minimumZoomScale = 1.0;
self.maximumZoomScale = 1.0;
self.bounces = NO;
self.alwaysBounceVertical = NO;
self.scrollEnabled = NO;
}else if (UIInterfaceOrientationIsLandscape(forOrientation)){
self.minimumZoomScale = 1.333333f;
self.maximumZoomScale = 1.333333f;
self.bounces = YES;
self.alwaysBounceVertical = YES;
self.scrollEnabled = YES;
}
self.scrollsToTop = NO;
self.showsVerticalScrollIndicator = YES;
self.showsHorizontalScrollIndicator = NO;
self.directionalLockEnabled = YES;
self.delegate = self;
self.pagingEnabled = NO;
self.canCancelContentTouches = NO;
self.delaysContentTouches = YES;
When the iPad is rotated bouncing will come and go for the scroll view as well and will have a bouncing glitch too.
Is this a bug ? or is it just me that I'm messing it up?
thanks in advance for your time and interest!
UPDATE:
I'm nesting two scrollviews that are actually Subclasses of UIScrollView. I'm doing this because I need to override hitTest an other methods as well. I also tried the Better solution described here http://openradar.appspot.com/8045239 and did not get any good results.
I'm answering my own question.
Open radar's bug solution that's posted on the question post, it's not very clean on what to isolate in order to stop the uiscrollview from resizing and cancel bounces
Basically EVERYTHING that can change the view's frame during layoutSubViews has to be done only ONCE by double checking that if the size is equal to the change that's coded in layoutSubviews, then that's not ran more than once.
-(void)layoutSubviews {
///...
if (!self.bounces) {
self.bounces = YES;
}
if(!self.scrollEnabled){
self.scrollEnabled = YES;
}
if (!CGSizeEqualToSize(rect.size, self.contentSize)) {
self.contentSize = rect.size;
}
if (self.zoomScale < MAXIMUM_ZOOM_SCALE ) {
[self zoomToRect:ZOOM_RECT_MAKE animated:NO];// otherwise this line of code won't do anything at all
}
}
Nesting scrollviews will always lead to touch-responder issues. I'm assuming that the inner is the primary one—the one users interact with more? In general, you're probably going to have to write some of your own touch responders (at least, that has been my experience, both in code and around SO). Make the vertical scrolling more forgiving, but only detect a narrow range of swipes and drags (near-horizontal lines) for the outer. That way, most of the touch inputs will be sent to the inner view.
As far as the bouncing goes, remove one of the places where you set the property and see what happens.
Which method is this in? Hopefully this code is residing in a UIView subclass…?