I am using SVGKit to draw a map, and it basically use SVG data to form a UIBezierPath, and pass it to CAShapeLayer and render it.
important code:
for (FSSVGPathElement* path in _svg.paths) {
// Make the map fits inside the frame
float scaleHorizontal = self.frame.size.width / _svg.bounds.size.width;
float scaleVertical = self.frame.size.height / _svg.bounds.size.height;
float scale = MIN(scaleHorizontal, scaleVertical);
CGAffineTransform scaleTransform = CGAffineTransformIdentity;
scaleTransform = CGAffineTransformMakeScale(scale, scale);
scaleTransform = CGAffineTransformTranslate(scaleTransform,-_svg.bounds.origin.x, -_svg.bounds.origin.y);
UIBezierPath* scaled = [path.path copy];
[scaled applyTransform:scaleTransform];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = scaled.CGPath;
[self.layer addSublayer:shapeLayer];
[_scaledPaths addObject:scaled];
}
The mapView's frame is set as the screen's width and height, and I found the map view's height is less than the screen height. So I want to cut the mapView's height to just same as the actual height of the map (UIBezierPah)
Because I don't know where to read the largest Y data for the UIBezierPath, I don't know how to calculate the max Y.
Can somebody help?
There is a bounds property for UIBezierPath, but what I am asking is the actual size of all the UIBeizerPath together.
I figured out with below code:
first add a property as the actual size,
#property (nonatomic, assign) CGSize actualSize;
and When handling the UIBezierPath:
for (FSSVGPathElement* path in _svg.paths) {
UIBezierPath* scaled = [path.path copy];
[scaled applyTransform:scaleTransform];
CGFloat absoluteWidth = scaled.bounds.origin.x + scaled.bounds.size.width;
CGFloat absoluteHeight = scaled.bounds.origin.y + scaled.bounds.size.height;
if (absoluteHeight > maxHeight)
maxHeight = absoluteHeight;
if (absoluteWidth > maxWidth)
maxWidth = absoluteWidth;
....
}
self.actualSize = CGSizeMake(ceil(maxWidth), ceil(maxHeight));
Now I can get the actual size after all paths are added into the CAShapeLayer.
Related
I need to create a UICollectionView like the following picture.
I have been able to make horizontally scrollable, but unable to make the UI like the picture. Any help? Thanks.
Here is what you wanted,you just need to have a custom UICollectionViewFlowLayout,and override the method -(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect to change the cell display attributes.
Here is the layout code with comment in each main operation
#implementation HorizonSclaeLayout
-(instancetype)init{
self = [super init];
if (self) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
}
return self;
}
static const CGFloat kMaxDistancePercentage = 0.3f;
static const CGFloat kMaxRotation = 0;//(CGFloat)(70.0 * (M_PI / 180.0));
static const CGFloat kMaxZoom = 0.6f;
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
//1 the visible rectangle of the collection view, calculated using the content offset of the view and the bounds size.
CGRect visibleRect = (CGRect){
.origin = self.collectionView.contentOffset,
.size = self.collectionView.bounds.size
};
//2 the maximum distance away from the center, which defines the distance from the center at which each cell is fully rotated
CGFloat maxDistance = visibleRect.size.width/2;
NSArray *array = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *attributes in array) {
//3 find the distance of the cell from the center of the current visible rectangle
CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
//4 normalize this distance against the maximum distance to give a percentage of how far the view is along the line from the center to the maximum points in either direction
CGFloat normalizedDistance = distance / maxDistance;
normalizedDistance = MIN(normalizedDistance, 1.0f);
normalizedDistance = MAX(normalizedDistance, -1.0f);
//5 calculate the rotation and zoom
CGFloat rotation = normalizedDistance * kMaxRotation;
CGFloat zoom = 1.0f + ((1.0f - ABS(normalizedDistance))*kMaxZoom);
//6 create the required transform by first setting m34 so that when the rotation is done
// skew is applied to make it have the appearance of coming out of and going into the screen
CATransform3D transform = CATransform3DIdentity;
transform.m34 = - 1.0 / 550.0;
transform = CATransform3DRotate(transform, rotation, 0.0f, 1.0f, 0.0f);
transform = CATransform3DScale(transform, zoom, zoom, 0.0f);
attributes.transform3D = transform;
}
return array;
}
-(CGSize)itemSize{
return CGSizeMake(60, 60 * 1.2);
}
-(CGFloat)minimumLineSpacing{
return 30;
}
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return YES;
}
#end
Here in this sample LineLayout has already implemented the required custom layout (that's what I guessed from the image shown).
I am have one view and I am rotating that view using CABasicAnimation.
Now my problem is that how I get a perfect position of that view while rotating. I have tried many type of codes but i can't got a perfect position during rotation of that view.
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
NSNumber *currentAngle = [CircleView.layer.presentationLayer valueForKeyPath:#"transform.rotation"];
rotationAnimation.fromValue = currentAngle;
rotationAnimation.toValue = #(50*M_PI);
rotationAnimation.duration = 50.0f; // this might be too fast
rotationAnimation.repeatCount = HUGE_VALF; // HUGE_VALF is defined in math.h so import it
[CircleView.layer addAnimation:rotationAnimation forKey:#"rotationAnimationleft"];
I am using this code for rotating my view.
I have also attached a one photo of my view.
Thank you In Advance Please Help If You Know.
To get view's parameters during the animation you should use view.layer.presentationLayer
ADDED:
In order to get the coordinate of top left corner of the view, use the following code:
- (CGPoint)currentTopLeftPointOfTheView:(UIView *)view
{
CGRect rect = view.bounds;
rect.origin.x = view.center.x - 0.5 * CGRectGetWidth(rect);
rect.origin.y = view.center.y - 0.5 * CGRectGetHeight(rect);
CGPoint originalTopLeftCorner = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint rectCenter = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
CGFloat radius = sqrt(pow(rectCenter.x - originalTopLeftCorner.x, 2.0) + pow(rectCenter.y - originalTopLeftCorner.y, 2.0));
CGFloat originalAngle = M_PI - acos((rectCenter.x - originalTopLeftCorner.x) / radius);
CATransform3D currentTransform = ((CALayer *)view.layer.presentationLayer).transform;
CGFloat rotation = atan2(currentTransform.m12, currentTransform.m11);
CGFloat resultAngle = originalAngle - rotation;
CGPoint currentTopLeftCorner = CGPointMake(round(rectCenter.x + cos(resultAngle) * radius), round(rectCenter.y - sin(resultAngle) * radius));
return currentTopLeftCorner;
}
Resulting CGPoint will be the coordinate of the top left corner of your (rotated) view relative to its superview.
Set the position of the layer that you what
[layer setPosition:endpoint];
Or you may also refer this CABasicAnimation rotate returns to original position
Through this question and answer I've now got a working means of detecting when an arbitrarily rotated image isn't completely outside a cropping rect.
The next step is to figure out how to correctly adjust it's containing scroll view zoom to ensure that there are no empty spaces inside the cropping rect. To clarify, I want to enlarge (zoom in) the image; the crop rect should remain un-transformed.
The layout hierarchy looks like this:
containing UIScrollView
UIImageView (this gets arbitrarily rotated)
crop rect overlay view
... where the UIImageView can also be zoomed and panned inside the scrollView.
There are 4 gesture events that occur that need to be accounted for:
Pan gesture (done): accomplished by detecting if it's been panned incorrectly and resets the contentOffset.
Rotation CGAffineTransform
Scroll view zoom
Adjustment of the cropping rect overlay frame
As far as I can tell, I should be able to use the same logic for 2, 3, and 4 to adjust the zoomScale of the scroll view to make the image fit properly.
How do I properly calculate the zoom ratio necessary to make the rotated image fit perfectly inside the crop rect?
To better illustrate what I'm trying to accomplish, here's an example of the incorrect size:
I need to calculate the zoom ratio necessary to make it look like this:
Here's the code I've got so far using Oluseyi's solution below. It works when the rotation angle is minor (e.g. less than 1 radian), but anything over that and it goes really wonky.
CGRect visibleRect = [_scrollView convertRect:_scrollView.bounds toView:_imageView];
CGRect cropRect = _cropRectView.frame;
CGFloat rotationAngle = fabs(self.rotationAngle);
CGFloat a = visibleRect.size.height * sinf(rotationAngle);
CGFloat b = visibleRect.size.width * cosf(rotationAngle);
CGFloat c = visibleRect.size.height * cosf(rotationAngle);
CGFloat d = visibleRect.size.width * sinf(rotationAngle);
CGFloat zoomDiff = MAX(cropRect.size.width / (a + b), cropRect.size.height / (c + d));
CGFloat newZoomScale = (zoomDiff > 1) ? zoomDiff : 1.0 / zoomDiff;
[UIView animateWithDuration:0.2
delay:0.05
options:NO
animations:^{
[self centerToCropRect:[self convertRect:cropRect toView:self.zoomingView]];
_scrollView.zoomScale = _scrollView.zoomScale * newZoomScale;
} completion:^(BOOL finished) {
if (![self rotatedView:_imageView containsViewCompletely:_cropRectView])
{
// Damn, it's still broken - this happens a lot
}
else
{
// Woo! Fixed
}
_didDetectBadRotation = NO;
}];
Note I'm using AutoLayout which makes frames and bounds goofy.
Assume your image rectangle (blue in the diagram) and crop rectangle (red) have the same aspect ratio and center. When rotated, the image rectangle now has a bounding rectangle (green) which is what you want your crop scaled to (effectively, by scaling down the image).
To scale effectively, you need to know the dimensions of the new bounding rectangle and use a scale factor that fits the crop rect into it. The dimensions of the bounding rectangle are rather obviously
(a + b) x (c + d)
Notice that each segment a, b, c, d is either the adjacent or opposite side of a right triangle formed by the bounding rect and the rotated image rect.
a = image_rect_height * sin(rotation_angle)
b = image_rect_width * cos(rotation_angle)
c = image_rect_width * sin(rotation_angle)
d = image_rect_height * cos(rotation_angle)
Your scale factor is simply
MAX(crop_rect_width / (a + b), crop_rect_height / (c + d))
Here's a reference diagram:
Fill frame of overlay rect:
For a square crop you need to know new bounds of the rotated image which will fill the crop view.
Let's take a look at the reference diagram:
You need to find the altitude of a right triangle (the image number 2). Both altitudes are equal.
CGFloat sinAlpha = sin(alpha);
CGFloat cosAlpha = cos(alpha);
CGFloat hypotenuse = /* calculate */;
CGFloat altitude = hypotenuse * sinAlpha * cosAlpha;
Then you need to calculate the new width for the rotated image and the desired scale factor as follows:
CGFloat newWidth = previousWidth + altitude * 2;
CGFloat scale = newWidth / previousWidth;
I have implemented this method here.
I will answer using sample code, but basically this problem becomes really easy, if you will think in rotated view coordinate system.
UIView* container = [[UIView alloc] initWithFrame:CGRectMake(80, 200, 100, 100)];
container.backgroundColor = [UIColor blueColor];
UIView* content2 = [[UIView alloc] initWithFrame:CGRectMake(-50, -50, 150, 150)];
content2.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.5];
[container addSubview:content2];
[self.view setBackgroundColor:[UIColor blackColor]];
[self.view addSubview:container];
[container.layer setSublayerTransform:CATransform3DMakeRotation(M_PI / 8.0, 0, 0, 1)];
//And now the calculations
CGRect containerFrameInContentCoordinates = [content2 convertRect:container.bounds fromView:container];
CGRect unionBounds = CGRectUnion(content2.bounds, containerFrameInContentCoordinates);
CGFloat midX = CGRectGetMidX(content2.bounds);
CGFloat midY = CGRectGetMidY(content2.bounds);
CGFloat scaleX1 = (-1 * CGRectGetMinX(unionBounds) + midX) / midX;
CGFloat scaleX2 = (CGRectGetMaxX(unionBounds) - midX) / midX;
CGFloat scaleY1 = (-1 * CGRectGetMinY(unionBounds) + midY) / midY;
CGFloat scaleY2 = (CGRectGetMaxY(unionBounds) - midY) / midY;
CGFloat scaleX = MAX(scaleX1, scaleX2);
CGFloat scaleY = MAX(scaleY1, scaleY2);
CGFloat scale = MAX(scaleX, scaleY);
content2.transform = CGAffineTransformScale(content2.transform, scale, scale);
I want to label every part of the following curve like in the figure. The label origin should start from the middle of the curve segment see the graphic. I think that something is not quite right in my logic. Could you assist where is the error in my code? I think that the code and the figure explain itself.
Thanks for your time and help!
CGFloat radius = self.height / 3.2f; // radius of the curve
CGPoint center = CGPointMake(self.width / 2.f, self.height / 2.f); //center of the curve
UIBezierPath* circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:degreesToRadian(270.f) endAngle:DEGREES_TO_RADIANS(269.999f) clockwise:YES]; // go around like the clock
CGFloat strokeStart = 0.f;
// Add the parts of the curve to the view
for (int i = 0; i < segmentValues.count; i++) {
NSNumber *value = [segmentValues objectAtIndex:i];
CGFloat strokeEnd = [value floatValue] / kPercentHundred;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = circlePath.CGPath;
circle.lineCap = kCALineCapButt;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [[colors objectAtIndex:i] CGColor];
circle.lineWidth = kWidhtLine;
circle.zPosition = 1.f;
circle.strokeStart = strokeStart;
circle.strokeEnd = strokeStart + strokeEnd;
[self.layer addSublayer:circle];
UIView *example = [[UIView alloc] init];
example.backgroundColor = kColorRed;
example.size = CGSizeMake(10, 10);
// middle = "(circle.strokeStart + circle.strokeEnd) / 2.f"
// * convert percent to degree
CGFloat angle = (circle.strokeStart + circle.strokeEnd) / 2.f * 360.f;
// determine the point and add the right offset
example.origin = CGPointMake(cosf(angle) * radius + center.x, sinf(angle) * radius + center.y);
[self addSubview:example];
strokeStart = circle.strokeEnd;
}
What I get see the figure.
What I expect in concept is something similar to the following figure.
There are two key issues:
You calculate the angle via:
CGFloat angle = (circle.strokeStart + circle.strokeEnd) / 2.f * 360.f;
You then proceed to use that for the cos and sin functions. Those expect radian values, though, so you have to convert this to radians before you use it in those functions. Thus, yielding:
CGFloat angle = degreesToRadian((circle.strokeStart + circle.strokeEnd) / 2.f * 360.f);
(BTW, I don't know if you prefer degreesToRadian or DEGREES_TO_RADIANS, but you should probably use one or the other.)
Or, more simply, you can convert to radians directly:
CGFloat angle = (circle.strokeStart + circle.strokeEnd) / 2.f * M_PI * 2.0;
You have rotated the circle 90 degrees (presumably so that it would start a "12 o'clock" rather than "3 o'clock"). Well, if you rotate the circle, then you have to rotate angle when you calculate where to put your example view, as well:
example.center = CGPointMake(cosf(angle - M_PI_2) * radius + center.x, sinf(angle - M_PI_2) * radius + center.y);
This yields:
I'm not sure what your goal is. You say you want to create labels, but what you create in your code is generic UIView objects.
Then you are setting a property "origin" on the view, but UIView does not have an origin as far as I know. It has a frame, which has an origin component, but not an origin property. I'm guessing that the code you posted is not actual, running code.
Do you want the labels to be centered on top of the circle? If so, you should both set the text alignment to NSTextAlignmentCenter, and set the center property of the views rather than the frame.origin.
I have a working sample project on github that creates an analog clock and puts labels for the hours around it. You can look at the code for an idea of how to do it:
Analog clock sample project
(The project also shows how to use the new spring physics UIView animation method.)
hmm it i a little late for me ;)
I think you have to use UIView::setCenter this will solve you problem. Then the center of the view is on your circle.
Edit: And it is easier to manipulate the position.
I have two sliders - one for changing image size and one for rotating this image. My imageview is 60x60. The problem is that I rotate the image using CGAffineTransformMakeRotation, but when I try to resize it after that (like, from 60x60 to 65x65 using the slider), it acts weirdly - the frame of the image view has changed like 80x2. How can I avoid this? Here is my code for the slider that resizes the image:
-(IBAction)imageSliderAction:(UISlider *)sender
{
NSUInteger value = sender.value;
float oldCenterX = logoImageView.center.x;
float oldCenterY = logoImageView.center.y;
newWidth = value;
newHeight = value;
CGRect frame = [logoImageView frame];
frame.size.width = newWidth;
frame.size.height = newHeight;
[logoImageView setFrame:frame];
logoImageView.center = CGPointMake(oldCenterX, oldCenterY);
}
And here is the code for my rotating slider:
-(IBAction)rotationSliderAction:(UISlider *)sender
{
NSUInteger angle = sender.value;
if (sender.value >= 1)
{
CGAffineTransform rotate = CGAffineTransformMakeRotation(angle / 180.0 * 3.14);
[logoImageView setTransform:rotate];
}
if (sender.value <= 0 )
{
CGAffineTransform rotate = CGAffineTransformMakeRotation( (360 + sender.value) / 180.0 * 3.14);
[logoImageView setTransform:rotate];
}
}
How can I avoid autochanging frame's width and height when rotating? Because after that I can't resize the image correctly.
From UIView reference
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
If you want to change the size of view that has nontrivial transform you should do that by changing its bounds property (view's center will remain the same so you won't need any extra logic to maintain its position):
[logoImageView setBounds:CGRectMake(0,0,sender.value, sender.value)];