Implement custom zooming for a UIScrollView - iphone

I have a UIScrollView with the requirement that, when zooming, the contentSize.height should remain the same. Zooming in from 200x100 should result in a new contentSize of 400x100 instead of 400x200, for instance. I'd like to do my own drawing while the user is zooming.
I don't think I can use the normal zooming behaviour of UIScrollView to achieve this, so I'm trying to roll my own. (I could just let it do its thing and then redraw my contents when -scrollViewDidEndZooming:withView:atScale: gets called, but that wouldn't be very pretty).
Currently I am subclassing UIScrollView and trying to do my own zooming when two fingers are on the screen:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([touches count] != 2) {
[super touchesMoved:touches withEvent:event];
} else {
// do my own stuff
}
}
I thought that by overriding touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent: and touchesCancelled:withEvent: in this way should work, but it doesn't.
An earlier failed attempt was to place a transparent view on top of the scrollview and send touches that I'm not interested in to the scrollview :
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([touches count] != 2) {
[self.theScrollView touchesMoved:touches withEvent:event];
} else {
// do my own stuff
}
}
Any help would be appreciated.
Thanks,
Thomas

You probably won't be able to maintain decent performance during zooming if you attempt to redraw your content on every frame of a pinch-zooming event. I'd recommend taking the approach of letting the UIScrollView zoom a scaled version of your drawing in or out, then redraw the content to be sharp at the end of the zoom in the -scrollViewDidEndZooming:withView:atScale: delegate method. This is what I do in my application, and it ends up working very well.
There are some tricks to resizing your content view properly at the end of a zoom, which I describe in this answer. Basically, you need to intercept the setting of a transform to your content view so that you can set it to a scale factor of 1 when you redraw your content. You'll also need to keep track of that scale factor, because the UIScrollView doesn't, and use that scale factor to adjust the transform that UIScrollView tries to apply to your content view with subsequent zoom operations.
You could use a modification of this technique to force a redraw of your content during the pinch-zooming, but in my tests this ended up being far too jerky to provide a good user experience.

I'm not sure what you mean by this:
I thought that by overriding
touchesBegan:withEvent:,
touchesMoved:withEvent:,
touchesEnded:withEvent: and
touchesCancelled:withEvent: in this
way should work, but it doesn't.
Do you not get the events? You should receive the events, but I think there is a logic error in your if statement that may have been preventing this from working.
if ([touches count] != 2)
This is a problem, because the likelihood of the two touches happening precisely the same time is low. You'll want to be able to handle when touches happen independently, as well as when a user holds a finger stationary, and moves the other one. In these scenarios (which are common) you may only get one touch in that NSSet, even though the other is still valid.
The solution to handling touches properly is to remember some things about which touches came in and which touches left. Remember, the address of the UITouch does not change for the life of the touch, so you can safely compare addresses to ensure you are still dealing with the same touch as before, and track it's lifecycle.
If you are not getting the touch events, then that is a different problem altogether, you may need to turn set multiTouchEnabled:YES

I'm trying to do the same thing as this and I really want to be able to redraw while it's zooming. Fixing it up at the end in scrollViewDidEndZooming:withView:atScale is not really good enough.
The way I do it is pass a dummy view in viewForZoomingInScrollView: and read the height of this dummy view and set the frame on the actual content view to whatever I want. Because the frame changes, it means that drawRect gets called everytime. It seems fine on the simulator, I'm just drawing a few lines. But I don't actually own a device though, so I can't test it properly.
Also in the code you've got, you have touchesBegan:withEvent: but then you are forwarding to super touchesMoved:withEvent: instead of touchesBegan:withEvent:

Related

How can I prevent clipped portions of subviews from still taking touch events?

I'm currently clipping some UIViews within a parent view like in the illustration below:
The problem is that the clipped portions (the invisible portions) of the subviews are still receiving touch events that, intuitively, should be going to the other views visible there.
Is there something else I should be doing to achieve this behavior, or is this not actually an easy thing to do?
This was happening because the parent view in this case had a custom implementation of
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event,
and it wasn't performing this bounds test (which I now assume is part of the default implementation).
Adding:
if ([self pointInside:point withEvent:event]) {
....
}
around the code in that implementation solved the problem.

