In the latest Expedia app for iOS, they have a very interesting effect that I am trying to wrap my head around. The have two columns of infinitely scrolling subviews which I know can be accomplished with 2 scrollviews. The interesting part is that the overall scrollview appears to have a linen background that stays static and can be seen in the gap between each of the subview cells. The really cool part is that the subviews have a different background that stays in place. In the screenshot below it is city skyline image. When the subviews scroll, the city image can only be seen behind the subview cells. It appears to be some sort of masking trick but I can't quite figure out how the effect is done. How can I achieve the same result?
Essentially, how can you show a static background behind subviews that act as little windows and not show the linen. The linen should only be shown around the cells.
You can download the app, hit airplane mode and try it for yourself.
Here is a screenshot:
Here is another to show that the cells scrolled but the city stays the same:
I'd like to found an elegant solution, for now I would do it by tracking the visible subviews offset and configuring their appearance.
Please check the result at sample project.
For the future reference I'll attach the code below:
ViewController.m
//
// OSViewController.m
// ScrollMasks
//
// Created by #%$^Q& on 11/30/12.
// Copyright (c) 2012 Demo. All rights reserved.
//
#import "OSViewController.h"
#interface OSViewController ()
// subviews
#property (strong) IBOutlet UIScrollView * scrollView;
// all the subviews
#property (strong) NSArray * maskedSubviews;
// subviews visible at scrollview, we'll update only them
#property (strong) NSArray * visibleMaskedSubviews;
// updates the views from visibleMaskedSubviews
-(void) updateVisibleSubviews;
// updates the visibleMaskedSubviews array with the given scrollView offset
-(void) updateVisibleSubviewsArrayForOffset:(CGPoint) offset;
#end
#implementation OSViewController
-(void) unused {}
#pragma mark - view
-(void) viewWillAppear:(BOOL)animated {
[self updateVisibleSubviews];
[super viewWillAppear:animated];
}
- (void)viewDidLoad
{
[super viewDidLoad];
/*
See -updateVisibleSubviews comment for the class comments
*/
UIView * newMaskedView = nil;
NSMutableArray * newMaskedSubviews = [NSMutableArray array];
const CGSize scrollViewSize = self.scrollView.bounds.size;
const int totalSubviews = 10;
const float offset = 20.;
const float height = 100.;
UIImage * maskImage = [UIImage imageNamed:#"PeeringFrog.jpg"];
/*
// Uncomment to compare
UIImageView * iv = [[UIImageView alloc] initWithFrame:self.scrollView.bounds];
iv.image = maskImage;
[self.view insertSubview:iv atIndex:0];
*/
// add scrollview subviews
for (int i = 0; i < totalSubviews; i++) {
CGRect newViewFrame = CGRectMake(offset, offset*(i+1) + height*i, scrollViewSize.width - offset*2, height);
newMaskedView = [UIView new];
newMaskedView.frame = newViewFrame;
newMaskedView.clipsToBounds = YES;
newMaskedView.backgroundColor = [UIColor redColor];
newMaskedView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth;
UIImageView * maskImageView = [UIImageView new];
maskImageView.frame = CGRectMake(0, 0, self.scrollView.bounds.size.width, self.scrollView.bounds.size.height);
maskImageView.image = maskImage;
[newMaskedView addSubview:maskImageView];
[self.scrollView addSubview:newMaskedView];
[newMaskedSubviews addObject:newMaskedView];
}
self.scrollView.contentSize = CGSizeMake(scrollViewSize.width, (height+offset)*totalSubviews + offset*2);
self.maskedSubviews = [NSArray arrayWithArray:newMaskedSubviews];
[self updateVisibleSubviewsArrayForOffset:self.scrollView.contentOffset];
}
-(void) updateVisibleSubviews {
[self updateVisibleSubviewsArrayForOffset:self.scrollView.contentOffset];
for (UIView * view in self.visibleMaskedSubviews) {
/*
TODO:
view must be UIView subclass with the imageView initializer and imageView frame update method
*/
CGPoint viewOffset = [self.view convertPoint:CGPointZero fromView:view];
UIImageView * subview = [[view subviews] objectAtIndex:0];
CGRect subviewFrame = subview.frame;
subviewFrame = CGRectMake(-viewOffset.x, -viewOffset.y, subviewFrame.size.width, subviewFrame.size.height);
subview.frame = subviewFrame;
}
}
#pragma mark - scrollview delegate & utilities
-(void) scrollViewDidScroll:(UIScrollView *)scrollView {
[self updateVisibleSubviews];
}
-(void) updateVisibleSubviewsArrayForOffset:(CGPoint) offset {
NSMutableArray * newVisibleMaskedSubviews = [NSMutableArray array];
for (UIView * view in self.maskedSubviews) {
CGRect intersectionRect = CGRectIntersection(view.frame, self.scrollView.bounds);
if (NO == CGRectIsNull(intersectionRect)) {
[newVisibleMaskedSubviews addObject:view];
}
}
self.visibleMaskedSubviews = [NSArray arrayWithArray:newVisibleMaskedSubviews];
}
#pragma mark - memory
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
ViewController.h
//
// OSViewController.h
// ScrollMasks
//
// Created by #%$^Q& on 11/30/12.
// Copyright (c) 2012 Demo. All rights reserved.
//
/*
PeeringFrog image is taken (and resized) from Apple sample project "PhotoScroller"
*/
#import <UIKit/UIKit.h>
#interface OSViewController : UIViewController <UIScrollViewDelegate>
#end
I did something similar a few years ago. At first I tried using the CGImageMaskCreate stuff, but found it far easier just to create an image that had transparent "cutouts" and then used animation effects to scroll the picture(s) under it.
For your case, I'd find an image of linen the size of the screen. Then I'd use a image editor (I use GIMP) to draw some number of boxes on the linen using a flat color. Then I'd map that box color to transparent to make the cutouts. There's other ways to do this, but that's the way I do it.
In your app, add two or more image views to the main view. Don't worry about the placement because that will be determined at run time. You'll want to set these image views to contain the images you want to have "scroll" under. Then add your linen-with-cutouts UIImageView so it's on top and it's occupying the entire screen size. Make sure that the top UIImageView's background is set to transparent.
When the app starts, layout your "underneath" imageviews, top to bottom, and then start a [UIView beginAnimation] that scrolls your underneath images views up by modifying the "y" position. This animation should have a done callback that gets called when the top image view is completely off the screen. Then, in the done callback, layout the current state and start the animation again. Here's the guts of the code I used (but note, my scrolling was right to left, not bottom to top and my images were all the same size.)
- (void)doAnimationSet
{
[iv1 setFrame:CGRectMake(0, 0, imageWidth, imageHeight)];
[iv2 setFrame:CGRectMake(imageWidth, 0, imageWidth, imageHeight)];
[iv3 setFrame:CGRectMake(imageWidth*2, 0, imageWidth, imageHeight)];
[self loadNextImageSet];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:10];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[iv1 setFrame:CGRectMake(-imageWidth, 0, imageWidth, imageHeight)];
[iv2 setFrame:CGRectMake(0, 0, imageWidth, imageHeight)];
[iv3 setFrame:CGRectMake(imageWidth, 0, imageWidth, imageHeight)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(doneAnimation:finished:context:)];
[UIView commitAnimations];
}
- (void)doneAnimation:(NSString *)aid finished:(BOOL)fin context:(void *)context
{
[self doAnimationSet];
}
This should give you the effect that you are looking for. Good luck :)
Related
I need to calculate the visible CGRect of a UIView subview, in the coordinates of the original view. I've got it working if the scale is 1, but if one of the superviews or the view itself is scaled (pinch), the visible CGRect origin is offset slightly.
This works when the scale of the views is 1 or the view is a subview of the root view:
// return the part of the passed view that is visible
// TODO: figure out why result origin is wrong for scaled subviews
//
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view frame
}
When a subview is scaled, the size of the resultant view is correct, but the origin is offset by a small amount. It appears that the convertRect does not calculate the origin properly for scaled subviews.
I tried adjusting the origin relative to the X/Y transform scale but I could not get the calculation correct. Perhaps someone can help?
To save time, here is a complete test ViewController.m, where a box with an X is drawn on the visible part of the views - just create a reset button in the Main.storyboard and connect it to the reset method:
//
// ViewController.m
// VisibleViewDemo
//
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import "ViewController.h"
CG_INLINE void drawLine(UIView *view,CGPoint point1,CGPoint point2, UIColor *color, NSString *layerName) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:point1];
[path addLineToPoint:point2];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = color.CGColor;
shapeLayer.lineWidth = 2.0;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.name = layerName;
[view.layer addSublayer:shapeLayer];
}
CG_INLINE void removeShapeLayers(UIView *view,NSString *layerName) {
if (view.layer.sublayers.count > 0) {
for (CALayer *layer in [view.layer.sublayers copy]) {
if ([layer.name isEqualToString:layerName]) {
[layer removeFromSuperlayer];
}
}
}
}
CG_INLINE void drawXBox(UIView *view, CGRect rect,UIColor *color) {
NSString *layerName = #"xbox";
removeShapeLayers(view, layerName);
CGPoint topLeft = CGPointMake(rect.origin.x,rect.origin.y);
CGPoint topRight = CGPointMake(rect.origin.x + rect.size.width,rect.origin.y);
CGPoint bottomLeft = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height);
CGPoint bottomRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
drawLine(view,topLeft,topRight,color,layerName);
drawLine(view,topRight,bottomRight,color,layerName);
drawLine(view,topLeft,bottomLeft,color,layerName);
drawLine(view,bottomLeft,bottomRight,color,layerName);
drawLine(view,topLeft,bottomRight,color,layerName);
drawLine(view,topRight,bottomLeft,color,layerName);
}
#interface ViewController ()
#end
#implementation ViewController
UIView *view1;
UIView *view2;
UIView *view3;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGFloat width = [UIScreen mainScreen].bounds.size.width / 2;
CGFloat height = [UIScreen mainScreen].bounds.size.height / 4;
view1 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2, width, height)];
view1.backgroundColor = UIColor.yellowColor;
[self.view addSubview:view1];
[self addGestures:view1];
view2 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2 + height + 16, width, height)];
view2.backgroundColor = UIColor.greenColor;
[self.view addSubview:view2];
[self addGestures:view2];
view3 = [[UIView alloc] initWithFrame:CGRectMake(10, 10, width / 2, height / 2)];
view3.backgroundColor = [UIColor.blueColor colorWithAlphaComponent:0.5];
[view1 addSubview:view3]; // this one will behave differently
[self addGestures:view3];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self checkOnScreen:view1];
[self checkOnScreen:view2];
[self checkOnScreen:view3];
}
- (IBAction)reset:(id)sender {
view1.transform = CGAffineTransformIdentity;
view2.transform = CGAffineTransformIdentity;
view3.transform = CGAffineTransformIdentity;
[self.view setNeedsLayout];
}
- (void)addGestures:(UIView *)view {
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[view addGestureRecognizer:panGestureRecognizer];
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePinch:)];
[view addGestureRecognizer:pinchGestureRecognizer];
}
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view
}
- (void)checkOnScreen:(UIView *)view {
CGRect visibleRect = [self getVisibleRect:view];
if (CGRectEqualToRect(visibleRect, CGRectNull)) {
visibleRect = CGRectZero;
}
drawXBox(view,visibleRect,UIColor.blackColor);
}
//
// Pinch (resize) an image on the ViewController View
//
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
recognizer.view.transform = CGAffineTransformScale(initialTransform,recognizer.scale,recognizer.scale);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.transform = CGAffineTransformTranslate(initialTransform,translation.x,translation.y);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
#end
So you need to know the real visible frame of a view that is somehow derived from bounds+center+transform and calculate everything else from that, instead of the ordinary frame value. This means you'll also have to recreate convertRect:fromView: to be based on that. I always sidestepped the problem by using transform only for short animations where such calculations are not necessary. Thinking about coding such a -getVisibleRect: method makes me want to run away screaming ;)
What is a frame?
The frame property is derived from center and bounds.
Example:
center is (60,50)
bounds is (0,0,100,100)
=> frame is (10,0,100,100)
Now you change the frame to (10,20,100,100). Because the size of the view did not change, this results only in a change to the center. The new center is now (60,70).
How about transform?
Say you now transform the view, by scaling it to 50%.
=> the view has now half the size than before, while still keeping the same center. It looks like the new frame is (35,45,50,50). However the real result is:
center is still (60,50): this is expected
bounds is still (0,0,100,100): this should be expected too
frame is still (10,20,100,100): this is somewhat counterintuitive
frame is a calculated property, and it doesn't care at all about the current transform. This means that the value of the frame is meaningless whenever transform is not the identity transform. This is even documented behaviour. Apple calls the value of frame to be "undefined" in this case.
Consequences
This has the additional consequences that methods such as convertRect:fromView: do not work properly when there are non-standard transforms involved. This is because all these methods rely on either frame or bounds of views, and they break as soon as there are transforms involved.
What can be done?
Say you have three views:
view1 (no transform)
view2 (scale transform 50%)
view3 (no transform)
and you want to know the coordinates of view3 from the point of view of view1.
From the point of view of view2, view3 has frame view3.frame. Easy.
From the point of view of view1, view2 has not frame view2.frame, but the visible frame is a rectangle with size view2.bounds/2 and center view2.center.
To get this right you need some basic linear algebra (with matrix multiplications). (And don't forget the anchorPoint..)
I hope it helps..
What can be done for real?
In your question you said that there is an offset. Maybe you can just calculate the error now? The error should be something like 0.5 * (1-scale) * (bounds.size) . If you can calculate the error, you can subtract it and call it a day :)
Thanks to #Michael for putting in so much effort in his answer. It didn't solve the problem but it made me think some more and try some other things.
And voila, I tried something that I'm certain I had done before, but this time I started with my latest code. It turns out a simple solution did the trick. The builtin UIView convertRect:fromView and convertRect:toView worked as expected when used together.
I apologize to anyone that has spent time on this. I'm humbled in my foolishness and how much time I have spent on this. I must have made a mistake somewhere when I tried this before because it didn't work. But this works very well now:
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect rootRect = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect rootVisible = CGRectIntersection(vc.view.bounds, rootRect);
// convert the rect back to the initial view's coordinate system
CGRect visible = [view convertRect:rootVisible fromView:vc.view];
return visible; // may be same as the original view frame
}
If someone uses the Viewcontroller.m from my question, just replace the getVisibleRect method with this one and it will work very nicely.
NOTE: I tried rotating the view and the visible rect is rotated too because I displayed it on the view itself. I guess I could reverse whatever the view rotation is on the shape layers, but that's for another day!
I'm rendering CGPDFPage in UIImageView but not zooming how we can zoom if any know plz let me know
PDFDocument = CGPDFDocumentCreateWithURL((__bridge CFURLRef)pdfUrl);
totalPages = (int)CGPDFDocumentGetNumberOfPages(PDFDocument);
NSLog(#"total pages %i",totalPages);
//struct CGPDFPage *page =CGPDFDocumentGetPage(PDFDocument, 1);
CGFloat width = 600.0;
// Get the page
CGPDFPageRef myPageRef = CGPDFDocumentGetPage(PDFDocument, i);
// Changed this line for the line above which is a generic line
//CGPDFPageRef page = [self getPage:page_number];
CGRect pageRect = CGPDFPageGetBoxRect(myPageRef, kCGPDFMediaBox);
CGFloat pdfScale = width/pageRect.size.width;
pageRect.size = CGSizeMake(pageRect.size.width*pdfScale, pageRect.size.height*pdfScale);
pageRect.origin = CGPointZero;
UIGraphicsBeginImageContext(pageRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
// White BG
CGContextSetRGBFillColor(context, 1.0,1.0,1.0,1.0);
CGContextFillRect(context,pageRect);
CGContextSaveGState(context);
// ***********
// Next 3 lines makes the rotations so that the page look in the right direction
// ***********
CGContextTranslateCTM(context, 0.0, pageRect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextConcatCTM(context, CGPDFPageGetDrawingTransform(myPageRef, kCGPDFMediaBox, pageRect, 0, true));
CGContextDrawPDFPage(context, myPageRef);
CGContextRestoreGState(context);
imageView= UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
You should add your imageView as subview of UIScrollView.
This SO answer describes how to zoom UIImageView inside UIScrollView:
Set your view controller up as a <UIScrollViewDelegate>
Draw your UIScrollView the size you want for the rectangle at the center of the view. Set the max zoom in the inspector to something bigger than 1. Like 4 or 10.
Right click on the scroll view and connect the delegate to your view controller.
Draw your UIImageView in the UIScrollView and set it up with whatever image you want. Make it the same size as the UIScrollView.
Ctrl + drag form you UIImageView to the .h of your View controller to create an IBOutlet for the UIImageView, call it something clever like imageView.
Add this code:
-(UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
Run the app and pinch and pan til your heart's content.
Go with this
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.scrollView.minimumZoomScale=0.5;
self.scrollView.maximumZoomScale=6.0;
self.scrollView.contentSize=CGSizeMake(1280, 960);
self.scrollView.delegate=self;
}
Check Apple Documentation
Another way is to implement UITapGestureRecognizer in your viewController
- (void)viewDidLoad
{
[super viewDidLoad];
// target - what object is going to handle
// the gesture when it gets recognised
// the argument for tap: is the gesture that caused this message to be sent
UITapGestureRecognizer *tapOnce =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapOnce:)];
UITapGestureRecognizer *tapTwice =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapTwice:)];
tapOnce.numberOfTapsRequired = 1;
tapTwice.numberOfTapsRequired = 2;
//stops tapOnce from overriding tapTwice
[tapOnce requireGestureRecognizerToFail:tapTwice];
// then need to add the gesture recogniser to a view
// - this will be the view that recognises the gesture
[self.view addGestureRecognizer:tapOnce];
[self.view addGestureRecognizer:tapTwice];
}
Basically this code is saying that when a UITabGesture is registered in self.view the method tapOnce or tapTwice will be called in self depending on if its a single or double tap. You therefore need to add these tap methods to your UIViewController:
- (void)tapOnce:(UIGestureRecognizer *)gesture
{
//on a single tap, call zoomToRect in UIScrollView
[self.myScrollView zoomToRect:rectToZoomInTo animated:NO];
}
- (void)tapTwice:(UIGestureRecognizer *)gesture
{
//on a double tap, call zoomToRect in UIScrollView
[self.myScrollView zoomToRect:rectToZoomOutTo animated:NO];
}
Hope that helps
I'm a beginner with iOS, so i'm just not sure what to research here. I have a UIScrollView with a few square subViews added. How can i make the subviews smaller as they scroll off screen and bigger as they approach the center of the screen?
#import "HorizontalScrollMenuViewController.h"
#import <UIKit/UIKit.h>
#define SUBVIEW_WIDTH_HEIGHT 280
#interface HorizontalScrollMenuViewController : UIViewController
#property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
#end
#implementation HorizontalScrollMenuViewController
-(void)viewDidLoad{
[super viewDidLoad];
NSArray *colors = [NSArray arrayWithObjects:[UIColor greenColor],[UIColor redColor],[UIColor orangeColor],[UIColor blueColor],nil ];
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
CGFloat screenHeight = screenRect.size.height;
CGFloat originX = (screenWidth - SUBVIEW_WIDTH_HEIGHT)/2.0; // get margin to left and right of subview
CGFloat originY = ((screenHeight - SUBVIEW_WIDTH_HEIGHT)/2);
// add subviews of all activities
for (int i = 0; i < colors.count; i++){
CGRect frame = CGRectMake(0,0,SUBVIEW_WIDTH_HEIGHT,SUBVIEW_WIDTH_HEIGHT);
frame.origin.x = self.scrollView.frame.size.width * i + originX;
frame.origin.y = originY;
UIView *subView = [[UIView alloc] initWithFrame:frame];
[UIView setAnimationBeginsFromCurrentState: YES];
subView.layer.cornerRadius = 15;
subView.layer.masksToBounds = YES;
subView.backgroundColor = [colors objectAtIndex:i];
[self.scrollView addSubview:subView];
}
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * colors.count, self.scrollView.frame.size.height);
}
#end
Here you can find a fully working example of what you're trying to accomplish. It only has
one subview because it's just to give you an idea of how can you accomplish it. Also, this example was tested on an iPad (iOS7) simulator.
The *.h file
#import <UIKit/UIKit.h>
// Remember to declare ourselves as the scroll view delegate
#interface TSViewController : UIViewController <UIScrollViewDelegate>
#property (nonatomic, strong) UIView *squareView;
#end
The *.m file
#import "TSViewController.h"
#implementation TSViewController
#synthesize squareView = _squareView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Create and configure the scroll view (light gray)
UIScrollView *myScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(100, 100, 500, 500)];
CGRect contentSize = myScrollView.frame;
contentSize.size.height = contentSize.size.height + 400;
myScrollView.contentSize = contentSize.size;
myScrollView.userInteractionEnabled = YES;
// give the scroll view a gray color so it's easily identifiable
myScrollView.backgroundColor = [UIColor lightGrayColor];
// remember to set yourself as the delegate of the scroll view
myScrollView.delegate = self;
[self.view addSubview:myScrollView];
// Create and configure the square view (blue)
self.squareView = [[UIView alloc] initWithFrame:CGRectMake(200, 400, 60, 60)];
self.squareView.backgroundColor = [UIColor blueColor];
[myScrollView addSubview:self.squareView];
}
// Here is where all the work happens
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
// Get the difference between the contentOffset y position and the squareView y position
CGFloat y = self.squareView.frame.origin.y - scrollView.contentOffset.y;
// If the square has gone out of view, return
if (y <= 0) {
return;
}
// Modify the squareView's frame depending on it's current position
CGRect squareViewFrame = self.squareView.frame;
squareViewFrame.size.height = y + 5;
squareViewFrame.size.width = y + 5;
squareViewFrame.origin.x = (scrollView.contentSize.width - squareViewFrame.size.width) / 2.0;
self.squareView.frame = squareViewFrame;
}
#end
And here is a little explanation of what is going on:
A UIScrollView has several properties that allow you to configure it correctly. For example it has a frame (gray) which is inherited from UIView; with this property you specify the visible size of the scroll view. It also has the contentSize (red) which specifies the total size of the scroll view; in the image it's showed as the red area but this is only for illustration purposes as it will not be visible in the program. Imagine the scroll view's frame as the window that let's you see only a part of the bigger content the scroll view has.
When the user starts scrolling a gap appears between the top part of the contentSize and the top part of the frame. This gap is known as the contentOffset
Here is the reference to UIScrollView
Here is the reference to UIScrollViewDelegate
Hope this helps!
Assuming that you have the scrollView inside self.view, you can implement scrollViewDidScroll: in the scroll view delegate to find when it is scrolled.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (UIView *view in self.scrollView.subviews) {
CGRect frame = [view convertRect:view.frame toView:self.view]; // Contains the frame of view with respect to self.view
}
}
You can them use the frame to resize subviews as you want.
The answer starts with analyzing the UIScrollView Class Reference and it's delegate. In the delegate documentation see the responding to scrolling and dragging section. You should also review the sample code for each. You can create outlets to your subviews and change the subview properties within a uiview animation. These references will give you a good foundation in understanding where you can build the call to animate the subviews.
Here is a link to animating subviews. Additional examples can be found by Googling "uiview subview animation" (without the quotes). If you run into any major issues read the header files first and post some sample code for additional (more precise) help.
Other reference:
UIKit ScrollViews
Note to casual readers: Despite the title, this question has nothing to do with the UIScrollView properties bounces (scrolling related) or bouncesZoom.
I am using UIScrollView to add zooming to a custom view. The custom view uses sublayers to draw its content. Each sublayer is a CALayer instance that is added to the view's main layer with [CALayer addSublayer:]. Sublayers use CoreGraphics to render their content.
After each zoom completes, the custom view needs to redraw its content at the new zoom scale so that the content appears crisp and sharp again. I am currently trying to get the
approach to work that is shown in this SO question, i.e. I reset the scroll view's zoomScale property to 1.0 after each zoom operation, then I adjust the minimumZoomScale and maximumZoomScale properties so that the user cannot zoom in/out more than originally intended.
The content redrawing already works correctly (!), but what I am missing is a smooth GUI update so that the zoomed content is redrawn in place without appearing to move. With my current solution (code example follows at bottom of this question), I observe a kind of "bounce" effect: As soon as the zoom operation ends, the zoomed content briefly moves to a different location, then immediately moves back to its original location.
I am not entirely sure what the reason for the "bounce" effect is: Either there are two GUI update cycles (one for resetting zoomScale to 1.0, and another for setNeedsDisplay), or some sort of animation is taking place that makes both changes visible, one after the other.
My question is: How can I prevent the "bounce" effect described above?
UPDATE: The following is a minimal but complete code example that you can simply copy&paste to observe the effect that I am talking about.
Create a new Xcode project using the "Empty application" template.
Add the code below to AppDelegate.h and AppDelegate.m, respectively.
In the project's Link build phase, add a reference to QuartzCore.framework.
Stuff that goes into AppDelegate.h:
#import <UIKit/UIKit.h>
#class LayerView;
#interface AppDelegate : UIResponder <UIApplicationDelegate, UIScrollViewDelegate>
#property (nonatomic, retain) UIWindow* window;
#property (nonatomic, retain) LayerView* layerView;
#end
Stuff that goes into AppDelegate.m:
#import "AppDelegate.h"
#import <QuartzCore/QuartzCore.h>
#class LayerDelegate;
#interface LayerView : UIView
#property (nonatomic, retain) LayerDelegate* layerDelegate;
#end
#interface LayerDelegate : NSObject
#property(nonatomic, retain) CALayer* layer;
#property (nonatomic, assign) CGFloat zoomScale;
#end
static CGFloat kMinimumZoomScale = 1.0;
static CGFloat kMaximumZoomScale = 5.0;
#implementation AppDelegate
- (void) dealloc
{
self.window = nil;
self.layerView = nil;
[super dealloc];
}
- (BOOL) application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
[UIApplication sharedApplication].statusBarHidden = YES;
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
self.window.backgroundColor = [UIColor whiteColor];
UIScrollView* scrollView = [[[UIScrollView alloc] initWithFrame:self.window.bounds] autorelease];
[self.window addSubview:scrollView];
scrollView.contentSize = scrollView.bounds.size;
scrollView.delegate = self;
scrollView.minimumZoomScale = kMinimumZoomScale;
scrollView.maximumZoomScale = kMaximumZoomScale;
scrollView.zoomScale = 1.0f;
scrollView.bouncesZoom = NO;
self.layerView = [[[LayerView alloc] initWithFrame:scrollView.bounds] autorelease];
[scrollView addSubview:self.layerView];
[self.window makeKeyAndVisible];
return YES;
}
- (UIView*) viewForZoomingInScrollView:(UIScrollView*)scrollView
{
return self.layerView;
}
- (void) scrollViewDidEndZooming:(UIScrollView*)scrollView withView:(UIView*)view atScale:(float)scale
{
CGPoint contentOffset = scrollView.contentOffset;
CGSize contentSize = scrollView.contentSize;
scrollView.maximumZoomScale = scrollView.maximumZoomScale / scale;
scrollView.minimumZoomScale = scrollView.minimumZoomScale / scale;
// Big change here: This resets the scroll view's contentSize and
// contentOffset, and also the LayerView's frame, bounds and transform
// properties
scrollView.zoomScale = 1.0f;
CGFloat newZoomScale = self.layerView.layerDelegate.zoomScale * scale;
self.layerView.layerDelegate.zoomScale = newZoomScale;
self.layerView.frame = CGRectMake(0, 0, contentSize.width, contentSize.height);
scrollView.contentSize = contentSize;
[scrollView setContentOffset:contentOffset animated:NO];
[self.layerView setNeedsDisplay];
}
#end
#implementation LayerView
- (id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.layerDelegate = [[[LayerDelegate alloc] init] autorelease];
[self.layer addSublayer:self.layerDelegate.layer];
// super's initWithFrame already invoked setNeedsDisplay, but we need to
// repeat because at that time our layerDelegate property was still empty
[self setNeedsDisplay];
}
return self;
}
- (void) dealloc
{
self.layerDelegate = nil;
[super dealloc];
}
- (void) setNeedsDisplay
{
[super setNeedsDisplay];
// Zooming changes the view's frame, but not the frame of the layer
self.layerDelegate.layer.frame = self.bounds;
[self.layerDelegate.layer setNeedsDisplay];
}
#end
#implementation LayerDelegate
- (id) init
{
self = [super init];
if (self)
{
self.layer = [CALayer layer];
self.layer.delegate = self;
self.zoomScale = 1.0f;
}
return self;
}
- (void) dealloc
{
self.layer = nil;
[super dealloc];
}
- (void) drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
CGRect layerRect = self.layer.bounds;
CGFloat radius = 25 * self.zoomScale;
CGFloat centerDistanceFromEdge = 5 * self.zoomScale + radius;
CGPoint topLeftCenter = CGPointMake(CGRectGetMinX(layerRect) + centerDistanceFromEdge,
CGRectGetMinY(layerRect) + centerDistanceFromEdge);
[self drawCircleWithCenter:topLeftCenter radius:radius fillColor:[UIColor redColor] inContext:context];
CGPoint layerCenter = CGPointMake(CGRectGetMidX(layerRect), CGRectGetMidY(layerRect));
[self drawCircleWithCenter:layerCenter radius:radius fillColor:[UIColor greenColor] inContext:context];
CGPoint bottomRightCenter = CGPointMake(CGRectGetMaxX(layerRect) - centerDistanceFromEdge,
CGRectGetMaxY(layerRect) - centerDistanceFromEdge);
[self drawCircleWithCenter:bottomRightCenter radius:radius fillColor:[UIColor blueColor] inContext:context];
}
- (void) drawCircleWithCenter:(CGPoint)center
radius:(CGFloat)radius
fillColor:(UIColor*)color
inContext:(CGContextRef)context
{
const int startRadius = [self radians:0];
const int endRadius = [self radians:360];
const int clockwise = 0;
CGContextAddArc(context, center.x, center.y, radius,
startRadius, endRadius, clockwise);
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillPath(context);
}
- (double) radians:(double)degrees
{
return degrees * M_PI / 180;
}
#end
Based on your sample project, the key is that you're manipulating a CALayer directly. By default, setting CALayer properties, such as frame, cause animations. The suggestion to use [UIView setAnimationsEnabled:NO] was on the right track, but only affects UIView-based animations. If you do the CALayer equivalent, say in your setNeedsDisplay: method:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layerDelegate.layer.frame = self.bounds;
[CATransaction commit];
It prevents the implicit frame-changing animation and looks right to me. You can also disable these implicit animations via a CALayerDelegate method in your LayerDelegate class:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
return (id)[NSNull null]; // NSNull means "don't do any implicit animations"
}
Original Suggestions:
Perhaps you are in an animation block without knowing it? Or, perhaps one of the methods you're calling is setting up an animation block? What if you [UIView setAnimationsEnabled:NO] before your code and re-enable them after?
If it's not an animation, then it's probably as you suspect; two view updates of some kind. (Perhaps one from the scroll view, and one from your code somehow?) Some runnable sample code would be great in that case.
(Out of curiosity, have you tried using CALayer's shouldRasterize and rasterizationScale rather than faking out the zoom level?)
In the X Code user interface builder there's a Bounce setting (it's under Scroll View).
I'm trying to draw a rectangle which has four circular handles. Here's what it would look like:
o----o
| |
| |
o----o
The circular handles are "hot". In other words, when the user touches it, the handle can be moved around while the rest of the points are anchored. I wanted to know if anyone had an approach for coding this functionality. I'm looking at UIBezierPath to draw the rectangle with circles, but I'm having a hard time thinking about how to allow the user to tap only the circles. I was thinking it may need to be five different UIBezierPath objects, but eventually the UI will consist of multiples of these objects.
Any suggestions would be greatly appreciated. Thanks.
I wouldn't draw it as a single shape with complicated UIBezierPaths at all. I'd think about it as 6 different pieces. A Container, a rectangle, and 4 circles.
I would have a simple container UIView that has a rectangle view and four circular UIViews at its corners. Then put a UIPanGestureRecognizer on each circle. In the gesture handler, move the center of the circle and adjust the underlying rectangle rect by the same amount. This will avoid any complicated paths or math and make it simple add and subtract amounts on the rectangle itself.
Update: Code!
I created a self contained UIView subclass that handles everything. You can create one like so:
HandlesView *view = [[HandlesView alloc] initWithFrame:self.view.bounds];
[view setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];
// A custom property that contains the selected area of the rectangle. Its updated while resizing.
[view setSelectedFrame:CGRectMake(128.0, 128.0, 200.0, 200.0)];
The frame of the view itself is the total draggable area. The selected frame is the inner visible rectangle.
//
// HandlesView.h
// handles
//
// Created by Ryan Poolos on 2/12/13.
// Copyright (c) 2013 Ryan Poolos. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#interface HandlesView : UIView
#property (nonatomic, readwrite) CGRect selectedFrame;
#end
And here is the implementation.
//
// HandlesView.m
// handles
//
// Created by Ryan Poolos on 2/12/13.
// Copyright (c) 2013 Ryan Poolos. All rights reserved.
//
#import "HandlesView.h"
#interface HandlesView ()
{
UIView *rectangle;
NSArray *handles;
NSMutableArray *touchedHandles;
UIView *circleTL;
UIView *circleTR;
UIView *circleBL;
UIView *circleBR;
}
#end
#implementation HandlesView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
rectangle = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, 22.0, 22.0)];
[self addSubview:rectangle];
// Create the handles and position.
circleTL = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleTL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
circleTR = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleTR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
circleBL = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleBL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
circleBR = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleBR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
handles = #[ circleTL, circleTR, circleBL, circleBR ];
for (UIView *handle in handles) {
// Round the corners into a circle.
[handle.layer setCornerRadius:(handle.frame.size.width / 2.0)];
[self setClipsToBounds:YES];
// Add a drag gesture to the handle.
[handle addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)]];
// Add the handle to the screen.
[self addSubview:handle];
}
}
return self;
}
- (void)setSelectedFrame:(CGRect)selectedFrame
{
[rectangle setFrame:selectedFrame];
[circleTL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
[circleTR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
[circleBL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
[circleBR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
}
- (CGRect)selectedFrame
{
return rectangle.frame;
}
// Forward the background color.
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
// Set the container to clear.
[super setBackgroundColor:[UIColor clearColor]];
// Set our rectangle's color.
[rectangle setBackgroundColor:[backgroundColor colorWithAlphaComponent:0.5]];
for (UIView *handle in handles) {
[handle setBackgroundColor:backgroundColor];
}
}
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
// The handle we're moving.
UIView *touchedHandle = gesture.view;
// Keep track of touched Handles.
if (!touchedHandles) {
touchedHandles = [NSMutableArray array];
}
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
[touchedHandles addObject:touchedHandle];
break;
case UIGestureRecognizerStateChanged:
{
CGPoint tranlation = [gesture translationInView:self];
// Calculate this handle's new center
CGPoint newCenter = CGPointMake(touchedHandle.center.x + tranlation.x, touchedHandle.center.y + tranlation.y);
// Move corresponding circles
for (UIView *handle in handles) {
if (handle != touchedHandle && ![touchedHandles containsObject:handle]) {
// Match the handles horizontal movement
if (handle.center.x == touchedHandle.center.x) {
handle.center = CGPointMake(newCenter.x, handle.center.y);
}
// Match the handles vertical movement
if (handle.center.y == touchedHandle.center.y) {
handle.center = CGPointMake(handle.center.x, newCenter.y);
}
}
}
// Move this circle
[touchedHandle setCenter:newCenter];
// Adjust the Rectangle
// The origin and just be based on the Top Left handle.
float x = circleTL.center.x;
float y = circleTL.center.y;
// Get the width and height based on the difference between handles.
float width = abs(circleTR.center.x - circleTL.center.x);
float height = abs(circleBL.center.y - circleTL.center.y);
[rectangle setFrame:CGRectMake(x, y, width, height)];
[gesture setTranslation:CGPointZero inView:self];
}
break;
case UIGestureRecognizerStateEnded:
[touchedHandles removeObject:touchedHandle];
break;
default:
break;
}
}
#end
This is only a proof of concept. There are a lot of missing caveats like being able to drag outside the box, multitouch complications, negative sizes. All these problems can be handled very differently and are the secret sauce that makes something like this go from a nice idea to a beautiful custom interface. I'll leave that part up to you. :)
You will want to store the circle bezier paths in your class for when you implement gesture recognizers.
There is an Apple document describing how to implement a UIView or UIControl that accepts touch events with pictures and sample code.
http://developer.apple.com/library/ios/#documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/multitouch_background/multitouch_background.html#//apple_ref/doc/uid/TP40009541-CH5-SW9