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.
Related
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.
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!");
}
};
Update: Though I'd still like to solve this, I ended up switching to animateWithDuration:delay:options:animations:completion: and it works much nicer. It's missing that nice "bounce" at the end that the spring affords, but at least it's controllable.
I am trying to create a nice gesture-driven UI for iOS but am running into some difficulties getting the values to result in a nice natural feeling app.
I am using animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion: because I like the bouncy spring animation. I am initializing the velocity argument with the velocity as given by the gesture recognizer in the completed state. The problem is if I pan quickly enough and let go, the velocity is in the thousands, and my view ends up flying right off the screen and then bouncing back and forth with such dizzying vengeance.
I'm even adjusting the duration of the animation relative to the amount of distance the view needs to move, so that if there are only a few pixels needed, the animation will take less time. That, however, didn't solve the issue. It still ends up going nuts.
What I want to happen is the view should start out at whatever velocity the user is dragging it at, but it should quickly decelerate when reaching the target point and only bounce a little bit at the end (as it does if the velocity is something reasonable).
I wonder if I am using this method or the values correctly. Here is some code to show what I'm doing. Any help would be appreciated!
- (void)handlePanGesture:(UIPanGestureRecognizer*)gesture {
CGPoint offset = [gesture translationInView:self.view];
CGPoint velocity = [gesture velocityInView:self.view];
NSLog(#"pan gesture state: %d, offset: %f velocity: %f", gesture.state, offset.x, velocity.x);
static CGFloat initialX = 0;
switch ( gesture.state ) {
case UIGestureRecognizerStateBegan: {
initialX = self.blurView.x;
break; }
case UIGestureRecognizerStateChanged: {
self.blurView.x = initialX + offset.x;
break; }
default:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded: {
if ( velocity.x > 0 )
[self openMenuWithVelocity:velocity.x];
else
[self closeMenuWithVelocity:velocity.x];
break; }
}
}
- (void)openMenuWithVelocity:(CGFloat)velocity {
if ( velocity < 0 )
velocity = 1.5f;
CGFloat distance = -40 - self.blurView.x;
CGFloat distanceRatio = distance / 260;
NSLog(#"distance: %f ratio: %f", distance, distanceRatio);
[UIView animateWithDuration:(0.9f * distanceRatio) delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:velocity options:UIViewAnimationOptionBeginFromCurrentState animations:^{
self.blurView.x = -40;
} completion:^(BOOL finished) {
self.isMenuOpen = YES;
}];
}
Came across this post while looking for a solution to a related issue. The problem is, you're passing in the velocity from UIPanGestureRecognizer, which is in points/second, when - animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion wants… a slightly odder value:
The initial spring velocity. For smooth start to the animation, match this value to the view’s velocity as it was prior to attachment.
A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.
The animation method wants velocity in "distances" per second, not points per second. So, the value you should be passing in is (velocity from the gesture recognizer) / (total distance traveled during the animation).
That said, that's exactly what I'm doing and there's still a slight, yet noticeable "hiccup" between when the gesture recognizer is moving it, and when the animation picks up. That said, it should still work a lot better than what you had before. 😃
First you need to calculate the remaining distance that the animation will have to take care of. When you have the delta distance you can proceed to calculate the velocity like this:
CGFloat springVelocity = fabs(gestureRecognizerVelocity / distanceToAnimate);
For clean velocity transfer you must use UIViewAnimationOptionCurveLinear.
I had a slightly different need, but my code may help, namely the velocity calculation, based on (pan velocity / pan translation).
A bit of context: I needed to use a panGestureRecognizer on the side of a UIView to resize it.
If the iPad is in portrait mode, the view is attached on the left, bottom
and right sides, and I drag on the top border of the view to resize
it.
If the iPad is in landscape mode, the view is attached to the
left, top and bottom sides, and I drag on the right border to resize it.
This is what I used in the IBAction for the UIPanGestureRecognizer:
var velocity: CGFloat = 1
switch gesture.state {
case .changed:
// Adjust the resizableView size according to the gesture translation
let translation = gesture.translation(in: resizableView)
let panVelocity = gesture.velocity(in: resizableView)
if isPortrait { // defined previously in the class based on UIDevice orientation
let newHeight = resizableViewHeightConstraint.constant + (-translation.y)
resizableViewHeightConstraint.constant = newHeight
// UIView animation initialSpringVelocity requires a velocity based on the total distance traveled during the animation
velocity = -panVelocity.y / -panTranslation.y
} else { // Landscape
let newWidth = resizableViewWidthConstraint.constant + (translation.x)
// Limit the resizing to half the width on the left and the full width on the right
resizableViewWidthConstraint.constant = min(max(resizableViewInitialSize, newWidth), self.view.bounds.width)
// UIView animation initialSpringVelocity requires a velocity based on the total distance traveled during the animation
velocity = panVelocity.x / panTranslation.x
}
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: velocity,
options: [.curveEaseInOut],
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
// Reset translation
gesture.setTranslation(CGPoint.zero, in: resizableView)
}
Hope that helps.
in my app I have a UIImageView representing a chalk. The user can pick that up and drag it over the screen to draw with it.
I implemented that using touchesBegan, touchesMoved and touchesEnded. In touchesMoved I move the center of my UIImageView to the current touch location and draw a line with core graphics from the last to the current location.
This works well but the image view movement isn't very smooth and it also lags behind the touch. I already tried to use a UIPanGestureRecognizer (didn't recognize the gesture) and a UIScrollView in which I placed the chalk (didn't really figure out how to configure it so that it could be moved far enough in all directions).
Can you give me some hints how to improve the quality of my chalk movement?
Thanks!
The following tapGesture method method works smooth for me:
- (void)handlePan:(UIPanGestureRecognizer*)gesture{
View* view = (View*)gesture.view;
if (gesture.state == UIGestureRecognizerStateEnded){
view.dragStartingPoint = CGPointZero;
return;
}
CGPoint point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan){
view.dragStartingPoint = point;
view.dragStartingFrame = view.frame;
return;
}
if (CGPointEqualToPoint(view.dragStartingPoint, CGPointZero))
return;
CGFloat x = view.dragVerticallyOnly? view.frame.origin.x: view.dragStartingFrame.origin.x + point.x - view.dragStartingPoint.x;
CGFloat y = view.dragHorizontallyOnly? view.frame.origin.y: view.dragStartingFrame.origin.y + point.y - view.dragStartingPoint.y;
if (self.dragVerticallyOnly == NO){
if (x < view.dragMinPoint.x){
x = view.dragMinPoint.x;
}
else if (x > self.dragMaxPoint.x){
x = view.dragMaxPoint.x;
}
}
if (self.dragHorizontallyOnly == NO){
if (y < view.dragMinPoint.y){
y = view.dragMinPoint.y;
}
else if (y > self.dragMinPoint.y){
y = view.dragMinPoint.y;
}
}
CGFloat deltaX = x - view.frame.origin.x;
CGFloat deltaY = y - view.frame.origin.y;
if ((fabs(deltaX) <= 1.0 && fabs(deltaY) <= 1.0))
return; // Ignore very small movements
[UIView animateWithDuration:0.1 animations:^{
view.frame = CGRectMake(x, y, view.frame.size.width, view.frame.size.height);
}];
}
The most important points are:
Make the movement with the animated option; (one tenth of a second seems to do the job well done).
Avoid making movements when not necessary. Making some calculations will not make it slower, but making unnecessary movements might. However you cannot avoid to many movements otherwise it will not follow the user pan gesture.
Depending on how you want your view to behave, there can be more optimisations that you may want to make.
Remark: My method also takes into account boundaries and movement directions limits that you may define in case you need it. For instance, in my case I added a speed limit. If the user goes beyond a certain pan speed I just ignore further pan gestures and move the view (animated) where the user was pointing to, and at that speed. It may or not make sense in same cases. In mine, it did!
I hope I have been of some help!
look at this component : spuserresizableview
http://cocoacontrols.com/platforms/ios/controls/spuserresizableview
The source code is very simple and can help you to understand the view handling.
Try reducing the size of the Chalk image u are using....
Edited: By size i meant the image file size.
It helped me when i faced similar issue.
I rewrote how the movement is calculated and it is a little smoother now. Not perfect, but it's enough.