I would like to decrease the zooming speed behavior of a UIScrollView.
I've tried the solutions given in this two answers: Answer 1, Answer 2, but I'm not getting the expected results.
This is my implementation of answer 2:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
if(scrollView.multipleTouchEnabled){
CGFloat fSpeedZoom = 2;
CGFloat fMaxZoomScale = scrollView.maximumZoomScale;
CGFloat fMinZoomScale = scrollView.minimunZoomScale;
scrollView.maximumZoomScale = scrollView.zoomScale + fSpeedZoom;
scrollView.minimumZoomScale = scrollView.zoomScale - fSpeedZoom;
if(scrollView.maximumZoomScale > fMaxZoomScale){
self.scrollView.maximumZoomScale = fMaxZoomScale;
}
if(scrollView.minimumZoomScale < fMinZoomScale){
self.scrollView.minimumZoomScale = fMinZoomScale;
}
}
}
If I keep the fSppedZoom values between 2 and 5 I get kind of an exponential behavior in which the further I try to zoom, the slower it zooms. I would like to get a linear behavior in which with a single parameter I could control the zooming speed.
I would recommend against doing this.
When you drag, the point that was underneath your finger when the drag started stays underneath your finger as the drag pans. This remains the case until you reach the edge of the draggable area, at which time the point moves in the same direction as the pan, but not as far, to simulate resistance.
When you pinch to zoom, the same thing happens. The two points that were underneath your two fingers stay underneath your finger as the pinch moves (hence you can pan at the same time as zooming). Again, this only breaks down when further zooming is not possible.
If you break this touch-point relationship, user interactions feel alien and unnatural.
If you absolutely must do this, I would recommend, instead of trying to fight UIScrollView's built-in zooming behaviour, implementing your own pinch gesture recogniser and considering the relationship you want to achieve between the positions of your touches and the area of the scroll view you want to show.
Related
I'm trying to implement a paging interaction for one of my views. I thought I would just use UISwipeGestureRecognizers. But through trial and error as well as examination of the documentation it appears that
A swipe is a discrete gesture, and thus the associated action message
is sent only once per gesture.
So while I could trigger the page, I wouldn't be able to hook up animation that occurred during the drag.
Is my only alternative to use a UIPanGestureRecognizer and reimplement the basic filtering/calculations of the swipe?
Update/Redux
In hindsight, what I really should have been asking is how to implement a "flick" gesture. If you're not going to roll your own subclass (may bite that off in a bit), you use a UIPanGestureRecognizer as #Lyndsey 's answer indicates. What I was looking for after that (in the comments) was how to do the flick part, where the momentum of the flick contributes to the decision of whether to carry the motion of the flick through or snap back to the original presentation.
UIScrollView has behavior like that and it's tempting to mine its implementation for details on how one decelerates the momentum in a way that would be consistent, but alas the decelerationRate supplied for UIScrollView is "per iteration" value (according to some). I beat my head on how to properly apply the default value of 0.998 to the end velocity of my pan.
In the end, I used code pulled from sites about "flick" computation and did something like this in my gesture handler:
...
else if (pan.state == UIGestureRecognizerStateEnded) {
CGFloat v = [pan velocityInView: self.view].x;
CGFloat a = -4000.0; // 4 pixels/millisecond, recommended on gavedev
CGFloat decelDisplacement = -(v * v) / (2 * a); // physics 101
// how far have we come plus how far will momentum carry us?
CGFloat totalDisplacement = ABS(translation) + decelDisplacement;
// if that is (or will be) half way across our view, finish the transition
if (totalDisplacement >= self.view.bounds.size.width / 2) {
// how much time would we need to carry remainder across view with current velocity and existing displacement? (capped)
CGFloat travelTime = MIN(0.4, (self.view.bounds.size.width - ABS(translation)) * 2 / ABS(v));
[UIView animateWithDuration: travelTime delay: 0.0 options: UIViewAnimationOptionCurveEaseOut animations:^{
// target/end animation positions
} completion:^(BOOL finished) {
if (finished) {
// any final state change
}
}];
}
else { // put everything back the way it was
...
}
}
Yes, use a UIPanGestureRecognizer if you want the specific speed, angle, changes, etc. of the "swipe" to trigger your animations. A UISwipeGestureRecognizer is indeed a single discrete gesture; similar to a UITapGestureRecognizer, it triggers a single action message upon recognition.
As in physics, the UIPanGestureRecognizer's "velocity" will indicate both the speed and direction of the pan gesture. Here are the docs for velocityInView: method which will help you calculate the horizontal and vertical components of the changing pan gesture in points per second.
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'm using UIKit Dynamics to push a UIView off screen, similar to how Tweetbot performs it in their image overlay.
I use a UIPanGestureRecognizer, and when they end the gesture, if they exceed the velocity threshold it goes offscreen.
[self.animator removeBehavior:self.panAttachmentBehavior];
CGPoint velocity = [panGestureRecognizer velocityInView:self.view];
if (fabs(velocity.y) > 100) {
self.pushBehavior = [[UIPushBehavior alloc] initWithItems:#[self.scrollView] mode:UIPushBehaviorModeInstantaneous];
[self.pushBehavior setTargetOffsetFromCenter:centerOffset forItem:self.scrollView];
self.pushBehavior.active = YES;
self.pushBehavior.action = ^{
CGPoint lowestPoint = CGPointMake(CGRectGetMinX(self.imageView.bounds), CGRectGetMaxY(self.imageView.bounds));
CGPoint convertedPoint = [self.imageView convertPoint:lowestPoint toView:self.view];
if (!CGRectIntersectsRect(self.view.bounds, self.imageView.frame)) {
NSLog(#"outside");
}
};
CGFloat area = CGRectGetWidth(self.scrollView.bounds) * CGRectGetHeight(self.scrollView.bounds);
CGFloat UIKitNewtonScaling = 5000000.0;
CGFloat scaling = area / UIKitNewtonScaling;
CGVector pushDirection = CGVectorMake(velocity.x * scaling, velocity.y * scaling);
self.pushBehavior.pushDirection = pushDirection;
[self.animator addBehavior:self.pushBehavior];
}
I'm having an immense amount of trouble detecting when my view actually completely disappears from the screen.
My view is setup rather simply. It's a UIScrollView with a UIImageView within it. Both are just within a UIViewController. I move the UIScrollView with the pan gesture, but want to detect when the image view is off screen.
In the action block I can monitor the view as it moves, and I've tried two methods:
1. Each time the action block is called, find the lowest point in y for the image view. Convert that to the view controller's reference point, and I was just trying to see when the y value of the converted point was less than 0 (negative) for when I "threw" the view upward. (This means the lowest point in the view has crossed into negative y values for the view controller's reference point, which is above the visible area of the view controller.)
This worked okay, except the x value I gave to lowestPoint really messes everything up. If I choose the minimum X, that is the furthest to the left, it will only tell me when the bottom left corner of the UIView has gone off screen. Often times as the view can be rotating depending on where the user pushes from, the bottom right may go off screen after the left, making it detect it too early. If I choose the middle X, it will only tell me when the middle bottom has gone off, etc. I can't seem to figure out how to tell it "just get me the absolute lowest y value.
2. I tried CGRectIntersectsRect as shown in the code above, and it never says it's outside, even seconds after it went shooting outside of any visible area.
What am I doing wrong? How should I be detecting it no longer being visible?
If you take a look on UIDynamicItem protocol properties, you can see they are center, bounds and transform. So UIDynamicAnimator actually modifies only these three properties. I'm not really sure what happens with the frame during the Dynamics animations, but from my experience I can tell it's value inside the action block is not always reliable. Maybe it's because the frame is actually being calculated by CALayer based on center, transform and bounds, as described in this excellent blog post.
But you for sure can make use of center and bounds in the action block. The following code worked for me in a case similar to yours:
CGPoint parentCenter = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
self.pushBehavior.action = ^{
CGFloat dx = self.imageView.center.x - parentCenter.x;
CGFloat dy = self.imageView.center.y - parentCenter.y;
CGFloat distance = sqrtf(dx * dx + dy * dy);
if(distance > MIN(parentCenter.y + CGRectGetHeight(self.imageView.bounds), parentCenter.x + CGRectGetWidth(self.imageView.bounds))) {
NSLog(#"Off screen!");
}
};
I've added a UIPanGestureRecognizer to a cell, allowing the user to slide it horizontally. When the cell x origin becomes more than 50, I want the cell to move more slowly, like happens with UIScrollView limits, for example, but I'm having a problem: the cell doesn't follow my finger as it should.
I slide my finger from left to right on the screen multiple times, and when I finish the cell's in a different position from the starting position.
This is the code I'm using to move the cell (my guess is there's something wrong with the way I'm creating the elastic effect when it reaches 50):
- (void)slideCell:(UIPanGestureRecognizer *)panGestureRecognizer {
CGFloat horizontalTranslation = [panGestureRecognizer translationInView:self].x;
if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
CGFloat retention = 1;
if (self.frame.origin.x > 50) retention = 5;
[self setCenter:CGPointMake(self.center.x+horizontalTranslation/retention, self.center.y)];
}
[panGestureRecognizer setTranslation:CGPointZero inView:self];
}
The problem is that you're resetting the translation to zero all the time so you lose the knowledge about the absolute distance that the users touch has moved. If you change your algorithm so that the frame is set based on the translation without resetting (the absolute translation) then your calculation will be accurate when you drag back.
Experiment with different algorithms, try something like:
CGFloat tx = [panGestureRecognizer translationInView:self].x;
[self setCenter:CGPointMake(originalCenter.x + (tx - (tx > 50 ? sqrt(tx - 50) : 0)), originalCenter.y)];
If the deceleration is too fast, try using powf so you can specify the factor.
I'm developing an iPhone app where the main views are presented the user on the surface of a cube. Users switch views by rotating the cube with a pan gesture.
To achieve this I am using the GKLCubeController class from this GitHub project.
In terms of adding views to a cube and rotating, it works fine. However the angular rotation of the cube doesn't map correctly to the current x position of the finger as it pans across the screen.
The problem is that the cube rotation lags behind the finger movement by about ½ second making the cube feel ‘heavy’ as illustrated in this short screencast.
The code handling the rotation is shown below:
-(void)panHandler:(UIPanGestureRecognizer*)panner{
CGPoint translatedPoint = [panner translationInView:self.view.window];
CGFloat halfWidth = self.view.bounds.size.width / 2.0;
// save our starting points
if([panner state] == UIGestureRecognizerStateBegan) {
startingX = translatedPoint.x;
if (!transformLayer) {
transformLayer = [[CATransformLayer alloc] init];
transformLayer.frame = self.view.layer.bounds;
for (UIView *viewToTranslate in views) {
[viewToTranslate removeFromSuperview];
[transformLayer addSublayer:viewToTranslate.layer];
}
// add in this new layer
[self.view.layer addSublayer:transformLayer];
}
} else if([panner state] == UIGestureRecognizerStateEnded) {
...
} else {
// instantly adjust our transformation layer
CATransform3D transform = CATransform3DIdentity;
transform.m34 = kPerspective;
double percentageOfWidth = (translatedPoint.x - startingX) / self.view.frame.size.width;
transform = CATransform3DTranslate(transform, 0, 0, -halfWidth);
double adjustmentAngle = percentageOfWidth * M_PI_2 + startingAngle;
transform = CATransform3DRotate(transform, adjustmentAngle, 0, 1, 0);
transform = CATransform3DTranslate(transform, 0, 0, halfWidth);
transformLayer.transform = transform;
finishingAngle = adjustmentAngle;
}
}
I've a suspicion the problem is something to do with the conversion of the CGPoint.x returned by UIPanGestureRecognizer translationInView: to a rotation angle. Can anyone confirm whether this is the case, and suggest what the correct maths should be for mapping the touch position x to the rotation of a cube such that the cube edge tracks the finger motion as it pans across the screen?
There are two issues here:
The major performance issue here is the way this class is performing the transform of the sides of the cube. It's giving each side of the cube a complicated transform, and then as you're dragging the cube around, it's taken the relevant sides of the cube, added them to a CATransformLayer, and performing a complicated transform upon that layer (thus, when you look at the individual sides of the cube, you're doing a transform of a transform).
I pulled out that CATransformLayer logic, and updated the transform for the individual sides, and it was dramatically more responsive.
By the way, you may might want to still employ something like this CATransformLayer logic when you animate the letting go of the rotated cube, as that's an excellent way of synchronizing the animation of the individual sides of the cube (otherwise you get some separation in the sides of the cube during the animation). But while dragging, there's too much of a performance hit.
As you continue to refine this, there are possibly other optimizations that can be done, but my testing suggests that getting rid of a transformation on a complicated transformation made a huge impact on performance.
And, by the way, make sure to test this on a device, not the simulator, as the simulator's graphics performance is very different than that of the device.
A minor factor that might contribute a slight initial delay in responsiveness may be the inherent delay in UIPanGestureRecognizer (which looks for a certain amount of movement before recognizing the gesture as a pan, so that other gestures such as taps and the like can trigger if appropriate). It's a modest delay and a very small part of your performance problem, but for the quickest of response times, you might not want to use the UIPanGestureRecognizer. Either subclass your own, or use a UILongPressGestureRecognizer with a minimumPressDuration of 0.0, and you can get instantaneous response to the gesture.
You'll see this respond more quickly to movement (but it's also a gesture that doesn't play well with others, that if you have tap gestures or the like inside the view, they won't be triggered).