Animation inside a UIScrollView

I want to fade-out a view as it is scrolling inside a parent UIScrollview. When the fade-out animation begins, the scroll view stops scrolling. It jumps to the correct position when the fade is complete.
My fade-out is achieved with animateWithDuration and block objects, triggered upon a page-change I detect in scrollViewWillBeginDragging.
Does anyone know how to make them both happen simultaneously? Just to be clear, I am not 'animating' the UIScrollView scrolling - rather it is happening via user interaction of swiping.
EDIT:
Here is the code I'm using to fade the UIView. This code is in a UIViewController derived class, which is the delegate for a UIScrollView. When the user starts dragging his finger, I want to fade out the subView. But when the user starts draggin a finger, the subview fades and the scrolling stops. After the subView has completely faded out, the the scroll view will then snap to the location where the user's finger is.
-(void)scrollViewWillBeginDragging:(UIScrollView*)scrollView
{
[UIView animateWithDuration:0.5
animations:^
{
self.subView.alpha = 0.0f;
}
completion:^(BOOL finished) { }];
}
A little late, but if you want to keep using blocks, you can use:
animateWithDuration:delay:options:animation:complete:
add "UIViewAnimationOptionAllowUserInteraction" to options to allow interaction while scrolling.
I'm sure that you will still have the lag problem. Here's the best way I can explain it. Please forgive me in advance since I'm probably using the wrong terms. All animations must run on the main thread. When you call an animation, iOS first *P*rocesses then it *R*enders before it generates *F*rames. It looks like this.
PPPPRRRRFFFFFFFFFFFFFFFFFF
But since ScrollViews don't know how long your animation is going to be or when it will end, it has to perform the animation like this.
PRFPRFPRFPRFPRFPRFPRFPRF
My theory is that the lag you are experiencing has to do with these two calls colliding on the main thread at the same time. I'm not sure how you would solve this problem other than with a faster chip. I've that you could push one animation to the CPU and one to the GPU, but I'm not that advanced at programming yet.
very interesting ... I've checked this out, and yes, i have the same effect ... Well, it seems that the animateWithDuration somehow blocks the main thread ... which is not logical, and the documentation doesn't say anything about it either ..
However there is an easy workaround, something similar to this: (i've set the animation duration to 3 so i can see that it's working while i'm moving my scroll view :) ...)
[UIView beginAnimations:#"FadeAnimations" context:nil];
[UIView setAnimationDuration:3];
self.subview.alpha = 0.0f;
[UIView commitAnimations];
I would suggest, since the opacity is based on the user's finger's movements in the UIScrollView, using the delegate method scrollViewDidScroll:. The scrollView passed as a parameter can be used to check the contentOffset which is simply a CGPoint indicating how far into the content view of the UIScrollView the user has scrolled. Something like this can be used to relate the scroll position to the opacity of a given view in a paginated UIScrollView:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// The case where I used this, the x-coordinate was relevant. You may be concerned with the y-coordinate--I'm not sure
CGFloat percent = ((int)(scrollView.contentOffset.x) % (int)(scrollView.frame.size.width)) / scrollView.frame.size.width;
if (percent > 0.0 && percent < 1.0) { // Of course, you can specify your own range of alpha values
relevantView.alpha = percent; // You could also create a mathematical function that maps contentOffset to opacity in a different way than this
}
}
According to information that is still not supposed to be widely released, all iOS 4.x versions completely block user interaction while the animation is in progress.
Isn't it interesting, though, that you're UITouches are obviously still registered during the animation? Hmm... maybe that HINTS that something NEW is coming in a yet-to-be-released version!
I.e., If you can, read the iOS 5 Beta documentation on UIView class methods.

touchesBegan stops working when view is moved outside its superview

I'm using [aSubview touchesBegan] to move aSubview's position around on a screen in relation to its superview. Its superview is not much larger than the subview itself. This is quite straightforward to do as the following snippet shows:
UITouch* touch = [touches anyObject];
CGPoint touchPoint = [touch locationInView:[self superview]];
self.center = touchPoint;
However once a aSubview is moved, as soon as any portion of it falls outside the bounds of its superview, touches in that section no longer register. In other words, touchesBegan no longer fire. I want touches in aSubview to register no matter where it's moved in relation to its superview.
Any thoughts?
Howard
mcpunky's answer is almost good, except you can NOT make pointInside function to always return YES. This way the view will intercept all touches.
Instead, one needs to do more fine check:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return CGRectContainsPoint(self.subviewOutsideMe.frame, point) || CGRectContainsPoint(self.bounds, point);
}
I just had this problem with subviews not receiving input because the superview was simply not sending touch events for subviews that are outside it's bounds. Also, it was crucial to keep the bounds of the superview as they were and moving subviews up the hierarchy also wasn't feasible. What did the job for me was overriding superview's
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
to always return YES. Result: pretty much nothing changed in superview, nor the subviews, but input is received outside superview's boundaries.
I've talked to an Apple engineer about this. touchesBegan won't work in the portion of a subview that's not contained w/in its superview because the system clips each subview on the way down the hierarchy as it tries to determine which subview's touchesBegan gets called.
In order to resolve the issue, I removed the intermediate wrapper views that were causing the clipping problem and hoisted the subviews up one level. This necessitated a minor change in logic but ultimately proved to be a cleaner solution -- and more importantly, one that worked.
I'm unsure if this is the correct way, but it's certainly one way.
Listen to the touchesBegan even in your parent object.
If you get an event, pass it onto the childs view by calling its touchesBegan yourself.

