Is a Right-to-Left progress bar possible on iOS? - cocoa-touch

I've tried sending [UIProgressView setProgress] negative values, and that doesn't work.
Is there some other way to get a progress bar that fills from the right-hand end?

You could try setting the transform property of your UIProgressView to a new CGAffineTransform that rotates the view by 180 degrees and flips it vertically (to preserve the "shininess") (see CGAffineTransformMake() and CGAffineTransformRotate()).
Something along the lines of:
UIProgressView *pv = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar];
pv.frame = CGRectMake(10, 100, 300, 11);
CGAffineTransform transform = CGAffineTransformMake(1, 0, 0, -1, 0, pv.frame.size.height); // Flip view vertically
transform = CGAffineTransformRotate(transform, M_PI); //Rotation angle is in radians
pv.transform = transform;
pv.progress = 0.5;

You can rotate the UIProgressView:
progressView.transform = CGAffineTransformMakeRotation(DegreesToRadians(180));
where DegreesToRadians is:
#define DegreesToRadians(d) ((d) * M_PI / 180.0)
To change the progress value, use positive numbers.

A simpler version is to flip it horizontally.
progressView.transform = CGAffineTransformMakeScale(-1.0f, 1.0f);

In iOS 9+, you can use semanticContentAttribute:
progressView.semanticContentAttribute = .forceRightToLeft

You can rotate the view by 180°:
progressView.transform = CGAffineTransformMakeRotation(-M_PI);

Swift answer:
progressView.transform = CGAffineTransform(rotationAngle: .pi)

In iOS 7 with storyboards, you can set the Progress Tint to the Track Tint and vice versa, then subtract the regular value from one and set that to the current progress. Probably better to do it the other (rotation) way, but I thought I would throw this out there.

Swift 5 Version
progressView.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)

Related

Objective-c CATransform3DMakeRotation Clockwise/Counterclockwise

I'm rotating an UIView along the x axis using CATransform3DMakeRotation using the below code:
float radians = DegreesToRadians(30);
[UIView animateWithDuration:0.15 animations:^{
self.moveControlView.layer.transform = CATransform3DMakeRotation(radians,1,0,0);
}];
The rotation is applied always in the same versus (clockwise). I want to rotate the UIView in the opposite versus.
In order to achieve my goal I've tried:
Set a negative angle (-30)
Set the angle to 330
But the versus of the orientation doesn't change.
I have also try to set x = -1 leaving the angle positive.
Any suggestion?
You should apply a perspective to your transform, as it's explained in the similar question:
CATransform3D perspectiveTransform = CATransform3DIdentity;
perspectiveTransform.m34 = 1.0 / -500;
self.moveControlView.layer.transform =
CATransform3DRotate(perspectiveTransform, radians, 1.0f, 0.0f, 0.0f);;

Interpolating CGAffineTransform

