I'm trying to add a vertical parallax scrolling effect between several views that are all the size of the screen.
This wasn't too complicated when done through buttons, however I want it to be controlled by a UIPanGesture, similar to the Year Walk Companion App for iOS. The current equation shown here doesn't work correctly.
Currently this version only moves the first two views, because once I get those working the rest should be easy. I've also not included all the checks that would stop certain views moving in certain directions because that is also not relevant to the issue.
Here is a web version of it working with buttons: http://flycarpodacus.zxq.net/iPhone%20Parallax/event.html
Hope someone can help.
ViewController.h
#interface ViewController : UIViewController {
UIPanGestureRecognizer *panGesture;
int activeView;
int nextView;
float offset;
float speed;
}
#property (weak, nonatomic) IBOutlet UIView *view1;
#property (weak, nonatomic) IBOutlet UIView *view2;
#property (weak, nonatomic) IBOutlet UIView *view3;
#property (weak, nonatomic) IBOutlet UIView *view4;
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize view1, view2, view3, view4;
- (void)viewDidLoad
{
[super viewDidLoad];
activeView = 1;
offset = 220;
speed = 1;
panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanGesture:)];
[self.view addGestureRecognizer:panGesture];
if (self.view.frame.size.height != 568) {
CGRect frame = self.view.frame;
frame.size.height = self.view.frame.size.height;
view1.frame = frame;
view2.frame = frame;
view3.frame = frame;
view4.frame = frame;
}
view4.center = CGPointMake(view4.center.x, self.view.frame.size.height/2 + offset);
view3.center = CGPointMake(view3.center.x, self.view.frame.size.height/2 + offset);
view2.center = CGPointMake(view2.center.x, self.view.frame.size.height/2 + offset);
view1.center = CGPointMake(view1.center.x, self.view.frame.size.height/2);
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint panGestureTranslation = [panGestureRecognizer translationInView:self.view];
switch (activeView) {
case 1:
view1.center = CGPointMake(view1.center.x, panGestureTranslation.y + self.view.frame.size.height/2);
view2.center = CGPointMake(view2.center.x, view2.center.y - 2.5f);
break;
default:
break;
}
}
Ok so, in my point of view you could try to do something like that :
first use only two view, in your example only two view is visible at the same time and so you could re-use the view for
the next slide you want to display.
So at the start you have 2 views (lets say they have the same size than the screen 300x500, yes I know they are not the real size,
but it will be more easy for the math :p).
In you init function place you view one in position 150x250 (the center of the screen), and the screen 2 at 150x500 (so you see only
the upper half of your view).
lets call panGestureTranslation.y : DeltaY (it will be more quick). so in the function panGestureRecognizer you want to move the two view
with the parralex effect, one simple solution in this exact case will be to move view1 of 2*DeltaY and view2 of DeltaY, like that, when the view1
will have quit the screen completly, the view two will be right in the center of it.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
you can know if is finish by test the position of view one (if the position.y is <= than 0, so it's finish.
when is finish, you have two choice, first you put the view1 in the bottom of view2 (at position 150x500 and display behind view2 ) and you feel it with your 3rd content, then switch a booleen
to do the excact opposite the next frame (you will move view2 of 2*DeltaY and view1 of DeltaY).
other solution is to move the two view at the original position (view1 at 150x250 and view two at 150x500), and feel view1 with the content of view2 (you have to do it without animation
so the user doesn't see that the view have switch), and feel view2 with you 3rd content, and so next frame, the exact same code will work again. you can have an infinity of content with this method.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
let me know if I wasn't clear on some point ^^.
I would suggest checking our article on parallax effect with detailed explanation and open source implementation => http://blog.denivip.ru/index.php/2013/08/parallax-in-ios-applications/?lang=en
Related
In iOS, when dragging an UIImageView (red square) using UIPanGestureRecognizer in a scrollview (bar at bottom within another UIImageView), I cannot drag out of the constraints of the bar into the main body UIView sitting above? Any help would be greatly appreciated. Thanks.
For clarity, here’s my view structure:
view (main controller)
|
——> view (nav bar)
|
——> view (main body)
|
——> view (yellow bar)
|——> scroll view (dynamic)
|
——>view (dynamic - wider than scroll view)
|
——> image view (dynamically created red square)
which looks like:
what I'm doing:
Basically, in ViewController.m, I’m programmatically creating a scrollview (barScrollView) and adding it to a view (yellow bar). I dynamically create another new view and pass it to a function and within that function, in a loop, I dynamically create several ImageView items (red squares), enable userInteractionEnabled on them, create the UIPanGestureRecognizer and add it to each ImageView created, then add them to the view that was passed in. Lastly, I add the view that was passed in to the scrollview
my code:
In storyboard:
- dragged in 3 UIViews (see image above)
In ViewController.h:
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#property (weak, nonatomic) IBOutlet UIView *navBar;
#property (weak, nonatomic) IBOutlet UIView *mainBodyView;
#property (weak, nonatomic) IBOutlet UIView *yellowBar;
#end
In ViewController.m:
- (void)viewDidLoad {
[super viewDidLoad];
CGRect barRect = CGRectMake(0, 0, 320, 60);
CGRect containerRect = CGRectMake(0, 0, 820, 60);
UIScrollView *barSV = [[UIScrollView alloc] initWithFrame:barRect];
[_yellowBar addSubview:barSV];
UIView *redSquaresContainerView = [[UIScrollView alloc] initWithFrame:containerRect];
[self generateRedSquares:redSquaresContainerView];
[barSV addSubview:redSquaresContainerView];
barSV.contentSize =containerRect.size;
}
-(void)generateRedSquares:(UIView*) containerView{
int x =0;
for( int i = 0; i < 5; i++ ) {
CGRect redSquareRect = CGRectMake(x, 10, 30, 30);
UIImageView* aRedSquareView = [[UIImageView alloc] initWithFrame:redSquareRect];
aRedSquareView.backgroundColor = [UIColor redColor];
aRedSquareView.userInteractionEnabled = YES;
UIPanGestureRecognizer *dragRecognizer = [[UIPanGestureRecognizer
alloc]initWithTarget:self action:#selector(dragRedSquare:)];
[aRedSquareView addGestureRecognizer:dragRecognizer];
[containerView addSubview:aRedSquareView];
x+=60;
}
}
- (void) dragRedSquare:(UIPanGestureRecognizer*)recognizer{
static CGRect originalFrame;
if ([recognizer state] == UIGestureRecognizerStateBegan) {
originalFrame = recognizer.view.frame;
} else if ([recognizer state] == UIGestureRecognizerStateChanged) {
// Drag moved
CGPoint translate = [recognizer translationInView:recognizer.view];
CGRect newFrame = CGRectMake(originalFrame.origin.x + translate.x,
originalFrame.origin.y + translate.y,
originalFrame.size.width,
originalFrame.size.height);
if (CGRectContainsRect(recognizer.view.superview.bounds, newFrame)){
recognizer.view.frame = newFrame;
}
} else if ([recognizer state] == UIGestureRecognizerStateEnded) {
// Drag completed
}
}
If you are using storyboards, make sure that that UIElement is UNDERNEATH the view you want it to go over.
So, For example in the image below, I want the UIImage to be drawn over all other view so I can drag it over everything else on the screen.
If you want to drag the ImageView out of the constraints of the then try to bring it to the font of the main view instead of adding it as a child you may need to addict as a sibling to pan across the view
This worked for me and hopefully will help someone else down the line. Regardless, if you're working with scrollviews/gestures and stuck, watching the below video is truly helpful. (I'll try to elaborate more later..thanks)
In dragRedSquare function, include the following two lines w/in the if statement:
- (void) dragRedSquare:(UIPanGestureRecognizer*)recognizer{
...
if ([recognizer state] == UIGestureRecognizerStateBegan) {
recognizer.view.center = [self.view convertPoint:recognizer.view.center fromView:recognizer.view.superview];
[self.view addSubview:recognizer.view];
}
...
}
Resource: https://developer.apple.com/videos/wwdc/2014/
(Advanced Scrollviews and Touch Handling Techniques: ~28min mark)
I have a CustomScrollView which holds a list of UILabels and scrolls through them (the subclassing is for automatic positioning of the views). In the class I have a list of these views stored as an NSMutableArray. I have found that when I ask for label.view.frame.origin after getting the label from the array, I always get {0, 0}. Because of this, the logic I use to scroll does not work (the scroll is done programmatic-ally). I am trying to have the UILabel scroll to the center of the frame of the CustomScrollView. Here is a code sample to show where I am having problems:
#interface CustomScrollView : UIScrollView
#property (nonatomic) NSMutableArray* labels; //This is the array of UILabels
#end
#implementation CustomScrollView
-(void)scrollToIndex:(int)index withAnimation:(bool)animated
{
UILabel *label = (UILabel*)self.labels[index];
CGRect rect = CGRectMake(label.frame.origin.x - ((self.frame.size.width - label.frame.size.width) / 2), 0, self.frame.size.width, self.frame.size.height);
[self scrollRectToVisible:rect animated:animated];
}
#end
TL:DR - label.frame.origin.x is returning me 0 and not what it's relative position is within the superview.
Bonus - Whenever I instantiate my custom scroll view, it automatically adds two subviews which I have no idea where the come from. I set the background color and turn off the scroll bars and scroll enabled.
Thanks in advance for the help.
Edit [Jun 25 11:38] - Turns out the rect I am creating to scroll is correct, but calling [self scrollRectToVisible:rect animated:animated] is just not working.
Edit [Jun 25 12:04] - Here is the method which I call every time I add a subview
-(UILabel*)lastLabel
{
if (self.labels.count == 0)
return nil;
return (UILabel*)self.labels.lastObject;
}
-(void)adjustContentSize
{
if (self.labels.count > 0)
{
float lastModifier = [self lastLabel].frame.origin.x + [self lastLabel].frame.size.width + ((self.frame.size.width - [self lastLabel].frame.size.width) / 2);
self.contentSize = CGSizeMake(MAX(lastModifier, self.contentSize.width), self.frame.size.height);
}
}
Try using the convertRect function:
CGRect positionOfLabelInScrollView = [self.scrollView convertRect:myLabel.frame toView:nil];
[self.scrollView setContentOffset:CGPointMake(0, topOfLabel - positionOfLabelInScrollView.origin.y + positionOfLabelInScrollView.size.height) animated:YES];
This should scroll your scrollView to display the label.
Recently I used UIDynamics to animate an image view into place. However, because its autolayout y-pos constraint was set to off-screen, when navigating away from the screen and then returning to it, my image view was being placed off-screen again. The animation took about 3 seconds, so after three seconds I just reset the constraint. That feels a little hacky.
So my question is this: what is the proper way to handle autolayout and UIDynamics at the same time?
This is not really a dynamics problem. Autolayout is incompatible with any view animation, or any manual setting of the frame: when layout comes along, it is the constraints that will be obeyed. It is up to you, if you move a view manually in any way, to update the constraints to match its new position/size/whatever.
Having said that: with UIKit Dynamics, when the animation ends, the animator will pause, and the animator's delegate is notified:
https://developer.apple.com/library/ios/documentation/uikit/reference/UIDynamicAnimatorDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIDynamicAnimatorDelegate/dynamicAnimatorDidPause:
So that is the moment to update the constraints.
You have a nice solution provided by Geppy Parziale in this tutorial.
Basically you can create an object that conforms to UIDynamicItem:
#interface DynamicHub : NSObject <UIDynamicItem>
#property(nonatomic, readonly) CGRect bounds;
#property(nonatomic, readwrite) CGPoint center;
#property(nonatomic, readwrite) CGAffineTransform transform;
#end
That needs to init the bounds or it will crash:
- (id)init {
self = [super init];
if (self) {
_bounds = CGRectMake(0, 0, 100, 100);
}
return self;
}
And then you use UIDynamics on that object and use the intermediate values to update your constraints:
DynamicHub *dynamicHub = [[DynamicHub alloc] init];
UISnapBehavior *snapBehavior = [[UISnapBehavior alloc] initWithItem:dynamicHub
snapToPoint:CGPointMake(50.0, 150.0)];
[snapBehavior setDamping:.1];
snapBehavior.action = ^{
self.firstConstraint.constant = [dynamicHub center].y;
self.secondConstraint.constant = [dynamicHub center].x;
};
[self.animator addBehavior:snapBehavior];
So I effectively have a image I'd like to zoom horizontally but also be able to respect the location of the pinch. So if you pinched on the left, It'd zoom into the left. Ideally, the points where you pinch would stay with your fingers.
My case is a little more specific, I'm plotting data on a graph, so I instead will be manipulating an array of data and taking a subset. However, I'm sure the math is similar. (BUT, I can't just use an Affinetransform as most examples I've found does)
Any ideas?
The default pinch gesture handler scales the graph around the point of the gesture. Use a plot space delegate to limit the scaling to only one axis.
Do you use a UIScrollView? As far as I know, you'll get this behaviour for free.
I built the below solution for standard UIViews where UIScrollView and CGAffineTransform zooms were not appropriate as I did not want the view's subviews to be skewed. I’ve also used it with a CorePlot line graph.
The zooming is centred on the point where the user starts the pinch.
Here’s a simple implementation:
#interface BMViewController ()
#property (nonatomic, assign) CGFloat pinchXCoord; // The point at which the pinch occurs
#property (nonatomic, assign) CGFloat minZoomProportion; // The minimum zoom proportion allowed. Used to control minimum view width
#property (nonatomic, assign) CGFloat viewCurrentXPosition; // The view-to-zoom’s frame’s current origin.x value.
#property (nonatomic, assign) CGFloat originalViewWidth, viewCurrentWidth; // original and current frame width of the view.
#end
#implementation BMViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Setup the pinch gesture recognizer
UIPinchGestureRecognizer *pinchZoomRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(pinchZoomGestureDetected:)];
[self.view addGestureRecognizer:pinchZoomRecognizer];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.originalViewWidth = self.view.frame.size.width;
self.viewCurrentWidth = self.originalViewWidth;
}
- (void)pinchZoomGestureDetected:(UIPinchGestureRecognizer *)recognizer
{
CGPoint pinchLocation = [recognizer locationInView:self.view];
if (recognizer.state == UIGestureRecognizerStateBegan) {
self.viewCurrentWidth = self.view.frame.size.width;
self.viewCurrentXPosition = self.view.frame.origin.x;
// Set the pinch X coordinate to a point relative to the bounds of the view
self.pinchXCoord = pinchLocation.x - self.viewCurrentXPosition;
self.minZoomProportion = self.originalViewWidth / self.viewCurrentWidth;
}
CGFloat proportion = recognizer.scale;
CGFloat width = self.viewCurrentWidth * MAX(proportion, self.minZoomProportion); // Set a minimum zoom width (the original size)
width = MIN(self.originalViewWidth * 4, width); // Set a maximum zoom width (original * 4)
CGFloat rawX = self.viewCurrentXPosition + ((self.viewCurrentWidth - width) * (self.pinchXCoord / self.viewCurrentWidth)); // Calculate the new X value
CGRect frame = self.view.frame;
CGFloat newXValue = MIN(rawX, 0); // Don't let the view move too far right
newXValue = MAX(newXValue, self.originalViewWidth - width); // Don't let the view move too far left
self.view.frame = CGRectMake(newXValue, frame.origin.y, width, frame.size.height);
NSLog(#"newXValue: %f, width: %f", newXValue, width);
}
#end
This is all that needs to be done to resize the horizontal axis of a UIView.
For Core-Plot, it is assumed the CPTGraphHostingView is a subview of the UIViewController’s view being resized. The CPTGraphHostingView is redrawn when its frame/bounds are changed so, in the layoutSubviews method of the containing view change the CPTGraphHostingView and plot area’s frame’s relative to the bounds of its parent (which is the view you should be resizing). Something like this:
self.graphHostingView.frame = self.bounds;
self.graphHostingView.hostedGraph.plotAreaFrame.frame = self.bounds;
I haven’t attempted to change data on the graph as it zooms, but I can’t imagine it would be too difficult. In layoutSubviews on your containing view, call reloadData on your CPTGraph.
I have a screen containing:
UIView A
UIScrollView, which contains UIView B
The UIScrollView overlaps with UIView A (up until about halfway through it), allowing me to "drag" UIView B and make it overlap it with UIView A. This is so that when I let go of it, UIView B bounces nicely back into its original position (I don't want to be able to drag it all around the screen, just from its starting point onto UIView A)
Now, I'm trying to detect a collision when the UIViews overlap. However, none is ever detected. I am using convertRect:toView: to get coordinates on the same system, but from tracing UIView B's coordinates in its touchesMoved I see that "dragging" (scrolling the UIScrollView, really) doesn't affect its frame's origin coordinates. They are the same in touchedBegan as they are every time touchedMoved fires.
I'm assuming this is because UIView B isn't really moving anywhere, UIScrollView is, meaning that the UIView's origin point is always the same, even if its absolute coordinates aren't. Any suggestion on how to handle this?
There will be overlap when scrollView.frame.origin.x + viewB.frame.size.width + scrollView.contentOffset.x >= viewA.frame.origin.x
After Edit:
I couldn't get the scroll view to work so I tried it another way that worked pretty well. Instead of a scroll view, I just used a rectangular view with a background color, and added the blue square to it. I gave the blue square a set width and height in IB, centered it in the long skinny view (in the y direction), and had one other constraint to the left side of that long view. The IBOutlet leftCon is connected to that constraint. I then used this code to drag it, and have it go back when I let go:
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (strong,nonatomic) UIPanGestureRecognizer *panner;
#property (strong,nonatomic) IBOutlet NSLayoutConstraint *leftCon;
#property (strong,nonatomic) CADisplayLink *displayLink;
#end
#implementation ViewController {
IBOutlet UIView *blueSquare;
}
-(void)viewDidLoad {
self.panner = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanGesture:)];
[blueSquare addGestureRecognizer:self.panner];
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender {
CGPoint translate = [sender translationInView:self.view];
if (self.leftCon.constant <160)
if (self.leftCon.constant > 100) NSLog(#"Bang! We have a collision");
self.leftCon.constant = translate.x;
if (sender.state == UIGestureRecognizerStateEnded){
[self startDisplayLink];
}
}
-(void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(bounceBack:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
-(void)bounceBack:(CADisplayLink *) link {
self.leftCon.constant -= 12;
if (self.leftCon.constant < 2) {
[self stopDisplayLink];
self.leftCon.constant = 2;
}
}
The numbers in the translate code (160 and 100) were determined empirically by logging. The 160 number keeps it from going off the right edge of the long view, and the 100 number is where it starts to overlap the black box.