Iphone multitouch handling

I'm writing an iPhone app, and I want to handle multitouches. I'm using cocos2d libs. So I've made a CCLayer subclass and set it to be a CCStandartTouchDelegate. For some reason I don't want to use UIGestureRecognizer and to build a correct logic I should know the answers for these questions:
If I tap the screen with one finger, and then with the other one. How many touches will be caught in ccTouchesBegan?
If I tap the screen with two fingers and then will move only one of them. How many touches will be caught in ccTouchesMoved?
The best thing to do when you have a question like this is just to implement the callbacks, and in the implementation, log the parameters. For example:
- (BOOL)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Log everything (there will be repetition because the event contains the set of touches):
NSLog(#"ccTouchesBegan: touches = %#; event = %#", touches, event);
// Or, just log the number of touches to simplify the output:
NSLog(#"ccTouchesBegan: %d touches", [touches count]);
return kEventHandled;
}
Then just run your app and experiment, watching the log. You'll learn more this way (and faster) than you will by asking here.
But to answer your specific questions:
You should get one call to ccTouchesBegan for each tap (even if the first finger is still down when the second tap occurs). If the two fingers hit simultaneously, you'll get one call with two touches.
You'll get repeated calls to ccTouchesMoved each time one or more of the fingers moves. If only one finger is moving, each call will be passed a single touch. Stationary fingers will not generate events until they are moved or lifted.
Of course, remember to set isTouchEnabled = YES for your CCLayer or you won't get any callbacks at all.

Not sure how to use ccTouchesBegan to do what I want it to do

So when I see ccTouchesBegan (or touchesBegan for that fact of the matter) I see something like this usually:
- (void)ccTouchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
UITouch* touch = [touches anyObject];
}
But, what I am not getting is how do you just detect if one object has been touched? For example how would I check if a specific CCSprite I declared has been touched?
Sorry if this is a newbish question but, I just don't understand and if you need more clarification just ask how I can clarify myself more.
I'm not familiar with cocoas2d but in the standard API it sends the touches first to the view touched and then up the view responder chain to a view that has a controller. If that controller does not handle the touch then it goes up to the next view until it ends up at the Window object.
See Responder Objects in the Responder Chain
The best place to trap touches for specific objects is in the object themselves. In the case of sprite-like view, the sprite itself most likely needs to respond to the touch e.g. by moving itself. If you need the touch to be communicated to another object, you should use the delegate pattern so that the sprite can tell its delegate how its been touched.
That last sentence sounded weird.
I don't have the samples in front of me but there should be an example in the Cocos2D download package which demonstrates a touch event and how it propagates down to sprites.