I am animating a UIView using its CALayer's affineTransform property. The transform I am animating to is built this way : CGAffineTransformTranslate(CGAffineTransformScale(zoomContainer.layer.transform, scaleX, scaleY), translateX, translateY).
Now I want to improve this animation and make it interactive. Thus I need to interpolate the CGAffineTransform using a completion percentage. However, I can't seem to find the way the system interpolates the transformation when animating it for me. I always end up with weird curves where the translation isn't synchronized with the scaling. Here's the code I currently have :
CGFloat completion = fmax(0, fmin(completionPercentage, 1));
CGFloat scaleX = CGRectGetWidth(zoomContainerInitialBounds) / CGRectGetWidth(zoomTargetInitialFrame);
CGFloat scaleY = CGRectGetHeight(zoomContainerInitialBounds) / CGRectGetHeight(zoomTargetInitialFrame);
scaleX = 1 + ((scaleX - 1) * (1 - completion));
scaleY = 1 + ((scaleY - 1) * (1 - completion));
CGFloat translateX = CGRectGetMidX(zoomContainerInitialBounds) - CGRectGetMidX(zoomTargetInitialFrame);
CGFloat translateY = CGRectGetMidY(zoomContainerInitialBounds) - CGRectGetMidY(zoomTargetInitialFrame);
translateX *= (1 - completion);
translateY *= (1 - completion);
zoomContainer.layer.affineTransform = CGAffineTransformTranslate(CGAffineTransformScale(initialTransform, scaleX, scaleY), translateX, translateY);
What do I need to change to interpolate the transform the same way UIKit does it for me?
If you want to be able to interact with the animation (maybe using a gesture or a slider or some other mechanism) then you can use a little trick where you "pause" the animation by setting the speed of the view's layer to 0 and then move the animation to a specific point in time by setting the timeOffset of the layer.
I have an explanation of this in an answer to a similar (but animating a different property) question and I a more detailed explanation in the context of animation timing in a blog post.
So that this isn't just a link answer, this is the very little code that you would need to do this interaction. I'm assuming a slider in this case. The blog post shows how to use it with scroll events and if you want to do it with gestures then I'm sure you can figure it out :)
In you animation setup (note that I'm using the CATransform3D instead of CGAffineTransform)
CABasicAnimation *yourAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
yourAnimation.duration = 1.; // For convenience (so that timeOffset is from 0.0 to 1.0)
yourAnimation.fromValue = [NSValue valueWithCATransform3D:fromTransform];
yourAnimation.toValue = [NSValue valueWithCATransform3D:toTransform];
[self.yourView.layer addAnimation: yourAnimation forKey:#"Your Animation"];
self.yourView.layer.speed = 0.0; // Pause every animation on that layer
And the slider to drive the interaction of the animation (slider is configured to go from 0.0 to 1.0):
- (IBAction)sliderChanged:(UISlider *)sender {
self.yourView.layer.timeOffset = sender.value; // Change the "current time" of the animation
}

Rotation of UIImageView does not conserve center

Just setting a rotation transform to an UIImageView does not keep the center still. The image translate around de center, I'm puzzled. Any idea?
CGAffineTransform
transform = CGAffineTransformIdentity;
transform = CGAffineTransformRotate(transform, self.filter.angle);
self.sourceImageView.transform = transform;
Note: the angle is coupled to a UISlider so that the user changes its value from -90 to 90.
I found that setting the transformation to the layer fixes the issue.
self.sourceImageView.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale, scale, 1.), angle, 0, 0, -1);

Why does My Rotation only go Anti Clockwise when I want to go Clockwise?

