iOS Animate/Morph Shape from Circle to Square

iOS animate/morph shape from circle to square

After playing with it for a little while in the playground I noticed that each arc adds two segments to the bezier path, not just one. Moreover, calling closePath adds two more. So to keep the number of segments consistent, I ended up with adding fake segments to my square path. The code is in Swift, but I think it doesn't matter.

func circlePathWithCenter(center: CGPoint, radius: CGFloat) -> UIBezierPath {
let circlePath = UIBezierPath()
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI), endAngle: -CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI/2), endAngle: 0, clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: CGFloat(M_PI/2), endAngle: CGFloat(M_PI), clockwise: true)
circlePath.closePath()
return circlePath
}

func squarePathWithCenter(center: CGPoint, side: CGFloat) -> UIBezierPath {
let squarePath = UIBezierPath()
let startX = center.x - side / 2
let startY = center.y - side / 2
squarePath.moveToPoint(CGPoint(x: startX, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.closePath()
return squarePath
}

enter image description here

UIBezierPath animation from circle to rounded square

You can simply have two paths

    func circlePath () -> UIBezierPath {
return UIBezierPath(roundedRect: CGRect(x:0, y:0, width:50, height:50), cornerRadius: 25)
}

func squarePath () -> UIBezierPath {
return UIBezierPath(roundedRect: CGRect(x:0, y:0, width:30, height:30), cornerRadius: 4)
}

How CAShapelayer transform from circle to square (or vice versa) with animation?

After doing some work around, i have ended with this

enter image description here

First make global object of UIBezierPath for square and circle path and also for CAShapeLayer object.

@implementation Demo{
CAShapeLayer *shapeLayer;
UIBezierPath *sqPath;
UIBezierPath *crclePath;
}

Now, customize your shapeLayer and add to layer like below in viewDidLoad.

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [[UIColor redColor] CGColor];
shapeLayer.lineWidth = 2;
shapeLayer.lineJoin = kCALineCapRound;
sqPath = [self makeSquare:self.view.center squareWidth:200];
shapeLayer.path = [sqPath CGPath];
crclePath = [self makeCircle:self.view.center radius:100];
[self.view.layer addSublayer:shapeLayer];
}

Below is method that will make perfect square

-(UIBezierPath*)makeSquare:(CGPoint)centrePoint squareWidth:(float)gain{
float x=centrePoint.x-(gain/2);
float y=centrePoint.y-(gain/2);
UIBezierPath *apath = [UIBezierPath bezierPath];
[apath moveToPoint:CGPointMake(x,y)];
[apath addLineToPoint:apath.currentPoint];
[apath addLineToPoint:CGPointMake(x+gain,y)];
[apath addLineToPoint:apath.currentPoint];
[apath addLineToPoint:CGPointMake(x+gain,y+gain)];
[apath addLineToPoint:apath.currentPoint];
[apath addLineToPoint:CGPointMake(x,y+gain)];
[apath addLineToPoint:apath.currentPoint];
[apath closePath];
return apath;
}

Below is method helps to make circle

- (UIBezierPath *)makeCircle:(CGPoint)center radius:(CGFloat)radius
{
UIBezierPath *circlePath = [UIBezierPath bezierPath];
[circlePath addArcWithCenter:center radius:radius startAngle:-M_PI endAngle:-M_PI/2 clockwise:YES];
[circlePath addArcWithCenter:center radius:radius startAngle:-M_PI/2 endAngle:0 clockwise:YES];
[circlePath addArcWithCenter:center radius:radius startAngle:0 endAngle:M_PI/2 clockwise:YES];
[circlePath addArcWithCenter:center radius:radius startAngle:M_PI/2 endAngle:M_PI clockwise:YES];
[circlePath addArcWithCenter:center radius:radius startAngle:M_PI endAngle:-M_PI clockwise:YES];
[circlePath closePath];
return circlePath;
}

Finally, animation done in action of btnDone which use CABasicAnimation to animate transformation from square to circle or vice versa.

- (IBAction)btnDone:(id)sender {
btnDowrk.selected=!btnDowrk.selected;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
animation.duration = 1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
if (btnDowrk.selected) {
animation.fromValue = (__bridge id)(sqPath.CGPath);
animation.toValue = (__bridge id)(crclePath.CGPath);
shapeLayer.path = crclePath.CGPath;
}
else{
animation.fromValue = (__bridge id)(crclePath.CGPath);
animation.toValue = (__bridge id)(sqPath.CGPath);
shapeLayer.path = sqPath.CGPath;
}
[shapeLayer addAnimation:animation forKey:@"animatePath"];
}

How do I animate a caret shape into a smooth arc?

The secret to this sort of animation is to install a path into a CAShapeLayer and use a CABasicAnimation on the path that transforms the path from its starting state to it's end state. The tricky part is that the starting and ending paths need to have the same number and type of control points.

I wrote a demo app that creates exactly the animation above. You can download it from Github (link)

Here is the readme from the repo, which explains how it works in some detail:


CaretToArcAnimation

This project animates a simple transform of a caret symbol to an arc that spans 3/4 of a circle. It looks like this:

enter image description here

It works by animating the path installed into a CAShapeLayer.

In order for a path animation to work correctly, the starting and ending paths need to have the same number and type of control points.

To animate from the caret to the arc, it creates the caret as 2 cubic bezier curves. The first "curve" (Which is actually a straight line) starts at the lower left of the caret, passes through points 1/3 and 2/3 of the way along the lower line segment, and ends at the "bend" in the caret. That creates a Bezier curve that renders as a line segment. The second curve is also a straight line Bezier curve. The second one starts at the bend in the caret and ends at the top left corner.

The arc is also composed of 2 cubic bezier curves, drawn in the same direction as those in the caret symbol. However, the control points for the arc's Bezier curves are chosen so that the resulting curve closely approximates an arc of 3π/2 radians, or 270 degrees, or 3/4 of a cicrcle. The arc is slightly larger than the caret it replaces, and faces the same way.

It's easier to understand if you add a visual representation of the Bezier control points, like this:

enter image description here

The red dots that land on the caret/curve are the endpoints of the 2 Bezier curves. The red dots that are on the outside of the arc are the control points that define the shape of the curves. For the caret shape, the control points are on the lines, which causes the Bezier curve to take a straight line shape.

I used this article to get the control points for the two Bezier curves. I didn't feel like figuring out the math, so I just set the curve on that page's interactive arc renderer to draw 3/8 of a circle, wrote down the coordinates of all the control points, and then flipped them to get the second Bezier curve.

Here is a screenshot from the Bezier Arc approximator web simulation I used to find the Bezier control points:

enter image description here

(In that screenshot, the angle slider is expressed in radians. One half of a 3/4 circle arc is 3/8 of a full circle, or 3/8 of 2π. 2π * 3/8 is about 2.36, so that is the arc angle I chose.)

The CaretToArcAnimation app defines a CaretToArcView class which is a subclass of UIView.

Most of the interesting work is done in the CaretToArcView class.

It has a static var layerClass which returns CAShapeLayer.self. This causes the view's backing layer to be set up as a CAShapeLayer.

class override var layerClass: AnyClass {
return CAShapeLayer.self
}

The CaretToArcView.swift file defines an enum ViewState:

enum ViewState: Int {
case none
case caret
case arc
}

The custom view class has a var viewState of type ViewState. Its initial value is .none, meaning no path is installed in the view.

If you set the state to .caret or .arc, it checks to see if the current layer path is nil. If it is, it installs the appropriate path into the layer without animation.

If the previous path was the other path type, it builds a path with the new shape, and creates a CABasicAnimation with a fromState of the previous path, and toState of the new path. The animation's duration is set using an instance variable, animationDuration. The animation uses ease-in, ease-out timing, although it would be easy to change.

The class also has public function rotate(_ doRotate: Bool). If you call it with doRotate == false, it removes all animations from the view's layer. If you call it with doRotate == true, it adds an infinitely repeating rotation animation to the layer, rotating it 360°, over and over, using linear timing.

The app's view controller drives the settings in the CaretToArcView, and it has logic that prevents the custom view from invoking both animations at the same time (Strange things would happen if you did that. Don't.)

