I'm a beginner to the iOS development, and I'm trying to display an image (a map in fact) which need to be zoomable and pannable. My problem is that I don't know how to add an overlay which will follow the pan to always represent the same location, but won't be scaled by the zoom. There will be several overlays, and each one must be clickable.
To zoom and pan the image (map), I added my UIImageView in a UIScrollView and it works great, but I have no idea how to add this feature.
I found this thread but it is not really the same problem since his overlay is static :
UIScrollview with two images - Keeping 1 image zoomable and 1 image static (fixed size)
I've developped the same application in android, and to make this work I was converting a pixel of the map in screen coordinates thanks to the transformation matrix, and I overrode the onDraw method to display it, but I don't know how to do the same on iOS, and I don't know if this is the better solution.
Thanks for any help.
Ok so I found a way to do it, if it can helps anybody :
I added my overlay in the scrollView, with the background image (the map).
+CustomScrollView
----> UIImageView (map)
----> OverlayImageView (overlay)
In order to zoom, the custom scrollview need a delegate with the following methods :
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
//The background image with the map
return mapView;
}
//When a zoom occurs, move the overlay
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
UIImageView* overlayView = [scroll.subviews objectAtIndex:1];
float x;
float y;
float width;
float height;
//keep the width and height of the overlay
width = overlayView.frame.size.width;
height = overlayView.frame.size.height;
//Move it to stay over the same pixel of the map, and centers it
x = (self.overlay.point.x * scroll.zoomScale - width/2);
y = (self.overlay.point.y * scroll.zoomScale - height/2);
overlayView.frame = CGRectMake(x,y,width,height);
}
With this, we are saying that the zoom only occurs on the background image, but as the overlay is in the UIScrollView, it pans with it. So the only thing we need to care is to move the Overlay when the zoom change, and we know it with the scrollViewDidZoom method.
To handle the touch events, we override the touchEnded:withEvent: of CustomScrollView and we forward it to the overlay if there is only one tap. I don't show the OverlayImageView since it only override this same method (toucheEnded:withEvent:) to handle a touch on it.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
// Coordinates in map view
CGPoint point = [touch locationInView:[self.subviews objectAtIndex:0]];
//forward
if(touch.tapCount == 1){
OverlayImageView* overlayView = [self.subviews objectAtIndex:1];
CGPoint newPoint = [touch locationInView:overlayView];
BOOL isInside = [overlayView pointInside:newPoint withEvent:event];
if(isInside){
[overlayView touchesEnded:touches withEvent:event];
}
}
// zoom
else if(touch.tapCount == 2){
if(self.zoomScale == self.maximumZoomScale){
[self setZoomScale:[self minimumZoomScale] animated:YES];
} else {
CGRect zoomRect = [self zoomRectForScrollView:self withScale:self.maximumZoomScale withCenter:point];
[self zoomToRect:zoomRect animated:YES];
//[self setZoomScale:[self maximumZoomScale] animated:YES];
}
[self setNeedsDisplay];
}
}
Hope this will help.
You can put an imageView on top as an overLay and set its userInteractionEnabled property to NO. Then you have to pan it programmatically.
Related
I have a UICollectionView with 6 pages, and paging enabled, and a UIPageControl. What I want is, when I came to the last page, if I drag to right, UICollectionView reloads from first page seamlessly.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
// The key is repositioning without animation
if (collectionView.contentOffset.x == 0) {
// user is scrolling to the left from image 1 to image 10.
// reposition offset to show image 10 that is on the right in the scroll view
[collectionView scrollRectToVisible:CGRectMake(collectionView.frame.size.width*(pageControl.currentPage-1),0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
else if (collectionView.contentOffset.x == 1600) {
// user is scrolling to the right from image 10 to image 1.
// reposition offset to show image 1 that is on the left in the scroll view
[collectionView scrollRectToVisible:CGRectMake(0,0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
pageControlUsed = NO;
}
It doesn't work like I want. What can I do?
Here's what I ended up with for my UICollectionView (horizontal scrolling like the UIPickerView):
#implementation UIInfiniteCollectionView
- (void) recenterIfNecessary {
CGPoint currentOffset = [self contentOffset];
CGFloat contentWidth = [self contentSize].width;
// don't just snap to center, since this might be done in the middle of a drag and not aligned. Make sure we account for that offset
CGFloat offset = kCenterOffset - currentOffset.x;
int delta = -round(offset / kCellSize);
CGFloat shift = (offset + delta * kCellSize);
offset += shift;
CGFloat distanceFromCenter = fabs(offset);
// don't always recenter, just if we get too far from the center. Eliza recommends a quarter of the content width
if (distanceFromCenter > (contentWidth / 4.0)) {
self.contentOffset = CGPointMake(kCenterOffset, currentOffset.y);
// move subviews back to make it appear to stay still
for (UIView *subview in self.subviews) {
CGPoint center = subview.center;
center.x += offset;
subview.center = center;
}
// add the offset to the index (unless offset is 0, in which case we'll assume this is the first launch and not a mid-scroll)
if (currentOffset.x > 0) {
int delta = -round(offset / kCellSize);
// MODEL UPDATE GOES HERE
}
}
}
- (void) layoutSubviews { // called at every frame of scrolling
[super layoutSubviews];
[self recenterIfNecessary];
}
#end
Hope this helps someone.
I've been using the Street Scroller sample to create an infinite scroller for images. That works fine until I wanted to set pagingEnabled = YES; Tried tweaking around the recenterIfNecessary code and finally realized that it's the contentOffset.x that has to match the frame of the subview that i want visible when paging stops. This really isn't going to work in recenterIfNecessary since you have no way of knowing it will get called from layoutSubviews. If you do get it adjusted right, the subview may pop out from under your finger. I do the adjustment in scrollViewDidEndDecelerating. So far I haven't had problems with scrolling fast. It will work and simulate paging even when pagingEnabled is NO, but it looks more natural with YES.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[super scrollViewDidEndDecelerating:scrollView];
CGPoint currentOffset = [self contentOffset];
// find the subview that is the closest to the currentOffset.
CGFloat closestOriginX = 999999;
UIView *closestView = nil;
for (UIView *v in self.visibleImageViews) {
CGPoint origin = [self.imageContainerView convertPoint:v.frame.origin toView:self];
CGFloat distanceToCurrentOffset = fabs(currentOffset.x - origin.x);
if (distanceToCurrentOffset <= closestOriginX) {
closestView = v;
closestOriginX = distanceToCurrentOffset;
}
}
// found the closest view, now find the correct offset
CGPoint origin = [self.imageContainerView convertPoint:closestView.frame.origin toView:self];
CGPoint center = [self.imageContainerView convertPoint:closestView.center toView:self];
CGFloat offsetX = currentOffset.x - origin.x;
// adjust the centers of the subviews
[UIView animateWithDuration:0.1 animations:^{
for (UIView *v in self.visibleImageViews) {
v.center = [self convertPoint:CGPointMake(v.center.x+offsetX, center.y) toView:self.imageContainerView];
}
}];
}
I have not used UICollectionView for infinite scrolling, but when doing it with a UIScrollView you first adjust your content offset (instead of using scrollRectToVisible) to the location you want. Then, you loop through each subview in your scroller and adjust their coordinates either to the right or left based on the direction the user was scrolling. Finally, if either end is beyond the bounds you want them to be, move them to the far other end. Their is a very good WWDC video from apple about how to do infinite scrolling you can find here: http://developer.apple.com/videos/wwdc/2012/
In my code I have an array of objects which are UIImageViews with UIImages in them.
I use a for loop in -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event to move all objects from the array one by one.
Once they are moved to different positions on the main view I would like to be able to grab any of those objects and move them somewhere else.
I'm not sure how best to do this. Is there a way I can use touchesbegin so that I can drag only the item I clicked on?
Basically, there are five UIImageViews there now and each one has UIImage and I am trying to get to the point where when i click on any of them they are able to move around.
EDIT: John Muchow posted this on a blog here. You should be able to adapt for your own purposes.
//UIDraggableImageView.h
#import <UIKit/UIKit.h>
#interface UIDraggableImageView : UIImageView
{
CGPoint currentPoint;
}
#end
//UIDraggableImageView.m
#import "UIDraggableImageView.h"
#implementation UIDraggableImageView
- (id)initWithImage:(UIImage *)image
{
if (self = [super initWithImage:image]) {
self.userInteractionEnabled = YES;
}
return self;
}
-(void)dealloc {
[super dealloc];
}
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
// When a touch starts, get the current location in the view
currentPoint = [[touches anyObject] locationInView:self];
}
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
// Get active location upon move
CGPoint activePoint = [[touches anyObject] locationInView:self];
// Determine new point based on where the touch is now located
CGPoint newPoint = CGPointMake(self.center.x + (activePoint.x - currentPoint.x),
self.center.y + (activePoint.y - currentPoint.y));
//--------------------------------------------------------
// Make sure we stay within the bounds of the parent view
//--------------------------------------------------------
float midPointX = CGRectGetMidX(self.bounds);
// If too far right...
if (newPoint.x > self.superview.bounds.size.width - midPointX)
newPoint.x = self.superview.bounds.size.width - midPointX;
else if (newPoint.x < midPointX) // If too far left...
newPoint.x = midPointX;
float midPointY = CGRectGetMidY(self.bounds);
// If too far down...
if (newPoint.y > self.superview.bounds.size.height - midPointY)
newPoint.y = self.superview.bounds.size.height - midPointY;
else if (newPoint.y < midPointY) // If too far up...
newPoint.y = midPointY;
// Set new center location
self.center = newPoint;
}
#end
I've done something similar with great success using the UIPanGestureRecognizer in my draggable view. To hook it up, it might look something like this:
#implementation DraggableView
-(id)initWithFrame:(CGRect)frame
{
...
...
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)] autorelease];
[self addGestureRecognizer:panGesture];
..
return self;
}
#end
In your pan: method, you could check the state of the gesture and even capture the gesture's location and use it to relocate your view. Here's a rough example of what it might look like:
-(void)pan:(UIPanGestureRecognizer *)gesture
{
if (gesture.state==UIGestureRecognizerStateChanged)
{
//We're moving!
UIView *aRootView = <perhaps your root view controller's view>;
CGPoint currentOrigin = [gesture translationInView:aRootView];
self.frame = CGRectMake(currentOrigin.x,currentOrigin.y,self.frame.size.width,self.frame.size.hight);
...
...
}
}
Here is more reading on the UIPanGestureRecognizer.
You might want to consider adding gesture recognizers to the views that you want to move, that way you can easily tell when they are tapped and you can use touchesMoved to move them around...
On a button click add an image from camera roll to the imageview. Image can zoom in and out and should drag that image to any place in the view.
I have an UIImageView on my view. Certain parts of that image, I want to use as UIButtons. But the problem is, Those image parts are not of in a regular shape. I haven't really tried anything. The immediate solution which came to my mind was to recognize small frames of image and put custom button over that frame. But since the image portion (button area) is not of regular shape, this concept failed.
How can I place buttons on that UIImageView so that particular portion(not of regular shape) of image responds to some action.
Consider the following image :
I want to place one button at the center circle which is dark. I want to place buttons in between two perpendicular lines so that a quarter of button is clickable each with different action. So, in all I have 5 buttons.
I can think of 2 options:
first one
-(CGFloat)percentFromHeight:(CGFloat)percent{
return (CGRectGetHeight(self.bounds)/100)*percent;
}
-(CGFloat)percentFromWidth:(CGFloat)percent{
return (CGRectGetWidth(self.bounds)/100)*percent;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self];
//first check if the tup is in the center
if(currentTouchPosition.x > [view percentFromWidth:45] && currentTouchPosition.x < [view percentFromWidth:55] && currentTouchPosition.y > [view percentFromWidth:45] && currentTouchPosition.y < [view percentFromWidth:55]){
//the tup is in the center
//Do your thing
return;
}
//if this is not in the center, check other options
if(currentTouchPosition.x > [view percentFromWidth:50]){
if(currentTouchPosition.y > [view percentFromHeight:50]{
//this is the bottom left side
else{
//this is the top left side
}
}else if(currentTouchPosition.x < [view percentFromWidth:50]{
if(currentTouchPosition.y > [view percentFromHeight:50]{
//this is the bottom right side
else{
//this is the top right side
}
}
second -
subclass UIButton / UIImage as you prefer and use UIBezierPath to draw custom shapes in drawRect:, then use [UIBezierPath containsPoint] to check if the those is inside the path of the view/button.
Read this topic - even the same image.
My opinion is you should use touches method to find which portion is touched(using the image frame and touchable portions' frame) and make appropriate action than adding a button.
I am attempting to make a UIView/UIControl that people can drag up and reveal a text box, and then drag down to hide it. However, I have yet to find a method to make this "fluid" - it always seems to stop at random places and doesn't allow any more movement. At the moment I am using a UIView for the top part of the view and here is the current code:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
if ([touch view] == topMenuView) {
CGPoint location = [touch locationInView:self.superview];
CGPoint locationInsideBox = [touch locationInView:self];
CGPoint newLocation = location;
newLocation.x = self.center.x;
newLocation.y = newLocation.y + (self.frame.size.height - locationInsideBox.y) / 2;
if ((self.superview.frame.size.height - newLocation.y) < (self.frame.size.height / 2) && (self.superview.frame.size.height - newLocation.y) > -32)
{
self.center = newLocation;
}
return;
}
}
Any help would be much appreciated!
I would use a pan gesture recognizer. The following code will simply move the view up and down along with the user's finger. If you want to limit how far up it moves, have it snap to place or have momentum you'll need to add to it.
UIView * view; // The view you're moving
CGRect originalFrame; // The frame of the view when the touch began
- (void) pan:(UIPanGestureRecognizer *)pan {
switch (pan.state) {
case UIGestureRecognizerStateBegan: {
originalFrame = view.frame;
} break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded: {
CGPoint translation = [pan translationInView:view];
CGRect frame = originalFrame;
frame.origin.y += translation.y;
view.frame = frame;
} break;
}
}
Removing this line may well help you:
newLocation.y = newLocation.y + (self.frame.size.height - locationInsideBox.y) / 2;
What you're trying to accomplish is really nothing more than a scrollable view, so I would recommend using a UIScrollView.
Put your UIView in a UIScrollView with transparent background, and place the UIScrollView on top of your textbox. Set the correct contentSize and you're good to go.
use a uiview animation block to update the frame of the sliding view corresponding with the touch point recieved. set the animation blocks duration to something really short like .01 or lower.
I'd recommend splitting the issue in two:
Implement the view which has the text box at the bottom - You will just have to implement your own custom view/view controller.
Add your view as a subview of a UIScrollView.
This is a good tutorial which demonstrates proper initialization of UIScrollView and embedding content in it.
Custom views/controllers are a bit of a broader subject :)
Could someone please tell me how to make an image appear when the user taps the screen and make it appear at the position of the tap.
Thanks in advance,
Tate
UIView is a subclass of UIResponder, which has the following methods that might help: -touchesBegan:withEvent:, -touchesEnded:withEvent:, -touchesCancelled:withEvent: and -touchesMoved:withEvent:.
The first parameter of each of those is an NSSet of UITouch objects. UITouch has a -locationInView: instance method which should yield the position of the tap in your view.
You could create an initial star and just move it every time view is touched.
I'm not sure what you're end result will look like.
Note:
This code will give you 1 star that moves with a tap
Here is my code:-
(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSSet *allTouches = [event allTouches];
switch ([allTouches count]) {
case 1:
{
UITouch *touch = [[allTouches allObjects] objectAtIndex:0];
CGPoint point = [touch locationInView:myView];
myStar.center = point;
break;
}
default:
break;
}
}
It seems implied from the question that you want the user to be able to tap anywhere on the screen and have an image drawn where they tap? As opposed to tapping in a designated place and having the image appear there?
If so, you're probably going to have to go with a custom view. In that case, you'd do something like the following:
Create a subclass of UIView.
Override the touchesBegan method. Call [[touches anyObject] locationInView:self] (where touches is the first argument to the method, an NSSet of UITouch objects) to get the location of the touch, and record it.
Override the touchesEnded method. Determine the location touches ended at using the same method as in step 2.
If the second location is near the first, you'll want to place your image at that location. Record that location and call [self setNeedsDisplay] to cause the custom view to be redrawn.
Override the drawRect method. Here, if the location has been set in step 4, you can use the UIImage method drawAtPoint to draw your image at the selected location.
For further details, this link might be worth a look. Hope that helps!
EDIT: I've notice you've asked essentially the same question before. If you're not happy with the answers given there, it's generally considered better to "bump" the old one, perhaps by editing it to ask for further clarification, rather than create a new question.
EDIT: As requested, some very brief sample code follows. This is probably not the best code around, and I haven't tested it, so it may be a little iffy. Just for clarification, the THRESHOLD allows the user to move their finger a little while tapping (up to 3px), because it's very difficult to tap without moving your finger a little.
MyView.h
#define THRESHOLD 3*3
#interface MyView : UIView
{
CGPoint touchPoint;
CGPoint drawPoint;
UIImage theImage;
}
#end
MyView.m
#implementation MyView
- (id) initWithFrame:(CGRect) newFrame
{
if (self = [super initWithFrame:newFrame])
{
touchPoint = CGPointZero;
drawPoint = CGPointMake(-1, -1);
theImage = [[UIImage imageNamed:#"myImage.png"] retain];
}
return self;
}
- (void) dealloc
{
[theImage release];
[super dealloc];
}
- (void) drawRect:(CGRect) rect
{
if (drawPoint.x > -1 && drawPoint.y > -1)
[theImage drawAtPoint:drawPoint];
}
- (void) touchesBegan:(NSSet*) touches withEvent:(UIEvent*) event
{
touchPoint = [[touches anyObject] locationInView:self];
}
- (void) touchesEnded:(NSSet*) touches withEvent:(UIEvent*) event
{
CGPoint point = [[touches anyObject] locationInView:self];
CGFloat dx = point.x - touchPoint.x, dy = point.y - touchPoint.y;
if (dx + dy < THRESHOLD)
{
drawPoint = point;
[self setNeedsDisplay];
}
}
#end