[CATransaction begin];
[CATransaction setAnimationDuration:5];
CGAffineTransform currentTransform = squareLayer.affineTransform;
CGFloat angle = M_PI;
squareLayer.affineTransform = CGAffineTransformRotate(currentTransform, angle);
[CATransaction commit];
and
[CATransaction begin];
[CATransaction setAnimationDuration:5];
CGAffineTransform currentTransform = squareLayer.affineTransform;
CGFloat angle = (M_PI * -1);
squareLayer.affineTransform = CGAffineTransformRotate(currentTransform, angle);
[CATransaction commit];
I would have thought the -1 would have reversed the direction but apparently not?
The angle is the same. To go full circle you need 2 * Pi, so half a circle is Pi. If you take -Pi it would end at the same point.
EDIT: If you need to make it turn clockwise you can go slightly off -Pi. Instead of -M_PI use -3.141593 for example.
Just ran into this and while #radicalraid's solution works fine, I prefer adjusting the starting angle before rotation vs rotating to an angle less than the desired end point. Also, you can animate CALayer transforms using animation blocks.
My code:
// iOS will rotate the shortest distance to the end angle,
// using counter-clockwise if equal.
// Force the rotation to open clockwise and close counter-clockwise by bumping
// the initial angle
CGFloat startRadians, endRadians;
if (isOpening) {
startRadians = 0.01f;
endRadians = M_PI;
}
else {
startRadians = M_PI - 0.01f;
endRadians = 0.0f;
}
self.myImageView.layer.affineTransform = CGAffineTransformMakeRotation(startRadians);
[UIView animateWithDuration:0.3f
animations:^{
self.myImageView.layer.affineTransform = CGAffineTransformMakeRotation(endRadians);
}];
My experience with Core animatinon does not agree with that Apple quote. What I've found is that the only thing that matters is the final angle value. Positive pi and negative pi rotations result in the same transformation matrix, so the system just goes counter-clockwize regardless.
What I've done to go rotations in a specific direction, or rotations of a full 360 degrees, is to create an explicit CABasicAnimation using a type like transform.rotation.z, with the fill mode set to kCAFillModeForwards, and only provide a toValue. That causes the animation to start from the previous value. You can then set up a rotation that is an even fraction of your total desired rotation, and give it a repeat count.
For example, the code below creates an animation that rotates 5 quarter turns, or 450 degrees. If you wanted to do 180 degrees clockwise, you could a rotation of pi/2 with a repeat count of 2. For counterclockwise, use -pi/2 with a repeat count of 2.
CABasicAnimation* rotate = [CABasicAnimation animationWithKeyPath:
#"transform.rotation.z"];
rotate.removedOnCompletion = FALSE;
rotate.fillMode = kCAFillModeForwards;
//Do a series of 5 quarter turns for a total of a 1.25 turns
//(2PI is a full turn, so pi/2 is a quarter turn)
[rotate setToValue: [NSNumber numberWithFloat: -M_PI / 2]];
rotate.repeatCount = 5;
rotate.duration = duration/rotate.repeatCount * 2 ;
rotate.beginTime = start;
rotate.cumulative = TRUE;
rotate.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
EDIT: Instead of -M_PI use -3 * M_PI, for anti-clockwise
and remove '-' sign for opposite direction.
Take a look at this previous question: Force clockwise/anticlockwise rotation for a CABasicAnimation of a UIImageView the code is a bit different but it does work.
Here's a quote from the apple documentation "In iOS, a positive value specifies counterclockwise rotation and a negative value specifies clockwise rotation." Have you tried doing this:
squareLayer.affineTransform = CGAffineTransformRotate(currentTransform, -M_PI);
Thats what I have used to do all my clockwise rotation in the past.
This is a function I've used to make it always rotate clockwise if endAngle > startAngle and counterClockwise if startAngle > endAngle. (e.g. the animation should never "cross the 2*pi line")
private func rotate(view: UIView, from startAngle: CGFloat, to endAngle: CGFloat) {
let midAngle = (startAngle.truncatingRemainder(dividingBy: 2 * .pi) + endAngle.truncatingRemainder(dividingBy: 2 * .pi)) / 2
UIView.animateKeyframes(withDuration: UIView.inheritedAnimationDuration, delay: 0, options: .calculationModeCubicPaced, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0) {
view.transform = CGAffineTransform(rotationAngle: midAngle)
}
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0) {
view.transform = CGAffineTransform(rotationAngle: endAngle)
}
}, completion: nil)
}
You can call this from inside another UIView.animate block to animate it with other things as well. For example:
UIView.animate(withDuration: 1) {
rotate(view: view, from: .pi * 0.25, to: 1.5 * .pi)
otherView.alpha = 1
}
It's important to use the .calculationModeCubicPaced option otherwise you have to manually specify the relative start time and duration
Agree with Duncan and XJones. Final value is all that seems to matter. CA will then find the shortest path.
Using XJones's idea. This seemed to work for me:
private let angleRadians = CGFloat(M_PI)
private let angleRadiansOffset = CGFloat(0.1 * M_PI/180.0)
let turnAngle = angleRadians - angleRadiansOffset;
//pre-rotate by an 'invisible amount', to force rotation in the same direction every time
primView.layer.transform = CATransform3DRotate(primView.layer.transform, angleRadiansOffset, 0.0, 1.0, 0.0)
UIView.animateKeyframesWithDuration(duration, delay: 0, options: [.BeginFromCurrentState], animations: { () -> Void in
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.8, animations: { () -> Void in
//now we will complete the rotation to the correct end value
primView.layer.transform = CATransform3DRotate(primView.layer.transform, turnAngle, 0.0, 1.0, 0.0)
})
//... other code ....
}) { (finished) -> Void in
}

Core Animation and Transformation like zoomRectToVisible

