Neumorphism has been around for a while now and seemingly quite popular. It’s been a while since I last tried a design challenge, I was looking through dribbble and found this interesting design…
So lets make it (or very close to it)… TL;DR give me the code: https://github.com/tunitowen/Flutter-Dark-Neumorphism
Let’s ignore the header, it’s just text and and image. We’ll focus on the dial, and the cards at the bottom.
Dial
To breakdown the dial we have:
An inset circle, a raised inner circle, and a progress ring in between
Inset Circle
Flutter doesn’t have an inner shadow (that I could find anyway), so we have to be a little creative here. In order to get the effect (shadow top left, highlight bottom right) we’re going to play with RadialGradients and ClipPaths.
There will be a few layers to this component, to do this we’ll use the Stack
widget and add the following:
- A circle
Container
the same color as our background - A
ClipPath
containing a circularContainer
with a radial gradient for the highlight - A
ClipPath
containing a circularContainer
with a radial gradient for the shadow
We can create a RadialGradient
that looks like an inner shadow by playing around with the radius, focalRadius, center, and focal properties… this was all trial and error on my part. But, we only want that shadow in the top left; to achieve this we can use a ClipPath
to only show that corner as shown below…
Here’s the StatelessWidgets that I ended up with…
class CircleInnerShadow extends StatelessWidget {
final Color shadowColor;
final Color backgroundColor;
const CircleInnerShadow(
{Key key, @required this.shadowColor, @required this.backgroundColor})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
backgroundColor,
shadowColor,
],
center: AlignmentDirectional(0.05, 0.05),
focal: AlignmentDirectional(0, 0),
radius: 0.5,
focalRadius: 0,
stops: [0.75,
1.0],
),
),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0, 0.45],
colors: [backgroundColor.withOpacity(0), backgroundColor])),
),
);
}
}
class ShadowClipper extends CustomClipper<Path> {
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return true;
}
@override
Path getClip(Size size) {
Path path = Path();
path.moveTo(0, 0);
path.lineTo(size.width, 0);
path.lineTo(0, size.height);
path.close();
return path;
}
}
and to use them together…
ClipPath(
clipper: ShadowClipper(),
child: CircleInnerShadow(
shadowColor: shadowColor,
backgroundColor: backgroundColor,
),
)
The same, but inverse, for the highlight can be found in the git repo.
At this point we have something that looks like this…
Lets move on to the inner raised circle. Flutter has us covered here, as a Container
can take an array of BoxShadow
…
Container(
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: highlightColor,
offset: Offset(-10, -10),
blurRadius: 20,
spreadRadius: 0),
BoxShadow(
color: shadowColor,
offset: Offset(10, 10),
blurRadius: 20,
spreadRadius: 0)
]
))
Above we simple supply two shadows, one -10, -10 to move top left. One 10, 10 to move bottom right. Once we add that in, we’re left with this…
Starting to look something like what we want.
Time for some color, the designer has chosen to have a colorful progress ring. To do this we’ll use a CustomPainter
. We need to draw an arc, and the Paint
needs to have a vertical LinearGradient
here’s the StatelessWidget
and CustomPainter
…
import 'package:vector_math/vector_math.dart' as vm;class ProgressRing extends StatelessWidget {
final double progress;
const ProgressRing({Key key, @required this.progress}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return SizedBox.expand(
child: CustomPaint(
painter: RingPainter(
strokeWidth: constraints.maxWidth * 0.15,
progress: progress)));
});
}
}
class RingPainter extends CustomPainter {
final double strokeWidth;
final double progress;
RingPainter({@required this.strokeWidth, @required this.progress});
@override
void paint(Canvas canvas, Size size) {
final inset = size.width * 0.18;
final rect =
Rect.fromLTRB(inset, inset, size.width - inset, size.height - inset);
canvas.drawArc(
rect,
vm.radians(-90),
vm.radians(360 * progress),
false,
Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Constants.gradientStart,
Constants.gradientMiddle,
Constants.gradientEnd
]).createShader(rect)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round);
}
@override
bool shouldRepaint(RingPainter oldDelegate) {
if (oldDelegate.progress != progress ||
oldDelegate.strokeWidth != strokeWidth) {
return true;
}
return false;
}
}
Simply, I created a StatelessWidget
give that the progress we need to draw, and then pass that on (along with the relative stroke width) to the RingPainter
. The RingPainter
draws an arc, setting it’s startAngle
to -90 (top of the circle), and the sweep angle to the correct percentage of 360. (Yes I know creating the Paint inside the draw method is bad, but this is a quick hack together, so deal with it 😜 ).
Now we have the finished Circle…
We’re done with the circle, on to the cards at the bottom of the screen…
These are really straightforward, as we’ll make use of the Container
and array of BoxShadow
…
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
color: Constants.backgroundColor,
boxShadow: [
BoxShadow(
color: Constants.softHighlightColor,
offset: Offset(-10, -10),
spreadRadius: 0,
blurRadius: 10),
BoxShadow(
color: Constants.softShadowColor,
offset: Offset(10, 10),
spreadRadius: 0,
blurRadius: 10)
]))
Above I create a Container
, again give two shadows (-10,-10 & 10,10), and give the Container rounded corners. Set the color of the Container to be the same as your background color, and we have something like this…
For the circle inside this view, I reused the circle code from above, checkout the git repo to see exactly whats going on. Throw some text and icons in and we have …
So thats it, done.
I added a bit of responsiveness as I went along. For example, the circle and progress ring are all relative in size, so this component scales well. I also added a layout change for landscape …
Of course this is Flutter, so here is it on the mobile web…
and stretched out for desktop…