I have a "world view," so to speak, that consists of a large, zoomable area on a UIScrollView. I want several buttons to retain their world location when I pinch to zoom, much like pins in Google Maps do. The code I have been trying (I have been coding for hours, but I think it sounds right...although I may be just burned out) is:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
CGPoint center = button.center;
center.y = center.y * scrollView.zoomScale;
center.x = center.x * scrollView.zoomScale;
button.center = center;
NSLog(#"Button coordinates are %f, %f, zoomScale is %f", button.center.x, button.center.y, scrollView.zoomScale);
}
Can anyone see what I'm doing wrong?
Thanks in advance!
I solved this by setting the CGPoint 'center' only once, on view load, and then calculating a new center point based off of the original location.
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
CGPoint newCenter = center; //center is set at load time, and only once
newCenter.y = center.y * scrollView.zoomScale; //new center is calculated on original position
newCenter.x = center.x * scrollView.zoomScale;
button.center = newCenter;
NSLog(#"Button coordinates are %f, %f, zoomScale is %f", button.center.x, button.center.y, scrollView.zoomScale);
}
Sorry about mistagging the question, Peter.
You don't need to do all the maths yourself. UIScrollView's base class, UIView, has four methods for converting between coordinate systems, which in the case of a UIScrollView take zooming and panning into account:
– convertPoint:toView:
– convertPoint:fromView:
– convertRect:toView:
– convertRect:fromView:
See the UIView documentation at http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
Related
I'm new to iOS development and I'm struggling with porting some code from iOS6 involving the use of MKOverlay.
When the overlay radius or coordinate change, the renderer should update the display accordingly in real time.
This part works, but if I drag the overlay too much, it reaches some boundary and the rendering gets cut off. I can't find any documentation or help on this behavior.
In the CircleOverlayRenderer class:
- (id)initWithOverlay:(id<MKOverlay>)overlay
{
self = [super initWithOverlay:overlay];
if (self) {
CircleZone *bOverlay = (CircleZone *)overlay;
[RACObserve(bOverlay, coordinate) subscribeNext:^(id x) {
[self setNeedsDisplay];
}];
[RACObserve(bOverlay, radius) subscribeNext:^(id x) {
[self setNeedsDisplay];
}];
}
return self;
}
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
CGRect rect = [self rectForMapRect:[self.overlay boundingMapRect]];
CGContextSaveGState(context);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextSetFillColorSpace(context, colorSpace);
CGColorSpaceRelease(colorSpace);
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextSetFillColor(context, color);
CGContextSetAllowsAntialiasing(context, YES);
// outline
{
CGContextSetAlpha(context, 0.8);
CGContextFillEllipseInRect(context, rect);
}
// red
{
CGContextSetAlpha(context, 0.5);
CGRect ellipseRect = CGRectInset(rect, 0.01 * rect.size.width / 2, 0.01 * rect.size.height / 2);
CGContextFillEllipseInRect(context, ellipseRect);
}
CGContextRestoreGState(cox);
}
In the CircleOverlay class:
- (MKMapRect)boundingMapRect
{
MKMapPoint center = MKMapPointForCoordinate(self.coordinate);
double mapPointsPerMeter = MKMapPointsPerMeterAtLatitude(self.coordinate.latitude);
double mapPointsRadius = _radius * mapPointsPerMeter;
return MKMapRectMake(center.x - mapPointsRadius, center.y - mapPointsRadius,
mapPointsRadius * 2.0, mapPointsRadius * 2.0);
}
Here are some screen shots of the problem I'm seeing:
Problem when dragging overlay too much:
Problem when changing the radius:
The problem does go away if I keep zooming the map out. After the map tiles refresh, the overlay no longer gets cut off...
If anyone had a similar problem, please help me, it's driving me crazy!
Looking at the radius example, it makes me suspect the boundingMapRect, given how its cropping. Looking at the boundingMapRect implementation, the reliance upon MKMapPointsPerMeterAtLatitude (esp when you're looking at a large region) is worrying. That function is useful if you are, for example, trying to figure out where a coordinate 10 meters from some other coordinate, but when looking at really large spans, it doesn't always work out well.
I might, instead, suggest something that gets the MKCoordinateRegion of where the circle is, and then convert that to MKMapRect. A simplistic implementation might look like:
- (MKMapRect)boundingMapRect {
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(self.coordinate, _radius * 2, _radius * 2);
CLLocationCoordinate2D upperLeftCoordinate = CLLocationCoordinate2DMake(region.center.latitude - region.span.latitudeDelta / 2, region.center.longitude - region.span.longitudeDelta / 2);
CLLocationCoordinate2D lowerRightCoordinate = CLLocationCoordinate2DMake(region.center.latitude + region.span.latitudeDelta / 2, region.center.longitude + region.span.longitudeDelta / 2);
MKMapPoint upperLeft = MKMapPointForCoordinate(upperLeftCoordinate);
MKMapPoint lowerRight = MKMapPointForCoordinate(lowerRightCoordinate);
return MKMapRectMake(MIN(upperLeft.x, lowerRight.x),
MIN(upperLeft.y, lowerRight.y),
ABS(upperLeft.x - lowerRight.x),
ABS(upperLeft.y - lowerRight.y));
}
You'll have to tweak with this to make sure it gracefully handles crossing of the 180th meridian and when the circle encompasses the north pole, but it illustrates the basic idea: Get MKCoordinateRegion for the circle and then convert that to MKMapRect.
I'm trying to play a video in UITableview cell using MPMovieplayerController. I'm calling the video play function inside scrollview delegate scrollViewDidScroll, when it scrolls to a visible rect.
I used the following codes for that:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
CGSize size = aScrollView.contentSize;
UIEdgeInsets inset = aScrollView.contentInset;
float y = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
/*
NSLog(#"offset: %f", offset.y);
NSLog(#"content.height: %f", size.height);
NSLog(#"bounds.height: %f", bounds.size.height);
NSLog(#"inset.top: %f", inset.top);
NSLog(#"inset.bottom: %f", inset.bottom);
NSLog(#"pos: %f of %f", y, h);
*/
if(offset.y - 100 > bounds.size.height/2 && !playLock)
{
[self performSelectorOnMainThread:#selector(startPlayingVideo) withObject:nil waitUntilDone:YES];
playLock = YES;
}
}
But the video starts playing only after when I remove fingers from screen. I want to play video without releasing the fingers (Similar to the vine app). When I checked MPMoviePlayer state during my touch in the view, it always falls to stopped state.
It will start playing only after removing the touch from the screen. Is there any right way to achieve this?
Solved this issue by replacing MPMoviePlayer with AVPlayer.
I am using a UILongPressGestureRecognizer which works perfectly but the data I get is not precise enough for my use case. CGPoints that I get are rounded off I think.
Example points that I get: 100.5, 103.0 etc. The decimal part is either .5 or .0 . Is there a way to get more precise points? I was hoping for something like .xxxx as in '100.8745' but .xx would do to.
The reason I need this is because I have a circular UIBezierPath, I want to restrict a drag gesture to only that circular path. The item should only be draggable along the circumference of this circle. To do this I calculated 720 points on the circle's boundary using it's radius. Now these points are .xxxx numbers. If I round them off, the drag is not as smooth around the middle section of the circle.This is because in the middle section, the equator, the points on the x-coordinate are very close together. So when I rounded of the y-coordinate, I lost a lot of points and hence the "not so smooth" drag action.
Here is how I calculate the points
for (CGFloat i = -154;i<154;i++) {
CGPoint point = [self pointAroundCircumferenceFromCenter:center forX:i];
[bezierPoints addObject:[NSValue valueWithCGPoint:point]];
i = i - .5;
}
- (CGPoint)pointAroundCircumferenceFromCenter:(CGPoint)center forX:(CGFloat)x
{
CGFloat radius = 154;
CGPoint upperPoint = CGPointZero;
CGPoint lowerPoint = CGPointZero;
//theta used to be the x variable. was first calculating points using the angle
/* point.x = center.x + radius * cosf(theta);
point.y = center.y + radius * sinf(theta);*/
CGFloat y = (radius*radius) - (theta*theta);
upperPoint.x = x+156;
upperPoint.y = 230-sqrtf(y);
lowerPoint.x = x+156;
lowerPoint.y = sqrtf(y)+230;
NSLog(#"x = %f, y = %f",upperPoint.x, upperPoint.y);
[lowerPoints addObject:[NSValue valueWithCGPoint:lowerPoint]];
[upperPoints addObject:[NSValue valueWithCGPoint:upperPoint]];
return upperPoint;
}
I know the code is weird I mean why would I add the points into arrays and return one point back.
Here is how I restrict the movement
-(void)handleLongPress:(UILongPressGestureRecognizer *)recognizer{
CGPoint finalpoint;
CGPoint initialpoint;
CGFloat y;
CGFloat x;
CGPoint tempPoint;
if(recognizer.state == UIGestureRecognizerStateBegan){
initialpoint = [recognizer locationInView:self.view];
CGRect rect = CGRectMake(initialpoint.x, initialpoint.y, 40, 40);
self.hourHand.frame = rect;
self.hourHand.center = initialpoint;
NSLog(#"Long Press Activated at %f,%f",initialpoint.x, initialpoint.y );
}
else if (recognizer.state == UIGestureRecognizerStateChanged){
CGPoint currentPoint = [recognizer locationInView:self.view];
x = currentPoint.x-initialpoint.x;
y = currentPoint.y-initialpoint.y;
tempPoint = CGPointMake( currentPoint.x, currentPoint.y);
NSLog(#"temp point ::%f, %f", tempPoint.x, tempPoint.y);
tempPoint = [self givePointOnCircleForPoint:tempPoint];
self.hourHand.center = tempPoint;
}
else if (recognizer.state == UIGestureRecognizerStateEnded){
// finalpoint = [recognizer locationInView:self.view];
CGRect rect = CGRectMake(tempPoint.x, tempPoint.y, 20, 20);
self.hourHand.frame = rect;
self.hourHand.center = tempPoint;
NSLog(#"Long Press DeActivated at %f,%f",tempPoint.x, tempPoint.y );
}
}
-(CGPoint)givePointOnCircleForPoint:(CGPoint) point{
CGPoint resultingPoint;
for (NSValue *pointValue in allPoints){
CGPoint pointFromArray = [pointValue CGPointValue];
if (point.x == pointFromArray.x) {
// if(point.y > 230.0){
resultingPoint = pointFromArray;
break;
// }
}
}
Basically, I taking the x-coordinate of the "touched point" and returning the y by comparing it to the array of points I calculated earlier.
Currently this code works for half a circle only because, each x has 2 y values because it's a circle, Ignore this because I think this can be easily dealt with.
In the picture, the white circle is the original circle, the black circle is the circle of the points I have from the code+formatting it to remove precision to fit the input I get. If you look around the equator(red highlighted part) you will see a gap between the next points. This gap is my problem.
To answer your original question: On a device with a Retina display, one pixel is 0.5 points, so 0.5 is the best resolution you can get on this hardware.
(On non-Retina devices, 1 pixel == 1 point.)
But it seems to me that you don't need that points array at all. If understand the problem correctly, you can use the following code to
"restrict" (or "project") an arbitrary point to the circumference of the circle:
CGPoint center = ...; // Center of the circle
CGFloat radius = ...; // Radius of the circle
CGPoint point = ...; // The touched point
CGPoint resultingPoint; // Resulting point on the circumference
// Distance from center to point:
CGFloat dist = hypot(point.x - center.x, point.y - center.y);
if (dist == 0) {
// The touched point is the circle center.
// Choose any point on the circumference:
resultingPoint = CGPointMake(center.x + radius, center.y);
} else {
// Project point to circle circumference:
resultingPoint = CGPointMake(center.x + (point.x - center.x)*radius/dist,
center.y + (point.y - center.y)*radius/dist);
}
Most of the sample code on the net assumes your UIScrollView will be full-screen.
I'm trying to create a UIScrollView with just a UIImageView inside it, and just want to be able to pinch zoom as would normally happen. My UIScrollView only fills a part of the screen, so I can't just use autosizing to keep it the right size, as the sample applications seem to do.
The closest I've got is to add this to the UIScrollViewDelegate:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
CGPoint cOffset = imageScrollView.contentOffset;
float cScale = imageScrollView.contentScaleFactor;
CGRect imageFrame = ptImage.frame;
float scale = scrollView.zoomScale;
imageFrame.size.width *= scale;
imageFrame.size.height *= scale;
ptImage.frame = imageFrame;
scrollView.zoomScale = 1.0;
scrollView.frame = CGRectMake(0.0, 0.0, scrollContainerView.frame.size.width, scrollContainerView.frame.size.height);
scrollView.bounds = CGRectMake(cOffset.x * cScale * scale , cOffset.y * cScale * scale, scrollView.bounds.size.width, scrollView.bounds.size.height);
scrollView.contentSize = imageFrame.size;
}
The only problem here is that the bounds offset (scrollView.bounds = ...) is incorrect.
Surely it's far simpler than I've done here?
Any help?
Try returning your UIImageView in :
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
And remove the code you put in the didZoom method and see if that works.
I am trying to create a higher resolution image of a UIView, specifically UITextView.
This question and answer is exactly what I am trying to figure out:
Retain the resolution of the label after scaling in iphone
But, when I do the same, my text is still blurry:
self.view.transform = CGAffineTransformMakeScale(2.f, 2.f);
[myText setContentScaleFactor:2.f]; // myText is a subview of self.view object
I have also tried the same in the Apple sample project "UICatalog" to UILabel and it is also blurry.
I can't understand why it would work for Warrior from the other question and not for me. I would have asked there — but I can't seem to leave a comment or a question there.
Setting the contentScaleFactor and contentsScale is in fact the key, as #dbotha pointed out, however you have to walk the view and layer hierarchies separately in order to reach every internal CATiledLayer that actually does the text rendering. Adding the screen scale might also make sense.
So the correct implementation would be something like this:
- (void)updateForZoomScale:(CGFloat)zoomScale {
CGFloat screenAndZoomScale = zoomScale * [UIScreen mainScreen].scale;
// Walk the layer and view hierarchies separately. We need to reach all tiled layers.
[self applyScale:(zoomScale * [UIScreen mainScreen].scale) toView:self.textView];
[self applyScale:(zoomScale * [UIScreen mainScreen].scale) toLayer:self.textView.layer];
}
- (void)applyScale:(CGFloat)scale toView:(UIView *)view {
view.contentScaleFactor = scale;
for (UIView *subview in view.subviews) {
[self applyScale:scale toView:subview];
}
}
- (void)applyScale:(CGFloat)scale toLayer:(CALayer *)layer {
layer.contentsScale = scale;
for (CALayer *sublayer in layer.sublayers) {
[self applyScale:scale toLayer:sublayer];
}
}
UITextView has textInputView property, which "both draws the text and provides a coordinate system" (https://developer.apple.com/reference/uikit/uitextinput/1614564-textinputview)
So i'm using the following code to scale UITextView - without any font changes, without using any "CALayer" property and keeping high quality:
float screenScaleFactor = [[UIScreen mainScreen] scale];
float scale = 5.0;
textView.transform = CGAffineTransformMakeScale(scale, scale);
textView.textInputView.contentScaleFactor = screenScaleFactor * scale;
Comment the last line if you need low quality (but better performance) scaling.
Transform uses view.center as scaling center point, so adding a 'translate transform' is needed to scale around view corner.
Be sure to apply the contentScaleFactor to all subviews of the UITextView. I've just tested the following with a UITextView and found it to work:
- (void)applyScale:(CGFloat)scale toView:(UIView *)view {
view.contentScaleFactor = scale;
view.layer.contentsScale = scale;
for (UIView *subview in view.subviews) {
[self applyScale:scale toView:subview];
}
}