custom map annotation callout - how to control width - ios

I've successfully implemented the custom map annotation callout code from the asynchrony blog post .
(When user taps a map pin, I show a customized image instead of the standard callout view).
The only remaining problem is that the callout occupies the entire width of the view, and the app would look much better if the width corresponded to the image I'm using.
I have subclassed MKAnnotationView, and when I set it's contentWidth to the width of the image, the triangle does not always point back to the pin, or the image is not even inside it's wrapper view.
Any help or suggestions would be great.
Thanks.

I ran into a similar problem when implementing the CalloutMapAnnotationView for the iPad. Basically I didn't want the iPad version to take the full width of the mapView.
In the prepareFrameSize method set your width:
- (void)prepareFrameSize {
// ...
// changing frame x/y origins here does nothing
frame.size = CGSizeMake(320.0f, height);
self.frame = frame;
}
Next you'll have to calculate the xOffset based off the parentAnnotationView:
- (void)prepareOffset {
// Base x calculations from center of parent view
CGPoint parentOrigin = [self.mapView convertPoint:self.parentAnnotationView.center
fromView:self.parentAnnotationView.superview];
CGFloat xOffset = 0;
CGFloat mapWidth = self.mapView.bounds.size.width;
CGFloat halfWidth = mapWidth / 2;
CGFloat x = parentOrigin.x + (320.0f / 2);
if( parentOrigin.x < halfWidth && x < 0 ) // left half of map
xOffset = -x;
else if( parentOrigin.x > halfWidth && x > mapWidth ) // right half of map
xOffset = -( x - mapWidth);
// yOffset calculation ...
}
Now in drawRect:(CGRect)rect before the callout bubble is drawn:
- (void)drawRect:(CGRect)rect {
// ...
// Calculate the carat lcation in the frame
if( self.centerOffset.x == 0.0f )
parentX = 320.0f / 2;
else if( self.centerOffset.x < 0.0f )
parentX = (320.0f / 2) + -self.centerOffset.x;
//...
}
Hope this helps put you on the right track.

Related

Restrict Touch on ColorPicker UIView - iOS

