UIScrollview subviews outside of clipping bounds not receiving touches - iphone

I have a UIScrollView which I have set up to swipe one column at a time (two columns per page) - by setting the frame to half of the views actual width, setting clipToBounds to NO and use hitTest to declare the area outside of the frame as belonging to the UIScrollView (see example below).
This works great, but my problem now is that the subviews of the UIScrollView don't get any touch events - only the main UIScrollView does.
In the following example, if the hitTest code is included, then the scrollview scrolls correctly, paging one column at a time and all its content may be seen - but the inner scrollviews do not receive touch events.
If I remove the hitTest code, then only the first child scrollview receives touches, and all its content may be seen - but the main scrollview wont get touches in the non-clipped area.
How can I solve this?
Example:
//=========================================
// UIScrollViewEx
// Just in order to log touches...
//=========================================
#interface UIScrollViewEx : UIScrollView {}
#end
#implementation UIScrollViewEx
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"Touches Began (0x%08X)", (unsigned int)self);
}
#end
//=========================================
// UIViewEx
// Dummy class - sets subview as hit target
// just to demonstrate usage of non-clipped
// content
//=========================================
#interface UIViewEx : UIView {}
#end
#implementation UIViewEx
- (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
return [self.subviews objectAtIndex:0];
}
return nil;
}
#end
//=========================================
// MainClass
// Any UIViewEx based class which returns
// the UIScrollView child on hittest
//=========================================
#implementation MyClass
- (UIColor*) randomColor
{
int r = arc4random() % 100;
int g = arc4random() % 100;
int b = arc4random() % 100;
return [UIColor colorWithRed:(0.01 * r) green:(0.01 * g) blue:(0.01 * b) alpha:1.0];
}
- (void) loadScrollviews
{
// Set frame to half of actual width so that paging will swipe half a page only
CGRect frame = CGRectMake(0, 0, self.bounds.size.width / 2, 400);
// Main scrollview
UIScrollView *scrollview = [[UIScrollView alloc] initWithFrame:frame];
[scrollview setBackgroundColor:[UIColor greenColor]];
[scrollview setPagingEnabled:YES];
[scrollview setClipsToBounds:NO];
// Create smaller scrollviews inside it - each one half a screen wide
const int numItems = 5;
for(int i = 0; i < numItems; ++i) {
frame.origin.x = frame.size.width * i;
UIScrollView *innerScrollview = [[UIScrollViewEx alloc] initWithFrame:frame];
[innerScrollview setContentSize:CGSizeMake(frame.size.width, 1000)];
[innerScrollview setBackgroundColor:[self randomColor]];
[scrollview addSubview:innerScrollview];
[innerScrollview release];
}
[scrollview setContentSize:CGSizeMake(numItems * frame.size.width, frame.size.height)];
[self addSubview:scrollview];
}
#end
Update
I get the touches forwarded to the inner view by doing the following, but surely there must be a better way?
- (UIView *) hitTest: (CGPoint) pt withEvent: (UIEvent *) event
{
if(CGRectContainsPoint(self.bounds, pt))
{
UIScrollView *scrollview = [self.subviews objectAtIndex:0];
CGPoint scrollViewpoint = [scrollview convertPoint:pt fromView:self];
for(UIView *view in scrollview.subviews) {
if(CGRectContainsPoint(view.frame, scrollViewpoint)) {
return view;
}
}
return scrollview;
}
else {
return [super hitTest:pt withEvent:event];
}
}

This may work:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
UIView* child = nil;
if ((child = [super hitTest:point withEvent:event]) == self)
return self.scrollView;
return child;
}
But if subview is out of scrollview bounds events were not fired and this function returns self.scrollView.

Related

How to pass the touches to view from scrollview

