Skip to main content

Leveraging ClipPath in Flutter

· 7 min read
Mahesh Jamdade

When it comes to drawing custom shapes in flutter we have custom paint and it works great. But what if you want to paint only a particular section of a widget? That's where ClipPath comes handy

ClipPath allows you to paint only a particular portion of your widget specified by the path

The path can be of any shape as long as your math skills go hand in hand with your imagination to draw the custom desired shape. You don't have to be a polymath to build some cool stuff, we can still build amazing things with basic Geometrical shapes.

Image is taken from dribble for demonstration purpose only, owned by Dan Stosich

To demonstrate some cool examples with ClipPath, we will add some dynamic content by redrawing frames and adding nuances of Animations. Because Static is Boring.

If you have no idea what the heck ClipPaths does then you should consider watching this video, Since No docs can explain you much better than this short Widget of the week video from the Flutter team. Also, Remember the above definition because we will leverage the definition of the ClipPath as we move along.

Now that you have some understanding of ClipPath Lets dive in

"We understimate ClipPaths in flutter "

The reason I say this is because of some of the examples that may look very complex or one would not ever imagine that they would use ClipPaths for such use cases. We will explore the use cases of ClipPath with the help of three examples.

ClipPaths in action

Example 1: Torch effect (aka The Harry Potter effect)

Of course, we do not have a magic wand to build this but we do have ClipPath lets break down how we can easily build this cool effect. There are three things here

  1. the background which is the hidden image (visible in a circle)
  2. The foreground which is the counter app (greyed out to reduce focus)
  3. And a Circle that exposes the background Now forget what we just saw so far and come back to basics, what do we do when we have to show two widgets on top of each other?

We use a STACK

Stack(
fit: StackFit.expand
children:[
ImageWidget(),
CounterApp(),
]
)

cool! that was the first step, and with this, we would get this output

I know it just looks like we have loaded a regular counter App but there's a ImageWidget underneath trust me 😀. if you don't believe me let's bring in ClipPath to shade some light on it to actually make it visible. so let's wrap the CounterApp with a ClipPath and give it a simple circular path as the clipper.

ClipPath(
clipper: CircleClipper(
center: Offset(500, 400),
radius: 100,
),
child: const CounterApp()
),

The CircleClipper is a simple class extending from CustomClipper which takes in a center, radius, and returns the circular path

CircleClipper({required this.center, required this.radius});
final Offset center;
final double radius;
@override
Path getClip(Size size) {
return Path()..addOval(
Rect.fromCircle(radius:radius,center: center));
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return true;
}
}

and that's it with that you can see this is the output and with that, it should be clear that there is actually an image below the counter app. Remember the definition "ClipPath only paints the required portion of the widget in this case it's the CounterApp".

Now, the only part left is to move the circle with the pointer. The movement is basically shifting the center of the circle. So as the mouse cursor moves we have to set the center of the circle to the coordinates of the mouse. And we can easily do this with the help of MouseRegion (A widget that tracks the movement of mice.) So by just wrapping the whole stack in a MouseRegion, you should get mouse events when the mouse is moved and you can update the position of the circle and that should be it.

void _updateLocation(PointerEvent details) {
setState(() {
dx = details.position.dx;
dy = details.position.dy;
});
}
Widget build(BuildContext context) {
return Scaffold(
body: MouseRegion(
cursor: SystemMouseCursors.click,
onHover: _updateLocation,
child: Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: [
ImageWidget(),
ClipPath(
clipper: CircleClipper(
center: Offset(dx, dy),
radius: 100,
),
child: const CounterApp()),
],
)
)
);
}

This gif may look laggy as the recorded gif runs at 15 fps but the original app is much smoother

Example 2: Telegram dark theme animation

If you have used the telegram app you must be familiar with the cool animation effect when switching themes in telegram. The gif on the left shows you this animation. At first look, this might look complex but if we break it down again we can easily nail it. And of course we will implement this using ClipPath. The above transition is pretty much straightforward since you know how example 1 was built. If we break down the telegram animation.

  1. It has a widget in dark mode.
  2. A widget in a light mode
  3. both overlapped on top of Each other

And on toggling the theme we are just painting the top widget using the ClipPath. So this is how roughly the body part of our widget tree looks

Stack(
children: [
_body(1),
ClipPath(
clipper: CircularClipper(
radius, position),
child: _body(2)),
],
);

Both the widgets (_body) at any given point will be the same except for their theme. So To identify them I am passing index values 1 and 2. And the position is basically the offset Indicating the Center point of the CircularClipper.

ThemeData getTheme(bool dark) {
if (dark)
return ThemeData.dark();
else
return ThemeData.light();
}
Widget _body(int index) {
return ValueListenableBuilder<bool>(
valueListenable: _darkNotifier,
builder: (BuildContext context, bool isDark, Widget? child) {
return Theme(
data: index == 2
? getTheme(!isDarkVisible)
: getTheme(isDarkVisible),
child: widget.childBuilder(context, index, GlobalKey()));
});
}

And then wiring this up with animationController you get this cool animation effect.

This gif is running at 30 fps and is not the exact representation of animation the actual animation is way more smoother at 60 fps

In order to reuse this animation effect, I wrote a wrapper widget around this which makes it pretty easy to use. You can find the complete source code to this widget on Github with example usage.

Example 3: Circular Navigation effect

If you have guessed it, This will be the same animation as example 2 just that instead of changing the theme we will change the routes. To make the route transition simpler flutter provides us with PageRouteBuilder class, which you can directly import in your Widget and animate between routes. Below is the method I used to animate using a ClipPath.

Route _pushRoute(Widget child) {
return PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 400),
reverseTransitionDuration: const Duration(milliseconds: 400),
opaque: false,
barrierDismissible: false,
pageBuilder: (context, animation, secondaryAnimation) => child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final screenSize = MediaQuery.of(context).size;
Offset center = Offset(screenSize.width / 2, screenSize.height / 2);
double beginRadius = 0.0;
double endRadius = screenSize.height * 1.5;
final tween = Tween(begin: beginRadius, end: endRadius);
final radiusTweenAnimation = animation.drive(tween);
return ClipPath(
clipper: CircleRevealClipper(
radius: radiusTweenAnimation.value, center: center),
child: child,
);
},
);
}

You can invoke a route transition as simple as this

Navigator.push(context, _pushRoute(YourWidget()));

and this is the output you get with the above transition.

And that's a wrap. You can find the complete source code on Github

Thanks for reading!