The app's screen looks like the image below.

The view controller disables both the button and the switch during a toggle animation, and disables the "Toggle" button when the rotate switch is turned on and the shape is rotating.

enter image description here

Morph a shape in WPF

You could animate the RadiusX and RadiusY properties of a Rectangle like this:

<Rectangle Width="100" Height="100" RadiusX="50" RadiusY="50"
Stroke="Red" StrokeThickness="3">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard BeginTime="0:0:2">
<DoubleAnimation
Storyboard.TargetProperty="RadiusX"
To="0" Duration="0:0:2"/>
<DoubleAnimation
Storyboard.TargetProperty="RadiusY"
To="0" Duration="0:0:2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

Alternatively, animate the CornerRadius of a Border with a custom animation:

<Border Width="100" Height="100" CornerRadius="50"
BorderBrush="Red" BorderThickness="3">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard BeginTime="0:0:2">
<local:CornerRadiusAnimation
Storyboard.TargetProperty="CornerRadius"
To="0" Duration="0:0:2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>

The CornerRadiusAnimation:

public class CornerRadiusAnimation : AnimationTimeline
{
public CornerRadius To { get; set; }

public override Type TargetPropertyType
{
get { return typeof(CornerRadius); }
}

protected override Freezable CreateInstanceCore()
{
return new CornerRadiusAnimation { To = To };
}

public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
{
if (!animationClock.CurrentProgress.HasValue)
{
return defaultOriginValue;
}

var p = animationClock.CurrentProgress.Value;
var from = (CornerRadius)defaultOriginValue;

return new CornerRadius(
(1 - p) * from.TopLeft + p * To.TopLeft,
(1 - p) * from.TopRight + p * To.TopRight,
(1 - p) * from.BottomRight + p * To.BottomRight,
(1 - p) * from.BottomLeft + p * To.BottomLeft);
}
}