I am using third party Color picker wheel (ISColorWheel- https://github.com/justinmeiners/ios-color-wheel) to pick a color and display it on screen. I need to restrict selecting blue color if particular button is enabled.
When i see the color picker library class, they have implemented following code to restrict the Knob view to move around the color picker.
- (void)setTouchPoint:(CGPoint)point
{
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
CGPoint center = CGPointMake(width / 2.0, height / 2.0);
// Check if the touch is outside the wheel
if (ISColorWheel_PointDistance(center, point) < _radius)
{
//NSLog(#"Distance is %f and Radius is %f",ISColorWheel_PointDistance(center, point),_radius);
_touchPoint = point;
}
else
{
// If so we need to create a drection vector and calculate the constrained point
CGPoint vec = CGPointMake(point.x - center.x, point.y - center.y);
float extents = sqrtf((vec.x * vec.x) + (vec.y * vec.y));
vec.x /= extents;
vec.y /= extents;
_touchPoint = CGPointMake(center.x + vec.x * _radius, center.y + vec.y * _radius);
NSLog(#"Touch Point is %f %f",_touchPoint.x,_touchPoint.y);
}
[self updateKnob];
}
The above code restrict the user to move knobView away from the circle. In my case i need to restrict the user not to select Blue color of ColorPicker. How can i implement it. How to find the trajectory of Blue color.
You should define a triangle that defines the color blue as you see it (How much green dose it contain in one side how much purple on the other) then look for you Point inside that triangle. One way to do it is here: https://stackoverflow.com/a/9755252/1870192

Sprite Kit - Move scene and camera with sprite when sprite is going off screen

I am trying to make a scene that extends up off the screen for a certain distance, and I want the camera to stay centered on the player node. When the player reaches the top of the screen, I want the bottom nodes to disappear below the screen bounds and the new off screen part of the scene to become visible. Like Mario!
I create the scene size like this:
CGSize screenSize = CGSizeMake(skView.bounds.size.width, skView.bounds.size.height + (skView.bounds.size.height/3));//+ (skView.bounds.size.height/3)
scene = [GameScene sceneWithSize:screenSize];
And then, add all of my nodes to a WorldNode as in This question.
I have modified it to be:
- (void)didSimulatePhysics
{
CGFloat margin = self.size.height/3;// previously defined
// make sure the cat's position is defined in scene coordinates
CGPoint catPosition = [self convertPoint:cat.position fromNode:cat.parent];
if (catPosition.y > (self.size.height - margin))
{
CGPoint worldPosition = worldNode.position;
worldPosition.y -= catPosition.y - (self.size.height - margin);
worldNode.position = worldPosition;
}
else if (catPosition.y < (self.size.height - margin))
{
CGFloat maxWorldHeight = self.size.height;
CGPoint worldPosition = worldNode.position;
// this keeps the cat on the margin line
if (worldPosition.y != worldStartPosition.y)
{
worldPosition.y += (self.size.height - margin) - catPosition.y;
}
if (worldPosition.y > maxWorldHeight) // assume maxWorldHeight is defined
{
worldPosition.y = maxWorldHeight;
}
worldNode.position = worldPosition;
}
}
Ok, I have it almost working. I can't seem to stop the world at the top boarder of the scene, and the character does not fall when he reaches below the margin, as in the world keeps following him down past the worlds initial start point. How can I get the screen to only move up and down if the character is above the margin, and then if the character is below the margin, the screen does not move?
You want to move your world node down by an amount equal to how far the cat is above where you want it to be. Your didSimulatePhysics method would end up looking something like this:
- (void)didSimulatePhysics
{
CGFloat margin; // set this to be whatever you want it to be
// make sure the cat's position is defined in scene coordinates
CGPoint catPosition = [self convertPoint:cat.position fromNode:cat.parent];
if (catPosition.y > (self.size.height - margin))
{
// the cat is above the margin, we need to move the world node down until the
// cat is at the margin
// pull world position into local variable so you can modify the y component
CGPoint worldPosition = worldNode.position;
// move the world down so the cat is on the margin
worldPosition.y -= catPosition.y - (self.size.height - margin);
worldNode.position = worldPosition;
}
else
{
// the cat is below the margin, we need to move the world node up, but only
// until the world node is at its starting position
CGPoint worldPosition = worldNode.position;
// move the cat up to the margin line
worldPosition.y += (self.size.height - margin) - catPosition.y;
// if this caused the world to be too high, move it back down
if (worldPosition.y > maxWorldHeight)
{
worldPosition.y = maxWorldHeight;
}
worldNode.position = worldPosition;
}
}
One thing to take note of is that maxWorldHeight is not how tall the world is, but rather how far above the bottom of the screen it's allowed to be. You should set it to the y-component of the word node's position when you first place it in the scene.
- (void)didSimulatePhysics
{
CGPoint target = [self pointToCenterViewOn:cat.position];
CGPoint newPosition = _worldNode.position;
newPosition.x += (target.x - worldNode.position.x) * 0.1f;
newPosition.y += (target.y - worldNode.position.y) * 0.1f;
worldNode.position = newPosition;
}
- (CGPoint)pointToCenterViewOn:(CGPoint)centerOn
{
CGFloat x = Clamp(centerOn.x, self.size.width / 2, worldNode.size.width - self.size.width / 2);
CGFloat y = Clamp(centerOn.y, self.size.height / 2, worldNode.size.height - self.size.height/2);
return CGPointMake(-x, -y);
}

In Cocos2d setting anchor point on a layer for pinch to zoom not working as expected

Right now I'm trying to implement a pinch-to-zoom feature in my Cocos2D game for iOS and I'm encountering really strange behavior. My goal is to use the handler for UIPinchGestureRecognizer to scale one of the CCNodes that represents the game level when a player pinches the screen. This has the effect of zooming.
The issue is if I set the anchor for zooming to some arbitrary value such as .5, .5 (the center of the level CCNode) it scales perfectly around the center of the level, but I want to scale around the center of the player's view. Here is what that looks like:
- (void) handlePinchFrom:(UIPinchGestureRecognizer*) recognizer
{
if(recognizer.state == UIGestureRecognizerStateEnded)
{
_isScaling = false;
_prevScale = 1.0;
}
else
{
_isScaling = true;
float deltaScale = 1.0 - _prevScale + recognizer.scale;
// Obtain the center of the camera.
CGPoint center = CGPointMake(self.contentSize.width/2, self.contentSize.height/2);
CGPoint worldPoint = [self convertToWorldSpace:center];
CGPoint areaPoint = [_area convertToNodeSpace:worldPoint];
// Now set anchor point to where areaPoint is relative to the whole _area contentsize
float areaLocationX = areaPoint.x / _area.contentSize.width;
float areaLocationY = areaPoint.y / _area.contentSize.height;
[_area moveDebugParticle:areaPoint];
[_area setAnchorPoint:CGPointMake(areaLocationX, areaLocationY)];
if (_area.scale*deltaScale <= ZOOM_RADAR_THRESHOLD)
{
_area.scale = ZOOM_RADAR_THRESHOLD;
}
else if (_area.scale*deltaScale >= ZOOM_MAX)
{
_area.scale = ZOOM_MAX;
}
else
{
// First set the anchor point.
_area.scale *= deltaScale;
}
_prevScale = recognizer.scale;
}
}
If I set the anchor point to .5, .5 and print the calculated anchor point (areaLocationX, areaLocationY) using a CCLog it looks right, but when I set the anchor point to these values the layer scales out of control and entirely leaves the view of the player. The anchor point takes on crazy values like (-80, 10), although generally it should be relatively close to something in the range of 0 to 1 for either coordinate.
What might be causing this kind of behavior?
Ok it looks like I solved it. I was continually moving the anchor point -during- the scaling rather than setting it at the very beginning once. The result was really erratic scaling rather than something smooth and expected. The resolved code looks like this:
- (void) handlePinchFrom:(UIPinchGestureRecognizer*) recognizer
{
if(recognizer.state == UIGestureRecognizerStateEnded)
{
_isScaling = false;
_prevScale = 1.0;
}
else
{
if (!_isScaling)
{
// Obtain the center of the camera.
CGPoint center = CGPointMake(self.contentSize.width/2, self.contentSize.height/2);
CGPoint areaPoint = [_area convertToNodeSpace:center];
// Now set anchor point to where areaPoint is relative to the whole _area contentsize
float anchorLocationX = areaPoint.x / _area.contentSize.width;
float anchorLocationY = areaPoint.y / _area.contentSize.height;
[_area moveDebugParticle:areaPoint];
[_area setAnchorPoint:CGPointMake(anchorLocationX, anchorLocationY)];
CCLOG(#"Anchor Point: (%f, %f)", anchorLocationX, anchorLocationY);
}
_isScaling = true;
float deltaScale = 1.0 - _prevScale + recognizer.scale;
if (_area.scale*deltaScale <= ZOOM_RADAR_THRESHOLD)
{
_area.scale = ZOOM_RADAR_THRESHOLD;
}
else if (_area.scale*deltaScale >= ZOOM_MAX)
{
_area.scale = ZOOM_MAX;
}
else
{
_area.scale *= deltaScale;
}
_prevScale = recognizer.scale;
}
}

Overlap and Swapping Images in iOS

I want to swap two images. When I move the drag image and it overlaps 50% or greater on any other image in the view it must be swapped. The problem is how can I check the drag image is 50% or more than 50% overlap with the other image. Please help and suggest the logic with a code example.
The code I am trying is :
if (imgVw.tag != self.tag) {
CGRect imgVwRect = CGRectMake(imgVw.frame.origin.x +(imgVw.frame.size.width/2), imgVw.frame.origin.y+(imgVw.frame.size.height/2), imgVw.frame.size.width, imgVw.frame.size.height);
CGRect movingImgRect = CGRectMake(newCenter.x+self.frame.size.width, newCenter.y+self.frame.size.height, self.frame.size.width, self.frame.size.height);
if (movingImgRect.origin.x >= imgVwRect.origin.x && movingImgRect.origin.y >= imgVwRect.origin.y)
{
NSLog(#"img view tag %lu",imgVw.tag);
UIImage *tempImg = self.image;
[self setImage:imgVw.image];
[imgVw setImage:tempImg];
}
}
This is pretty straightforward.
Use the method CGRectIntersection to calculate the intersection of the 2 rects.
Then compare the area of the intersection with the area of the moving rect. Something like this (assuming I understand your code
if (imgVw.tag != self.tag)
{
CGRect imgVwRect = CGRectMake(imgVw.frame.origin.x +(imgVw.frame.size.width/2),
imgVw.frame.origin.y+(imgVw.frame.size.height/2),
imgVw.frame.size.width,
imgVw.frame.size.height);
CGRect movingImgRect = CGRectMake(newCenter.x+self.frame.size.width,
newCenter.y+self.frame.size.height,
self.frame.size.width,
self.frame.size.height);
//Figure out the intersection rect of the 2 rectangles
CGRect intersectionRect = CGRectIntersection(imgVwRect, movingImgRect);
//Find the area of the intersection
CGFloat xArea = intersectionRect.size.height * intersectionRect.size.width;
//Find the area of the moving image
//(this code could be done once and saved in an iVar)
CGFloat movingImgArea = movingImgRect.size.height * movingImgRect.size.width;
//Is the intersection >= 1/2 the size of the moving image?
if (xArea*2 >= movingImgArea)
{
//Do whatever you need to do when the 2 images overlap by >= 50%
}
}

How do I position my UIScrollView's image properly when switching orientation?

I'm having a lot of trouble figuring out how best to reposition my UIScrollView's image view (I have a gallery kind of app going right now, similar to Photos.app, specifically when you're viewing a single image) when the orientation switches from portrait to landscape or vice-versa.
I know my best bet is to manipulate the contentOffset property, but I'm not sure what it should be changed to.
I've played around a lot, and it seems like for whatever reason 128 works really well. In my viewWillLayoutSubviews method for my view controller I have:
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.x >= 128) {
newContentOffset.x -= 128.0;
}
else {
newContentOffset.x = 0.0;
}
newContentOffset.y += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
else {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.y >= 128) {
newContentOffset.y -= 128.0;
}
else {
newContentOffset.y = 0.0;
}
newContentOffset.x += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
And it works pretty well. I hate how it's using a magic number though, and I have no idea where this would come from.
Also, whenever I zoom the image I have it set to stay centred (just like Photos.app does):
- (void)centerScrollViewContent {
// Keep image view centered as user zooms
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(self.scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(self.scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(self.scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(self.scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
}
So I need it to keep it positioned properly so it doesn't show black borders around the image when repositioned. (That's what the checks in the first block of code are for.)
Basically, I'm curious how to implement functionality like in Photos.app, where on rotate the scrollview intelligently repositions the content so that the middle of the visible content before the rotation is the same post-rotation, so it feels continuous.
You should change the UIScrollView's contentOffset property whenever the scrollView is layouting its subviews after its bounds value has been changed. Then when the interface orientation will be changed, UIScrollView's bounds will be changed accordingly updating the contentOffset.
To make things "right" you should subclass UIScrollView and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
The contentOffset calculation function should be placed inside UIScrollView's layoutSubviews method. The problem is that this method is called not only when the bounds value is changed but also when srollView is zoomed or scrolled. So the bounds value should be tracked to hint if the layoutSubviews method is called due to a change in bounds as a consequence of the orientation change, or due to a pan or pinch gesture.
So the first part of the UIScrollView subclass should look like this:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews method is called every time the UIScrollView is panned, zoomed or its bounds are changed. The _centerScrollViewContent method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView class and therefore instead of using self.imageView property to reference the zoomed view, which may not be available in the context of UIScrollView class, the viewForZoomingInScrollView: delegate method is used.
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset method. This method is responsible for adjusting the contentOffset. Such that when UIScrollView's bounds value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView's bounds is changed (e.g.: orientation change).
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView class (here is link to GitHub) implementing the above behavior and additional bonuses:
You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
In addition to adjusting contentOffset you may find that you also want to adjust the scrollView's zoomScale. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.

Resources