I have 3 tableviews placed upon scrollview at x = 0.0, x = 320.0 and x = 640.0.
When the user horizontally swipes the tableviews (as they are on top), I want to pass the swipe event to its superview and when the user vertically swipes tableviews the tableview must scroll vertically.
How can I achieve this?
To pass the touch from UIScrollView, use this code as a category:
#implementation UIScrollView (FixedApi)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject];
NSLog(#"touch view = %#", [[touch view].class description]);
if ([[[touch view].class description] isEqualToString:#"UITableViewCellContentView"]) {
//To pass the touch to UITableViewCell
} else if ([[[touch view].class description] isEqualToString:#"UITableView"] && isHorizntalSCroll == true && pageNumber == 2) {
//To pass the touch to UITableView
} else if ([[[touch view].class description] isEqualToString:#"UIView"]) {
//To pass the touch to UIView
} else {
[self.superview touchesBegan:touches withEvent:event]; // or 1 nextResponder, depends
[super touchesBegan:touches withEvent:event];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if ( !self.dragging ) [self.nextResponder.nextResponder touchesEnded:touches withEvent:event];
[super touchesEnded:touches withEvent:event];
}
#end
To determine the vertical/Horizontal scrolling, you can use this code:
.h file <UIScrollViewDelegate>
BOOL pageControlIsChangingPage;
CGPoint startPos;
int scrollDirection;
int CX; //The width of the pages.
BOOL isHorizntalSCroll;
int pageNumber;
.m file
#pragma mark -
#pragma mark UIScrollViewDelegate stuff
- (void)scrollViewDidScroll:(UIScrollView *)_scrollView {
if (scrollDirection==0){//we need to determine direction
//use the difference between positions to determine the direction.
if (abs(startPos.x-scrollView.contentOffset.x)<abs(startPos.y-scrollView.contentOffset.y)){
NSLog(#"Vertical Scrolling");
scrollDirection=1;
isHorizntalSCroll = false;
[scrollView setPagingEnabled:NO];
} else {
NSLog(#"Horitonzal Scrolling");
scrollDirection=2;
isHorizntalSCroll = ture;
[scrollView setPagingEnabled:YES];
}
}
if (scrollDirection==1) {
[scrollView setContentOffset:CGPointMake(startPos.x,scrollView.contentOffset.y) animated:NO];
} else if (scrollDirection==2){
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x,startPos.y) animated:NO];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)_scrollView {
if (pageControlIsChangingPage) {
return;
}
CGFloat pageWidth = _scrollView.frame.size.width;
int page = floor((_scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
pageControl.currentPage = page;
pageNumber = page;
NSLog(#"page number = %d",page);
if (page == 3 || page == 0) {
[scrollView setContentSize:CGSizeMake(CX, personalInfo.frame.size.height + 100)];
} else {
[scrollView setContentSize:CGSizeMake(CX, [scrollView bounds].size.height)];
}
pageControlIsChangingPage = NO;
}
#pragma mark -
#pragma mark PageControl stuff
- (IBAction)changePage:(id)sender {
/*
* Change the scroll view
*/
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * pageControl.currentPage;
frame.origin.y = 0;
[scrollView scrollRectToVisible:frame animated:YES];
/*
* When the animated scrolling finishings, scrollViewDidEndDecelerating will turn this off
*/
pageControlIsChangingPage = YES;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollViews{
startPos = scrollView.contentOffset;
scrollDirection=0;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollViews willDecelerate:(BOOL)decelerate {
if (decelerate) {
scrollDirection=3;
}
}
That's it,
disable horizontal scroling for each tableview via IB or code and set the contentsize of the base scroll view to be 960,heightOfTableview and you should be just fine... this should get you going.

Can't set image in UIScrollView after zooming

I use ImageScrollView to show image. At the begining I show only small image. And when user tries to zoom, I load big image. First I wanted to do this using this code:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollViewCalled
{
ImageScrollView *scroll = (ImageScrollView *)scrollViewCalled;
UIImageView *imageView = (UIImageView *)[scroll imageView];
return imageView;
}
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollViewCalled withView:(UIView *)view
{
ImageScrollView *scroll = (ImageScrollView *)scrollViewCalled;
if (scroll != scrollViewCalled || scroll.isImageBig)
{
return;
}
scroll.userInteractionEnabled = NO;
[self setBigImageInBackgroundForIndex:nil];
}
But it works strange: when I try to zoom, the big image is shown, but I see its maximum zoom scale, and I can't scroll to see the rest part of the image. But when I zoom one more time instead of scrolling the image, it begins to work properly and I finally can scroll the image. But I should zoom 2 times for that.
Then I tried scrollViewDidEndZooming instead scrollViewWillBeginZooming:
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollViewCalled withView:(UIView *)view atScale:(float)scale
{
ImageScrollView *scroll = (ImageScrollView *)scrollViewCalled;
if (scroll != scrollViewCalled || scroll.isImageBig)
{
return;
}
scroll.userInteractionEnabled = NO;
[self setBigImageInBackgroundForIndex:nil];
}
And everything became perfect except one thing: it's too late. I zoom small image, I see it's zoomed variant, then big image loaded and I see minimum zoom for big image. For user it seems , that image came back to it's previus zoom. And only if I try to zoom one more time, I can zoom perfectly.
Thank you in advance!
here is the code for ImageScrollView:
ImageScrollView.h
#import <UIKit/UIKit.h>
#interface ImageScrollView : UIScrollView <UIScrollViewDelegate> {
UIView *imageView;
NSUInteger index;
}
#property (assign) NSUInteger index;
#property (retain, nonatomic) UIView *imageView;
#property (assign, nonatomic) BOOL isImageBig;
#property (assign, nonatomic) BOOL hasImage;
- (void)displayImage:(UIImage *)image;
- (void)displayTiledImageNamed:(NSString *)imageName size:(CGSize)imageSize;
- (void)setMaxMinZoomScalesForCurrentBounds;
- (CGPoint)pointToCenterAfterRotation;
- (CGFloat)scaleToRestoreAfterRotation;
- (void)restoreCenterPoint:(CGPoint)oldCenter scale:(CGFloat)oldScale;
#end
ImageScrollView.m
#import "ImageScrollView.h"
#import "TilingView.h"
#implementation ImageScrollView
#synthesize index;
#synthesize imageView;
#synthesize isImageBig, hasImage;
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.showsVerticalScrollIndicator = NO;
self.showsHorizontalScrollIndicator = NO;
self.bouncesZoom = YES;
self.decelerationRate = UIScrollViewDecelerationRateFast;
self.delegate = self;
}
return self;
}
- (void)dealloc
{
[imageView release];
[super dealloc];
}
#pragma mark -
#pragma mark Override layoutSubviews to center content
- (void)layoutSubviews
{
[super layoutSubviews];
// center the image as it becomes smaller than the size of the screen
CGSize boundsSize = self.bounds.size;
CGRect frameToCenter = imageView.frame;
// center horizontally
if (frameToCenter.size.width < boundsSize.width)
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
else
frameToCenter.origin.x = 0;
// center vertically
if (frameToCenter.size.height < boundsSize.height)
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
else
frameToCenter.origin.y = 0;
imageView.frame = frameToCenter;
if ([imageView isKindOfClass:[TilingView class]]) {
// to handle the interaction between CATiledLayer and high resolution screens, we need to manually set the
// tiling view's contentScaleFactor to 1.0. (If we omitted this, it would be 2.0 on high resolution screens,
// which would cause the CATiledLayer to ask us for tiles of the wrong scales.)
imageView.contentScaleFactor = 1.0;
}
}
#pragma mark -
#pragma mark UIScrollView delegate methods
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return imageView;
}
#pragma mark -
#pragma mark Configure scrollView to display new image (tiled or not)
- (void)displayImage:(UIImage *)image
{
// clear the previous imageView
[imageView removeFromSuperview];
[imageView release];
imageView = nil;
// reset our zoomScale to 1.0 before doing any further calculations
self.zoomScale = 1.0;
self.imageView = [[[UIImageView alloc] initWithImage:image] autorelease];
[self addSubview:imageView];
self.contentSize = [image size];
[self setMaxMinZoomScalesForCurrentBounds];
self.zoomScale = self.minimumZoomScale;
// self.zoomScale = self.maximumZoomScale;
}
- (void)setMaxMinZoomScalesForCurrentBounds
{
CGSize boundsSize = self.bounds.size;
CGSize imageSize = imageView.bounds.size;
// calculate min/max zoomscale
CGFloat xScale = boundsSize.width / imageSize.width; // the scale needed to perfectly fit the image width-wise
CGFloat yScale = boundsSize.height / imageSize.height; // the scale needed to perfectly fit the image height-wise
CGFloat minScale = MIN(xScale, yScale); // use minimum of these to allow the image to become fully visible
// on high resolution screens we have double the pixel density, so we will be seeing every pixel if we limit the
// maximum zoom scale to 0.5.
CGFloat maxScale = 1.0 / [[UIScreen mainScreen] scale];
// CGFloat maxScale = 4.0 / [[UIScreen mainScreen] scale];
// don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.)
if (minScale > maxScale) {
minScale = maxScale;
}
self.maximumZoomScale = maxScale;
self.minimumZoomScale = minScale;
}
#end
Add this method:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return imageView;
}
You need not do that much in fact. Create a ScrollView. Create imageview with bigger image. Set imageview's frame and scrollview's frame similar. Set the contentsize of scrollview with the images' width and height. Now, from scrollview's delegate method, do following:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return imageView;
}

Custom UIImageVIew with touchesEnded works only with the first view

Sorry for bad title :(
I've a controller that has a scrollview where I display some other views, in this case an IngredientImage, that is a subclass of uiimageview:
#import "IngredientImage.h"
#implementation IngredientImage
- (id) initWithImage:(UIImage *)image {
if (self = [super initWithImage:image]) {
}
[self setUserInteractionEnabled:YES];
return self;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint location = [[touches anyObject] locationInView:self];
if (CGRectContainsPoint([self frame], location)) {
NSLog(#"This works...");
}
}
- (void)dealloc {
[super dealloc];
}
#end
and there is the code that puts the views in the scrollview
- (void)viewDidLoad {
[super viewDidLoad];
[self addIngredients];
}
- (void)addIngredients {
NSUInteger i;
for (i = 1; i <= 10; i++) {
UIImage *image = [UIImage imageNamed:#"ing.png"];
IngredientImage *imageView = [[IngredientImage alloc] initWithImage:image];
// setup each frame to a default height and width, it will be properly placed when we call "updateScrollList"
CGRect rect = imageView.frame;
rect.size.height = 50;
rect.size.width = 50;
imageView.frame = rect;
imageView.tag = i; // tag our images for later use when we place them in serial fashion
[ingredientsView addSubview:imageView];
[imageView release];
[image release];
}
UIImageView *view = nil;
NSArray *subviews = [ingredientsView subviews];
// reposition all image subviews in a horizontal serial fashion
CGFloat curYLoc = INGREDIENT_PADDING;
for (view in subviews) {
if ([view isKindOfClass:[IngredientImage class]] && view.tag > 0) {
CGRect frame = view.frame;
frame.origin = CGPointMake(INGREDIENT_PADDING, curYLoc);
view.frame = frame;
curYLoc += (INGREDIENT_PADDING + INGREDIENT_HEIGHT);
}
}
// set the content size so it can be scrollable
[ingredientsView setContentSize:CGSizeMake([ingredientsView bounds].size.width, (10 * (INGREDIENT_PADDING + INGREDIENT_HEIGHT)))];
}
the problem is that only the first view handles the touch event, and I don't know why :(
Can you help me?
Thanks
When you call
CGPoint location = [[touches anyObject] locationInView:self];
you are setting location with respect to the bounds of your imageView. But then in your if statement,
if (CGRectContainsPoint([self frame], location))
you are asking if the location is within your frame. But frame and bounds are different. Frame gives coordinates relative to your superview; bounds gives it relative to the view itself.
To fix this, change your if statement to read
if (CGRectContainsPoint([self bounds], location))
Now you are consistently using the same coordinate system in both calls, and your problem should go away.

Fitting an Image to Screen on Rotation iPhone / iPad?

I have been playing around with one of the iPhone examples from Apple' web site (ScrollViewSuite) . I am trying to tweak it a bit so that when I rotate the the iPad the image will fit into the screen in landscape mode vertical. I have been successful in getting the image to rotate, but the image is larger than the height of the landscape screen, so the bottom is below the screen. I would like to image to scale to the height of the landscape screen.
I have been playing around with various autoSizingMask attributes without success.
The imageView is called "zoomView" this is the actual image which loads into a scrollView called imageScrollView.
I am trying to achieve the screen to rotate and look like this.... sorry only 1 link allowed new user :(
olsonvox.com/photos/correct.png
However, this is what My screen is looking like.
http://www.olsonvox.com/photos/incorrect.png
I would really appreciate some advice or guidance. Below is the RootViewController.m for the project.
Blade
#
import "RootViewController.h"
#define ZOOM_VIEW_TAG 100
#define ZOOM_STEP 1.5
#define THUMB_HEIGHT 150
#define THUMB_V_PADDING 25
#define THUMB_H_PADDING 25
#define CREDIT_LABEL_HEIGHT 25
#define AUTOSCROLL_THRESHOLD 30
#interface RootViewController (ViewHandlingMethods)
- (void)toggleThumbView;
- (void)pickImageNamed:(NSString *)name;
- (NSArray *)imageNames;
- (void)createThumbScrollViewIfNecessary;
- (void)createSlideUpViewIfNecessary;
#end
#interface RootViewController (AutoscrollingMethods)
- (void)maybeAutoscrollForThumb:(ThumbImageView *)thumb;
- (void)autoscrollTimerFired:(NSTimer *)timer;
- (void)legalizeAutoscrollDistance;
- (float)autoscrollDistanceForProximityToEdge:(float)proximity;
#end
#interface RootViewController (UtilityMethods)
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center;
#end
#implementation RootViewController
- (void)loadView {
[super loadView];
imageScrollView = [[UIScrollView alloc] initWithFrame:[[self view]bounds]];
// this code makes the image resize to the width and height properly.
imageScrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin| UIViewAutoresizingFlexibleBottomMargin| UIViewAutoresizingFlexibleBottomMargin;
// TRY SETTNG CENTER HERE SOMEHOW>....
[imageScrollView setBackgroundColor:[UIColor blackColor]];
[imageScrollView setDelegate:self];
[imageScrollView setBouncesZoom:YES];
[[self view] addSubview:imageScrollView];
[self toggleThumbView];
// intitializes with the first image.
[self pickImageNamed:#"lookbook1"];
}
- (void)dealloc {
[imageScrollView release];
[slideUpView release];
[thumbScrollView release];
[super dealloc];
}
#pragma mark UIScrollViewDelegate methods
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
UIView *view = nil;
if (scrollView == imageScrollView) {
view = [imageScrollView viewWithTag:ZOOM_VIEW_TAG];
}
return view;
}
/************************************** NOTE **************************************/
/* The following delegate method works around a known bug in zoomToRect:animated: */
/* In the next release after 3.0 this workaround will no longer be necessary */
/**********************************************************************************/
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
[scrollView setZoomScale:scale+0.01 animated:NO];
[scrollView setZoomScale:scale animated:NO];
}
#pragma mark TapDetectingImageViewDelegate methods
- (void)tapDetectingImageView:(TapDetectingImageView *)view gotSingleTapAtPoint:(CGPoint)tapPoint {
// Single tap shows or hides drawer of thumbnails.
[self toggleThumbView];
}
- (void)tapDetectingImageView:(TapDetectingImageView *)view gotDoubleTapAtPoint:(CGPoint)tapPoint {
// double tap zooms in
float newScale = [imageScrollView zoomScale] * ZOOM_STEP;
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint];
[imageScrollView zoomToRect:zoomRect animated:YES];
}
- (void)tapDetectingImageView:(TapDetectingImageView *)view gotTwoFingerTapAtPoint:(CGPoint)tapPoint {
// two-finger tap zooms out
float newScale = [imageScrollView zoomScale] / ZOOM_STEP;
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint];
[imageScrollView zoomToRect:zoomRect animated:YES];
}
#pragma mark ThumbImageViewDelegate methods
- (void)thumbImageViewWasTapped:(ThumbImageView *)tiv {
[self pickImageNamed:[tiv imageName]];
[self toggleThumbView];
}
- (void)thumbImageViewStartedTracking:(ThumbImageView *)tiv {
[thumbScrollView bringSubviewToFront:tiv];
}
// CONTROLS DRAGGING AND DROPPING THUMBNAILS...
- (void)thumbImageViewMoved:(ThumbImageView *)draggingThumb {
// check if we've moved close enough to an edge to autoscroll, or far enough away to stop autoscrolling
[self maybeAutoscrollForThumb:draggingThumb];
/* The rest of this method handles the reordering of thumbnails in the thumbScrollView. See */
/* ThumbImageView.h and ThumbImageView.m for more information about how this works. */
// we'll reorder only if the thumb is overlapping the scroll view
if (CGRectIntersectsRect([draggingThumb frame], [thumbScrollView bounds])) {
BOOL draggingRight = [draggingThumb frame].origin.x > [draggingThumb home].origin.x ? YES : NO;
/* we're going to shift over all the thumbs who live between the home of the moving thumb */
/* and the current touch location. A thumb counts as living in this area if the midpoint */
/* of its home is contained in the area. */
NSMutableArray *thumbsToShift = [[NSMutableArray alloc] init];
// get the touch location in the coordinate system of the scroll view
CGPoint touchLocation = [draggingThumb convertPoint:[draggingThumb touchLocation] toView:thumbScrollView];
// calculate minimum and maximum boundaries of the affected area
float minX = draggingRight ? CGRectGetMaxX([draggingThumb home]) : touchLocation.x;
float maxX = draggingRight ? touchLocation.x : CGRectGetMinX([draggingThumb home]);
// iterate through thumbnails and see which ones need to move over
for (ThumbImageView *thumb in [thumbScrollView subviews]) {
// skip the thumb being dragged
if (thumb == draggingThumb) continue;
// skip non-thumb subviews of the scroll view (such as the scroll indicators)
if (! [thumb isMemberOfClass:[ThumbImageView class]]) continue;
float thumbMidpoint = CGRectGetMidX([thumb home]);
if (thumbMidpoint >= minX && thumbMidpoint <= maxX) {
[thumbsToShift addObject:thumb];
}
}
// shift over the other thumbs to make room for the dragging thumb. (if we're dragging right, they shift to the left)
float otherThumbShift = ([draggingThumb home].size.width + THUMB_H_PADDING) * (draggingRight ? -1 : 1);
// as we shift over the other thumbs, we'll calculate how much the dragging thumb's home is going to move
float draggingThumbShift = 0.0;
// send each of the shifting thumbs to its new home
for (ThumbImageView *otherThumb in thumbsToShift) {
CGRect home = [otherThumb home];
home.origin.x += otherThumbShift;
[otherThumb setHome:home];
[otherThumb goHome];
draggingThumbShift += ([otherThumb frame].size.width + THUMB_H_PADDING) * (draggingRight ? 1 : -1);
}
// change the home of the dragging thumb, but don't send it there because it's still being dragged
CGRect home = [draggingThumb home];
home.origin.x += draggingThumbShift;
[draggingThumb setHome:home];
}
}
- (void)thumbImageViewStoppedTracking:(ThumbImageView *)tiv {
// if the user lets go of the thumb image view, stop autoscrolling
[autoscrollTimer invalidate];
autoscrollTimer = nil;
}
#pragma mark Autoscrolling methods
- (void)maybeAutoscrollForThumb:(ThumbImageView *)thumb {
autoscrollDistance = 0;
// only autoscroll if the thumb is overlapping the thumbScrollView
if (CGRectIntersectsRect([thumb frame], [thumbScrollView bounds])) {
CGPoint touchLocation = [thumb convertPoint:[thumb touchLocation] toView:thumbScrollView];
float distanceFromLeftEdge = touchLocation.x - CGRectGetMinX([thumbScrollView bounds]);
float distanceFromRightEdge = CGRectGetMaxX([thumbScrollView bounds]) - touchLocation.x;
if (distanceFromLeftEdge < AUTOSCROLL_THRESHOLD) {
autoscrollDistance = [self autoscrollDistanceForProximityToEdge:distanceFromLeftEdge] * -1; // if scrolling left, distance is negative
} else if (distanceFromRightEdge < AUTOSCROLL_THRESHOLD) {
autoscrollDistance = [self autoscrollDistanceForProximityToEdge:distanceFromRightEdge];
}
}
// if no autoscrolling, stop and clear timer
if (autoscrollDistance == 0) {
[autoscrollTimer invalidate];
autoscrollTimer = nil;
}
// otherwise create and start timer (if we don't already have a timer going)
else if (autoscrollTimer == nil) {
autoscrollTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0)
target:self
selector:#selector(autoscrollTimerFired:)
userInfo:thumb
repeats:YES];
}
}
- (float)autoscrollDistanceForProximityToEdge:(float)proximity {
// the scroll distance grows as the proximity to the edge decreases, so that moving the thumb
// further over results in faster scrolling.
return ceilf((AUTOSCROLL_THRESHOLD - proximity) / 5.0);
}
- (void)legalizeAutoscrollDistance {
// makes sure the autoscroll distance won't result in scrolling past the content of the scroll view
float minimumLegalDistance = [thumbScrollView contentOffset].x * -1;
float maximumLegalDistance = [thumbScrollView contentSize].width - ([thumbScrollView frame].size.width + [thumbScrollView contentOffset].x);
autoscrollDistance = MAX(autoscrollDistance, minimumLegalDistance);
autoscrollDistance = MIN(autoscrollDistance, maximumLegalDistance);
}
- (void)autoscrollTimerFired:(NSTimer*)timer {
[self legalizeAutoscrollDistance];
// autoscroll by changing content offset
CGPoint contentOffset = [thumbScrollView contentOffset];
contentOffset.x += autoscrollDistance;
[thumbScrollView setContentOffset:contentOffset];
// adjust thumb position so it appears to stay still
ThumbImageView *thumb = (ThumbImageView *)[timer userInfo];
[thumb moveByOffset:CGPointMake(autoscrollDistance, 0)];
}
#pragma mark View handling methods
- (void)toggleThumbView {
[self createSlideUpViewIfNecessary]; // no-op if slideUpView has already been created
CGRect frame = [slideUpView frame];
if (thumbViewShowing) {
frame.origin.y = 0;
} else {
frame.origin.y = -225;
}
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.3];
[slideUpView setFrame:frame];
[UIView commitAnimations];
thumbViewShowing = !thumbViewShowing;
}
- (void)pickImageNamed:(NSString *)name {
// first remove previous image view, if any
[[imageScrollView viewWithTag:ZOOM_VIEW_TAG] removeFromSuperview];
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:#"%#.jpg", name]];
TapDetectingImageView *zoomView = [[TapDetectingImageView alloc] initWithImage:image];
zoomView.autoresizingMask = UIViewAutoresizingFlexibleWidth ;
[zoomView setDelegate:self];
[zoomView setTag:ZOOM_VIEW_TAG];
[imageScrollView addSubview:zoomView];
[imageScrollView setContentSize:[zoomView frame].size];
[zoomView release];
// choose minimum scale so image width fits screen
float minScale = [imageScrollView frame].size.width / [zoomView frame].size.width;
[imageScrollView setMinimumZoomScale:minScale];
[imageScrollView setZoomScale:minScale];
[imageScrollView setContentOffset:CGPointZero];
}
- (NSArray *)imageNames {
// the filenames are stored in a plist in the app bundle, so create array by reading this plist
NSString *path = [[NSBundle mainBundle] pathForResource:#"Images" ofType:#"plist"];
NSData *plistData = [NSData dataWithContentsOfFile:path];
NSString *error; NSPropertyListFormat format;
NSArray *imageNames = [NSPropertyListSerialization propertyListFromData:plistData
mutabilityOption:NSPropertyListImmutable
format:&format
errorDescription:&error];
if (!imageNames) {
NSLog(#"Failed to read image names. Error: %#", error);
[error release];
}
return imageNames;
}
- (void)createSlideUpViewIfNecessary {
if (!slideUpView) {
[self createThumbScrollViewIfNecessary];
CGRect bounds = [[self view] bounds];
float thumbHeight = [thumbScrollView frame].size.height;
float labelHeight = CREDIT_LABEL_HEIGHT;
// create label giving credit for images
UILabel *creditLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, thumbHeight, bounds.size.width, labelHeight)];
[creditLabel setBackgroundColor:[UIColor clearColor]];
[creditLabel setTextColor:[UIColor whiteColor]];
// [creditLabel setFont:[UIFont fontWithName:#"Helvetica" size:16]];
// [creditLabel setText:#"SAMPLE TEXT"];
[creditLabel setTextAlignment:UITextAlignmentCenter];
// create container view that will hold scroll view and label
CGRect frame = CGRectMake(0.0, -225.00, bounds.size.width+256, thumbHeight + labelHeight);
slideUpView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
slideUpView = [[UIView alloc] initWithFrame:frame];
[slideUpView setBackgroundColor:[UIColor blackColor]];
[slideUpView setOpaque:NO];
[slideUpView setAlpha:.75];
[[self view] addSubview:slideUpView];
// add subviews to container view
[slideUpView addSubview:thumbScrollView];
[slideUpView addSubview:creditLabel];
[creditLabel release];
}
}
- (void)createThumbScrollViewIfNecessary {
if (!thumbScrollView) {
float scrollViewHeight = THUMB_HEIGHT + THUMB_V_PADDING;
float scrollViewWidth = [[self view] bounds].size.width;
thumbScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, scrollViewWidth, scrollViewHeight)];
[thumbScrollView setCanCancelContentTouches:NO];
[thumbScrollView setClipsToBounds:NO];
// now place all the thumb views as subviews of the scroll view
// and in the course of doing so calculate the content width
float xPosition = THUMB_H_PADDING;
for (NSString *name in [self imageNames]) {
UIImage *thumbImage = [UIImage imageNamed:[NSString stringWithFormat:#"%#_thumb.jpg", name]];
if (thumbImage) {
ThumbImageView *thumbView = [[ThumbImageView alloc] initWithImage:thumbImage];
[thumbView setDelegate:self];
[thumbView setImageName:name];
CGRect frame = [thumbView frame];
frame.origin.y = THUMB_V_PADDING;
frame.origin.x = xPosition;
[thumbView setFrame:frame];
[thumbView setHome:frame];
[thumbScrollView addSubview:thumbView];
[thumbView release];
xPosition += (frame.size.width + THUMB_H_PADDING);
}
}
[thumbScrollView setContentSize:CGSizeMake(xPosition, scrollViewHeight)];
}
}
#pragma mark Utility methods
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center {
CGRect zoomRect;
// the zoom rect is in the content view's coordinates.
// At a zoom scale of 1.0, it would be the size of the imageScrollView's bounds.
// As the zoom scale decreases, so more content is visible, the size of the rect grows.
zoomRect.size.height = [imageScrollView frame].size.height / scale;
zoomRect.size.width = [imageScrollView frame].size.width / scale;
// choose an origin so as to get the right center.
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
#pragma mark -
#pragma mark Rotation support
// Ensure that the view controller supports rotation and that the split view can therefore show in both portrait and landscape.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
#end
If you've set up your UIScrollView in a Nib file, make sure it is resizing properly when rotated (Use the Autosizing controls in the Size Inspector in Interface Builder: Both sets of arrows inside the box should be red)
Then use this to rescale when the iPad changes orientation:
- (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration{
//if your UIImageView is called zoomView use this
CGRect zoomRect=CGRectMake(0,0,zoomView.frame.size.width, zoomView.frame.size.height);
[scrollView zoomToRect:zoomRect animated:YES];}
(Sorry about the bad placement of the {} but it wasn't pasting properly as code for some reason)
Hope this helps!

How to enable touchEvents (scroll and pan) in MKMapview below a custom UIView?

alt text http://www.gisnotes.com/wordpress/wp-content/uploads/2009/09/poly.png
In a nutshell, I am trying to figure out how to scale the geometry (point, line, and polygon) implemented in a custom view (geometryView) on top of MKMapView (mapView).
What I did was..
Create DrawMapViewController. Add the UIBarButtonItems (MAP, PT, LN, PG) on the bottom toolbar.
When you click on map button, you are able to pan/zoom on the Map. This enables the mapView by setting the geometryView.hidden = YES;
When any of the three geometry buttons is clicked, the geometryView is displayed by geometryView.hidden = NO, thus, enabling touchEvents and drawing the geometry from GeometryView.drawRect's method.
Layer ordering is as follows: mapView is at the bottom of geometryView.
-geometryView
-mapView
What is my problem?
Ideally, during "map" mode and when the user is panning and zooming, I am hoping if its possible to display the drawing from geometryView. But when the user hits "map", then geometryView.hidden = YES, thus the drawing disappears. If I make geometryView visible, then the user interacts with geometryView not mapView, so there's no zooming and panning.
Is it possible to handle touchEvents (pan/zoom) of MKMapView below a custom view while the custom View is displayed? Any other ideas/approaches is very much appreciated.
Thanks,
Rupert
GeometryView Listing:
#synthesize mapview, pinFactory;
- (id)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
}
return self;
}
- (void)drawRect:(CGRect)rect {
// Drawing code
NSLog(#"DrawRect called");
CGContextRef context = UIGraphicsGetCurrentContext();
// Drawing lines with a white stroke color
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
if(pinFactory.geometryState == 2){ //Draw Line
if( [pinFactory actualPinCount] > 1){
Pin *pin1 = (Pin *)[[pinFactory pinArray] objectAtIndex:0];
CGPoint pt1 = pin1.touchLocation;
CGContextMoveToPoint(context, pt1.x, pt1.y);
for (int i = 1; i < ([pinFactory actualPinCount]); i++)
{
Pin *pin2 = (Pin *)[[pinFactory pinArray] objectAtIndex:i];
CGPoint pt2 = pin2.touchLocation;
CGContextAddLineToPoint(context, pt2.x, pt2.y);
}
CGContextStrokePath(context);
}
}
else if(pinFactory.geometryState == 3){ //Draw Polygon
//if there are two points, draw a line first.
//if there are three points, fill the polygon
if( [pinFactory actualPinCount] == 2){
Pin *pin1 = (Pin *)[[pinFactory pinArray] objectAtIndex:0];
CGPoint pt1 = pin1.touchLocation;
CGContextMoveToPoint(context, pt1.x, pt1.y);
Pin *pin2 = (Pin *)[[pinFactory pinArray] objectAtIndex:1];
CGPoint pt2 = pin2.touchLocation;
CGContextAddLineToPoint(context, pt2.x, pt2.y);
CGContextStrokePath(context);
}
else if([pinFactory actualPinCount] > 2){
//fill with a blue color
CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0);
Pin *pin1 = (Pin *)[[pinFactory pinArray] objectAtIndex:0];
CGPoint pt1 = pin1.touchLocation;
CGContextMoveToPoint(context, pt1.x, pt1.y);
for (int i = 1; i < ([pinFactory actualPinCount]); i++)
{
Pin *pin2 = (Pin *)[[pinFactory pinArray] objectAtIndex:i];
CGPoint pt2 = pin2.touchLocation;
CGContextAddLineToPoint(context, pt2.x, pt2.y);
}
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFillStroke);
}
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self setNeedsDisplay];
UITouch* aTouch = [touches anyObject];
location = [aTouch locationInView:self];
NSLog(#"touchesBegan: x:%f, y:%f", location.x, location.y );
CLLocationCoordinate2D coordinate = [mapview convertPoint:location toCoordinateFromView:self];
switch (pinFactory.geometryState) {
case 1:{
if( [pinFactory actualPinCount] == 1){
//[UIView beginAnimations:#"stalk" context:nil];
//[UIView setAnimationDuration:1];
//[UIView setAnimationBeginsFromCurrentState:YES];
Pin *pin = (Pin *)[pinFactory getObjectAtIndex:0];
[mapview removeAnnotation:pin];
[pinFactory removeObject:pin];
Pin *newPin = [[Pin alloc] initWithCoordinate:coordinate initLocation:location withTitle:#"My Pin"];
[pinFactory addObject:newPin];
[mapview addAnnotation:newPin];
[newPin release];
//[UIView commitAnimations];
}
else{
//Lets add a new pin to the geometry
Pin *pin = [[Pin alloc] initWithCoordinate:coordinate initLocation:location withTitle:#"My Pin"];
[pinFactory addObject:pin];
[mapview addAnnotation:pin];
[pin release];
}
break;
}
case 2:{
//Lets add a new pin
Pin *pin = [[Pin alloc] initWithCoordinate:coordinate initLocation:location withTitle:#"My Pin"];
[pinFactory addObject:pin];
[mapview addAnnotation:pin];
[pin release];
[self setNeedsDisplay];
break;
}
case 3:{
//Lets add a new pin
Pin *pin = [[Pin alloc] initWithCoordinate:coordinate initLocation:location withTitle:#"My Pin"];
[pinFactory addObject:pin];
[mapview addAnnotation:pin];
[pin release];
[self setNeedsDisplay];
break;
}
default:
break;
}
}
- (void)dealloc {
[super dealloc];
}
#end
DrawMapViewController Listing:
#import "DrawMapViewController.h"
#import "Pin.h"
#implementation DrawMapViewController
#synthesize mapview, mapBarButton, pointBarButton, lineBarButton, polygonBarButton, geometryView;
/* State represents state of the map
* 0 = map
* 1 = point
* 2 = line
* 3 = polygon
*/
// The designated initializer. Override to perform setup that is required before the view is loaded.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
// Custom initialization
self.title = #"Map";
}
return self;
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
mapview.mapType = MKMapTypeSatellite;
NSMutableArray *pinArray = [[NSMutableArray alloc] initWithObjects:nil];
pinFactory = [[PinFactory alloc] initWithArray:pinArray];
pinFactory.map = mapview;
[pinArray release];
geometryView = [[GeometryView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 372.0f)];
geometryView.pinFactory = pinFactory;
geometryView.mapview = mapview;
geometryView.backgroundColor = [UIColor clearColor];
[self.view addSubview:geometryView];
[self changeButtonAndViewState:0];
}
/*
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)dealloc {
[mapBarButton release];
[pointBarButton release];
[lineBarButton release];
[polygonBarButton release];
[super dealloc];
}
- (IBAction)mapBarButtonPressed{
NSLog(#"mapBarButtonPressed");
[self changeButtonAndViewState:0];
}
- (IBAction)pointBarButtonPressed{
NSLog(#"pointBarButtonPressed");
[self changeButtonAndViewState:1];
if( [pinFactory actualPinCount] > 0){
[self resetGeometry];
}
}
- (IBAction)lineBarButtonPressed{
NSLog(#"lineBarButtonPressed");
if( [pinFactory actualPinCount] > 0){
[self resetGeometry];
}
[self changeButtonAndViewState:2];
}
- (IBAction)polygonBarButtonPressed{
NSLog(#"polygonBarButtonPressed");
if( [pinFactory actualPinCount] > 0){
[self resetGeometry];
}
[self changeButtonAndViewState:3];
}
- (void)resetGeometry{
NSLog(#"resetting geometry.. deleting all pins");
[mapview removeAnnotations:[pinFactory pinArray]];
NSMutableArray *array = [pinFactory pinArray];
[array removeAllObjects];
[geometryView setNeedsDisplay];
}
- (void)changeButtonAndViewState:(int)s{
[pinFactory setGeometryState:s];
mapBarButton.style = UIBarButtonItemStyleBordered;
pointBarButton.style = UIBarButtonItemStyleBordered;
lineBarButton.style = UIBarButtonItemStyleBordered;
polygonBarButton.style = UIBarButtonItemStyleBordered;
pointBarButton.enabled = YES;
lineBarButton.enabled = YES;
polygonBarButton.enabled = YES;
switch ([pinFactory geometryState]) {
case 0:{
mapBarButton.style = UIBarButtonItemStyleDone;
geometryView.hidden = YES;
break;
}
case 1:{
pointBarButton.enabled = NO;
pointBarButton.style = UIBarButtonItemStyleDone;
geometryView.hidden = NO;
break;
}
case 2:{
lineBarButton.enabled = NO;
lineBarButton.style = UIBarButtonItemStyleDone;
geometryView.hidden = NO;
break;
}
case 3:{
polygonBarButton.enabled = NO;
polygonBarButton.style = UIBarButtonItemStyleDone;
geometryView.hidden = NO;
break;
}
default:
break;
}
}
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animate{
NSLog(#"regionDidChangeAnimated");
}
#end
Hey I see nobody has answered you, and I just figured out how to do this. Unfortunately you cannot intercept events and forward them to the mapView so something like
#interface MapOverlay
MKMapView* map;
#end
#implementation
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Do my stuff
foo();
bar();
// Forward to the map
[map touchesBegan....];
}
#end
This is what you want to do but it WILL NOT WORK. For some reason you cannot intercept the map's events, nor can you do the reverse and subclass MKMapView and overload it's touch methods. The only way I have found to do this is as follows.
Create your MapOverlay view, set it to have a transparent background and disable touches to it. Overlay this on top of your MKMapView. Then do the following
Subclass UIWindow with a custom class that will forward all touches to your overlay, either handling the logic as "if the overlay is not hidden, then forward", or in the overlay itself keep state. Anyway it looks like this
#implementation CustomWindow
- (void)sendEvent:(UIEvent*)event
{
[super sendEvent:event];
// Forward the event to the overlay
}
When you are forwarding the event to the overlay, you first check if the touches are inside of your mapView region, then figure out what type of touches they are, then call the correct touch method on your overlay.
Good luck!
I know this answer probably comes too late, but I'll take a stab anyway for the benefit of those (such as myself) who've also come across this problem as well.
The MKMapView class's touch events are all handled by a UIScrollView internally. You can capture the events of this scroll view by making the MKMapView a subview of a custom UIView, and providing your custom touches methods in the custom view.
The trick is to keep track of the UIScrollView used by the MKMapView. To do this, I overrode the "hitTest" method, which returns "self", which I believe means this custom view should handle the touch events.
Also, the hitTest method gets the UIScrollView of the MKMapView. In my custom UIView I called it "echo_to" because the events are then echoed to the UIScrollView to make the map work as it does normally.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// Get the UIView (in this case, a UIScrollView) that
// would ordinarily handle the events
echo_to = [super hitTest:point withEvent:event];
// But let it be known we'll handle them instead.
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"Touches Began");
// add your custom annotation behavior here
[echo_to touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"Touches moved");
// add your custom annotation behavior here
[echo_to touchesMoved:touches withEvent:event];
}
Best of luck.
Depending on the sophistication of your geometryView, you may be able to use an annotation view to draw it.
The downside to this approach is that the view won't scale with the map (although you can use the mapView:regionDidChangeanimated: delegate method to redraw after a zoom), but it will pan with the map.
The hitTest trick works great as long as you don't need users to be able to tap on annotations, or pinch-and-zoom the map. Neither of these are able to be forwarded.