Animate a CAShapeLayer circle into a rounded-corner triangle

You're not ending up with the same number of control points because addArcToPoint() skips creating a line segment if the path's current point and its first argument are equal.

From its documentation (emphasis mine):

If the current point and the first tangent point of the arc (the starting point) are not equal, Quartz appends a straight line segment from the current point to the first tangent point.

These two calls in your triangle-making code won't produce line segments, while the corresponding calls making the squircle will:

addArcToPoint(trianglePath, aroundPoint: cheatPoint, onWayToPoint: bottomCorner, radius: triangleRadius)
addArcToPoint(trianglePath, aroundPoint: bottomCorner, onWayToPoint: leftCorner, radius: triangleRadius)

You can manually create the lines, though, by calling CGPathAddLineToPoint. And you can check your work using CGPathApplierFunction, which will let you enumerate the control points.


Also~~~ I might suggest that spinning a display link and writing a function which specifies the desired shape for every t ∈ [0, 1] is probably going to be a clearer, more maintainable solution.

Turn rectangular UIImageView into a circle

Your solution sounds fine to me, but what might be going wrong, is the fact that animating corner radius is not supported by UIView.animateWithDuration as seen in the View Programming Guide for iOS. Therefor, the results are sometimes not whats expected.

You can try out the following code.