I'm trying to get an effect like the zoomRectToVisible-method of UIScrollview.
But my method should be able to center the particular rect in the layer while zooming and it should be able to re-adjust after the device orientation changed.
I'm trying to write a software like the marvel-comic app and need a view that presents each panel in a page.
For my implementation I'm using CALayer and Core Animation to get the desired effect with CATransform3D-transformations. My problem is, I'm not able to get the zoomed rect/panel centered.
the structure of my implementation looks like this: I have a subclass of UIScrollview with a UIView added as subview. The UIView contains the image/page in it's CALayer.contents and I use core animations to get the zooming and centering effect. The zoom effect on each panel works correcty but the centering is off. I'm not able to compute the correct translate-transformation for centering.
My code for the implementation of the effect is like this:
- (void) zoomToRect:(CGRect)rect animated:(BOOL)animated {
CGSize scrollViewSize = self.bounds.size;
// get the current panel boundingbox
CGRect panelboundingBox = CGPathGetBoundingBox([comicPage panelAtIndex:currentPanel]);
// compute zoomfactor depending on the longer dimension of the panelboundingBox size
CGFloat zoomFactor = (panelboundingBox.size.height > panelboundingBox.size.width) ? scrollViewSize.height/panelboundingBox.size.height : scrollViewSize.width/panelboundingBox.size.width;
CGFloat translateX = scrollViewSize.width/2 - (panelboundingBox.origin.x/2 + panelboundingBox.size.width/2);
CGFloat translateY = scrollViewSize.height/2 - (panelboundingBox.size.height/2 - panelboundingBox.origin.y);
// move anchorPoint to panelboundingBox center
CGPoint anchor = CGPointMake(1/contentViewLayer.bounds.size.width * (panelboundingBox.origin.x + panelboundingBox.size.width/2), 1/contentViewLayer.bounds.size.height * (contentViewLayer.bounds.size.height - (panelboundingBox.origin.y + panelboundingBox.size.height/2)));
// create the nessesary transformations
CATransform3D translateMatrix = CATransform3DMakeTranslation(translateX, -translateY, 1);
CATransform3D scaleMatrix = CATransform3DMakeScale(zoomFactor, zoomFactor, 1);
// create respective core animation for transformation
CABasicAnimation *zoomAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
zoomAnimation.fromValue = (id) [NSValue valueWithCATransform3D:contentViewLayer.transform];
zoomAnimation.toValue = (id) [NSValue valueWithCATransform3D:CATransform3DConcat(scaleMatrix, translateMatrix)];
zoomAnimation.removedOnCompletion = YES;
zoomAnimation.duration = duration;
// create respective core animation for anchorpoint movement
CABasicAnimation *anchorAnimatione = [CABasicAnimation animationWithKeyPath:#"anchorPoint"];
anchorAnimatione.fromValue = (id)[NSValue valueWithCGPoint:contentViewLayer.anchorPoint];
anchorAnimatione.toValue = (id) [NSValue valueWithCGPoint:anchor];
anchorAnimatione.removedOnCompletion = YES;
anchorAnimatione.duration = duration;
// put them into an animation group
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:zoomAnimation, anchorAnimatione, nil] ;
/////////////
NSLog(#"scrollViewBounds (w = %f, h = %f)", self.frame.size.width, self.frame.size.height);
NSLog(#"panelBounds (x = %f, y = %f, w = %f, h = %f)", panelboundingBox.origin.x, panelboundingBox.origin.y, panelboundingBox.size.width, panelboundingBox.size.height);
NSLog(#"zoomfactor: %f", zoomFactor);
NSLog(#"translateX: %f, translateY: %f", translateX, translateY);
NSLog(#"anchorPoint (x = %f, y = %f)", anchor.x, anchor.y);
/////////////
// add animation group to layer
[contentViewLayer addAnimation:group forKey:#"zoomAnimation"];
// trigger respective animations
contentViewLayer.anchorPoint = anchor;
contentViewLayer.transform = CATransform3DConcat(scaleMatrix, translateMatrix);
}
So the view requires the following points:
it should be able to zoom and center a rect/panel of the layer/view depending on the current device orientation. (zoomRectToVisible of UIScrollview does not center the rect)
if nessesary (either device orientation changed or panel requires rotation) the zoomed panel/rect should be able to rotate
the duration of the animation is depending on user preference. (I don't know whether I can change the default animation duration of zoomRectToVisible of UIScrollView ?)
Those points are the reason why I overwrite the zoomRectToVisible-method of UIScrollView.
So I have to know how I can correctly compute the translation parameters for the transformation.
I hope someone can guide me to get the correct parameters.
Just skimmed over your code and this line is probably not being calculated as you think:
CGPoint anchor = CGPointMake(1/contentViewLayer.bounds.size.width * (panelboundingBox.origin.x + panelboundingBox.size.width/2), 1/contentViewLayer.bounds.size.height * (contentViewLayer.bounds.size.height - (panelboundingBox.origin.y + panelboundingBox.size.height/2)));
You're likely to get 0 because of the 1/ at the start. C will do your multiplication before this division, resulting in values <1 - probably not what you're after. See this
You might find it more useful to breakdown your calculation so you know it's working in the right order (just use some temporary variables) - believe me it will help enormously in making your code easier to read (and debug) later. Or you could just use more brackets...
Hope this helps.