func animate() {

//Some properties that needs to be set on UIImageView
self.imageView.clipsToBounds = true
self.imageView.contentMode = .ScaleAspectFill

//Create animations
let cornerRadiusAnim = changeCornerRadiusAnimation()
let squareAnim = makeSquareAnimation()
let boundsAnim = changeBoundsAnimation()
let positionAnim = changePositionAnimation(CGPointMake(300,480))

//Use group for sequenced execution of animations
let animationGroup = CAAnimationGroup()
animationGroup.animations = [cornerRadiusAnim, squareAnim, boundsAnim, positionAnim]
animationGroup.fillMode = kCAFillModeForwards
animationGroup.removedOnCompletion = false
animationGroup.duration = positionAnim.beginTime + positionAnim.duration
imageView.layer.addAnimation(animationGroup, forKey: nil)

}

func makeSquareAnimation() -> CABasicAnimation {

let center = imageView.center
let animation = CABasicAnimation(keyPath:"bounds")
animation.duration = 0.1;
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = NSValue(CGRect: imageView.frame)
animation.toValue = NSValue(CGRect: CGRectMake(center.x,center.y,imageView.frame.width,imageView.frame.width))
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation

}

func changeBoundsAnimation() -> CABasicAnimation {

let animation = CABasicAnimation(keyPath:"transform.scale")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = imageView.layer.mask?.valueForKeyPath("transform.scale")
animation.toValue = 0.1
animation.duration = 0.5
animation.beginTime = 0.1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation

}

func changeCornerRadiusAnimation() -> CABasicAnimation {

let animation = CABasicAnimation(keyPath:"cornerRadius")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = 0
animation.toValue = imageView.frame.size.width * 0.5
animation.duration = 0.1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation

}

func changePositionAnimation(newPosition: CGPoint) -> CABasicAnimation {

let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = NSValue(CGPoint: imageView.layer.position)
animation.toValue = NSValue(CGPoint: newPosition)
animation.duration = 0.3
animation.beginTime = 0.6
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}

Android ImageView morph: from Square to Circle (Solution updated)

Another way that you can do this is with a MotionLayout and ImageFilterView.
ImageFilterView, introduced in ConstraintLayout version 2.0, allows for the manipulation of images. It can do amazing things like crossfade two images but it can also modify the radius of a given image. My example doesn't have the small bounce that the one that the accepted answer posted but it would be easy to add with KeyFrames.

Here's what my solution looks like: VIDEO

And here are the files required to make it happen

First, your activity/fragment should look like the following (note, this can be a subView as well if you wanted to incorporate this into an existing layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity"
app:layoutDescription="@xml/motion_scene">

<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/puthPic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/puth"/>

</androidx.constraintlayout.motion.widget.MotionLayout>

Note the MotionLayout has a field called app:layoutDescription that points to @xml/motion_scene... this is what the motion_scene.xml layout looks like

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@+id/start"
motion:duration="500"
motion:motionInterpolator="easeInOut">
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/puthPic" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@id/puthPic">
<Layout
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginStart="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
<CustomAttribute
motion:attributeName="roundPercent"
motion:customFloatValue="1" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint android:id="@id/puthPic">
<Layout
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginEnd="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
<CustomAttribute
motion:attributeName="roundPercent"
motion:customFloatValue="0.000001" />
</Constraint>
</ConstraintSet>
</MotionScene>

And just with that small amount of code, the motionLayout interpolates between the location and the circle to square for you!

You'll notice that I put motion:customFloatValue="0.000001" as the end scene value for the percentRound. This is because there is an existing bug that causes the image to stay rectangular if you set the percentRound to 0.0. I have filed this bug and you can see more about it here if you want.

And there you have it, another way to animate between a square and circular view with ease!

How to animate drawing shape fit in 60 second?

The problem is with the way you made the BezierPath.

Try this instead:

let circlePath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.lineWidth = 20
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
shapeLayer.position = self.view.center
shapeLayer.transform = CATransform3DRotate(CATransform3DIdentity, -CGFloat.pi / 2, 0, 0, 1)

The arcCenter must be at .zero and set the shape's center as the center of the view. Your circle will start animating from the right most point though, therefore add a CATransform to rotate the shape 90 degrees counterclockwise.



Related Topics



Leave a reply



Submit