0

I'm trying to create a button with these effects:

enter image description here

Currently I have a rounded flat button like this:

RaisedButton(
    color: Color(0xFFA93EF0),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(30.0),
    ),

How can I apply such effects to make it look 3D? I don't know the name of this effect but it looks like glass

Guerlando OCs
  • 1,886
  • 9
  • 61
  • 150

2 Answers2

6

Final Output:

enter image description here

We can achieve this look using ClipPath and CustomClipper,

It will be a little hard to comprehend the code below at first if you are not accustomed to using CustomClipper so you might have to spend some time understanding how path.lineTo and path.quadraticBezierTo are implemented.

Once you get a hang of it, you will be able to replicate this button as well as even more complex shapes with ease.

Full Source Code:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: RoundedButton(title: 'Flutter Demo Home Page'),
    );
  }
}

class RoundedButton extends StatefulWidget {
  RoundedButton({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _RoundedButtonState createState() => _RoundedButtonState();
}

class _RoundedButtonState extends State<RoundedButton> {
  int counter = 0;

  void _counter() {
    setState(() {
      counter = counter + 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: ClipPath(
              clipper: CustomClipperButton(),
              child: Stack(
                children: [
                  InkWell(
                    onTap: _counter,
                    child: Container(
                      width: 300,
                      height: 150,
                      color: Colors.purple[900],
                      child: Container(
                        width: 200,
                        height: 100,
                        decoration: BoxDecoration(
                          boxShadow: [
                            BoxShadow(
                              color: Colors.purple[300],
                              spreadRadius: 0,
                              blurRadius: 20.0,
                            ),
                          ],
                        ),
                      ),
                    ),
                  ),
                  Positioned(
                    bottom: -70,
                    right: -90,
                    child: Container(
                      width: 180,
                      height: 180,
                      decoration: BoxDecoration(
                        color: Colors.transparent,
                        shape: BoxShape.circle,
                        boxShadow: [
                          BoxShadow(
                            color: Colors.purple[800],
                            spreadRadius: 5,
                            blurRadius: 20.0,
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
          SizedBox(height: 30),
          Text(
            "You Pressed $counter times",
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
          )
        ],
      ),
    );
  }
}

class CustomClipperButton extends CustomClipper<Path> {
  @override
  getClip(Size size) {
    var path = Path();
    path.moveTo(0, size.height * 0.2);
    path.lineTo(0, size.height * 0.8);
    path.quadraticBezierTo(0, size.height, size.width * 0.1, size.height);
    path.lineTo(size.width * 0.7 - 10, size.height);
    path.quadraticBezierTo(
        size.width * 0.7, size.height, size.width * 0.7, size.height * 0.95);
    path.quadraticBezierTo(size.width * 0.7, size.height * 0.30,
        size.width - 10, size.height * 0.3);
    path.quadraticBezierTo(
        size.width, size.height * 0.3, size.width, size.height * 0.3 - 10);
    path.lineTo(size.width, size.height * 0.2);
    path.quadraticBezierTo(size.width, 0, size.width * 0.9, 0);
    path.lineTo(size.width * 0.1, 0);
    path.quadraticBezierTo(0, 0, 0, size.height * 0.2);
    return path;
  }

  @override
  bool shouldReclip(CustomClipper oldClipper) {
    return true;
  }
}

List To Documentations:

  1. ClipPath
  2. CustomClipper
  3. lineTo()
  4. quadraticBezierTo()

PS: I have used a dimension of 300*150 for the button container, you might have to take into account the dimension of the button that you will be creating, so calculating x,y coordinates will be a bit cumbersome at times, but as I said in the beginning after you understand the lineTo() and quadraticBezierTo() then implementation button of any size and shape will be very easy.

Update:

New Example without using ClipPath and probably the easiest:

Final Output:

enter image description here

The only thing here you have to sacrifice is the tiny rounded edges where that central quarter-circle starts-ends.

Width and heights can be dynamically set globally if required.

If You ask me, I will suggest you follow this method instead of ClipPath, which looks uniform, less complex, and easy to configure.

import 'package:flutter/material.dart';

class DpadButtons extends StatefulWidget {
  @override
  _DpadButtonsState createState() => _DpadButtonsState();
}

class _DpadButtonsState extends State<DpadButtons> {
  String button = "";

  void _selectedButton(String selectedButton) {
    setState(() {
      button = selectedButton;
    });
  }

  @override
  Widget build(BuildContext context) {
    double heightMain = 100;
    double widthMain = 180;
    return Scaffold(
      appBar: AppBar(title: Text("Dpad Button")),
      body: Container(
        width: double.infinity,
        child: Stack(
          children: [
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                buildButtonRow(
                  b1: "Button 1",
                  b2: "Button 2",
                  onClick: _selectedButton,
                  width: widthMain,
                  height: heightMain,
                ),
                SizedBox(
                  height: 10,
                ),
                buildButtonRow(
                  b1: "Button 3",
                  b2: "Button 4",
                  onClick: _selectedButton,
                  width: widthMain,
                  height: heightMain,
                ),
                SizedBox(
                  height: 10,
                ),
              ],
            ),
            Center(
              child: Container(
                width: heightMain * 1.5,
                height: heightMain * 1.5,
                decoration: BoxDecoration(
                  color: Colors.white,
                  shape: BoxShape.circle,
                ),
              ),
            ),
            Positioned(
              left: 100,
              top: 200,
              child: Text(
                "You clicked : $button",
                style: TextStyle(
                  fontSize: 20.0,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Row buildButtonRow(
      {String b1, String b2, Function onClick, double width, double height}) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        InkWell(
          onTap: () {
            onClick(b1);
          },
          child: Container(
            width: width,
            height: height,
            decoration: BoxDecoration(
              color: Colors.purple[900],
              borderRadius: BorderRadius.circular(height * 0.2),
            ),
            child: Container(
              width: width * 0.80,
              height: height * 0.80,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(height * 0.15),
                boxShadow: [
                  BoxShadow(
                    color: Colors.purple[200],
                    spreadRadius: -8,
                    blurRadius: 10.0,
                  ),
                ],
              ),
              child: Center(child: Text(b1)),
            ),
          ),
        ),
        SizedBox(
          width: 10,
        ),
        InkWell(
          onTap: () {
            onClick(b2);
          },
          child: Container(
            width: width,
            height: height,
            decoration: BoxDecoration(
              color: Colors.purple[900],
              borderRadius: BorderRadius.circular(height * 0.2),
            ),
            child: Container(
              width: width * 0.80,
              height: height * 0.80,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(height * 0.15),
                boxShadow: [
                  BoxShadow(
                    color: Colors.purple[200],
                    spreadRadius: -8,
                    blurRadius: 10.0,
                  ),
                ],
              ),
              child: Center(child: Text(b2)),
            ),
          ),
        ),
      ],
    );
  }
}
Ketan Ramteke
  • 10,183
  • 2
  • 21
  • 41
  • this loks very promising. However, when putting everything together, I noted that the bezier curve that forms the part that the circle cuts, is not a circle (its a bezier curve). Is there a way to substitute that bezier by a path that is a quarter of a circle? – Guerlando OCs Dec 08 '20 at 01:25
  • You can make bezier curve to absulute circles. Just have to declare control points properly. – Ketan Ramteke Dec 08 '20 at 01:59
  • I tried to do what you suggested but the widget that you done is very fixed for this specific width and height. I tried making it dependent on arbitrary width and height but respecting proportions, this is what I got: https://imgur.com/a/459pQYB. Apparently the inner shadows that you added wont work in proportion (code: https://pastebin.com/GzCnFgaR). Would you kindly update for a variable width/height? I promise to give you another bounty on top of this one. – Guerlando OCs Dec 08 '20 at 03:32
  • wait, will look into it. – Ketan Ramteke Dec 08 '20 at 03:35
  • one easy workaround, create an SVG in any image editor and convert it into the path from here: https://www.flutterclutter.dev/tools/svg-to-flutter-path-converter/#:~:text=Convert%20your%20SVG%20file%20directly,hit%20the%20respective%20buttons%20underneath. – Ketan Ramteke Dec 08 '20 at 05:13
  • I think this fixes the problem of the quadratic bezier being a circle. I have the SVG model so it won't be a problem. However, it does not solve the shadow problems – Guerlando OCs Dec 08 '20 at 06:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/225661/discussion-between-ketan-ramteke-and-guerlando-ocs). – Ketan Ramteke Dec 08 '20 at 10:05
5

It is easy to use image as button resource. If you want the 3D shadow effect by code, it can be achieved by shadows inside Container widget.

First: build the shape of the button

Here is the shape path

Path myCustomShapePath(Rect rect) {
  var r1 = rect.height * 0.5;
  var r2 = rect.height * 0.35;
  var r3 = rect.height * 0.1;
  var L = rect.left;
  var R = rect.right;
  var T = rect.top;
  var B = rect.bottom;

  return Path()
    ..moveTo(L, T + r2)
    ..arcTo(Rect.fromLTWH(L, T, r2, r2), pi, 0.5 * pi, false)
    ..lineTo(R - r2, T)
    ..arcTo(Rect.fromLTWH(R - r2, T, r2, r2), 1.5 * pi, 0.5 * pi, false)
    ..lineTo(R, B - r1 - 2 * r3)
    ..arcTo(Rect.fromLTWH(R - r3, B - r1 - 2 * r3, r3, r3), 0, 0.5 * pi, false)
    ..arcTo(Rect.fromLTWH(R - r1 - r3, B - r1 - r3, r1 * 2.5, r1 * 2.5),
        1.5 * pi, -0.5 * pi, false)
    ..arcTo(Rect.fromLTWH(R - r1 - 2 * r3, B - r3, r3, r3), 0, 0.5 * pi, false)
    ..lineTo(R - r2, B)
    ..arcTo(Rect.fromLTWH(L, B - r2, r2, r2), 0.5 * pi, 0.5 * pi, false)
    ..close();
}

Because I want the custom shape for the button and avoid the shadow blur outside of the button, so I extend both ShapeBorder and CustomClipper<Path> with the same path:

CustomClipPath

class CustomClipPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) =>
      myCustomShapePath(Rect.fromLTRB(0, 0, size.width, size.height));
  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

CustomShape

class CustomShape extends ShapeBorder {
  @override
  EdgeInsetsGeometry get dimensions => const EdgeInsets.only();

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) =>
      getOuterPath(rect, textDirection: textDirection);

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) =>
      myCustomShapePath(rect);
  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {}

  @override
  ShapeBorder scale(double t) => null;
}

Second: use BoxShadow to draw the button

Use ClipPath to clip the shadow blur and use shadows inside Container's ShapeDecoration to draw the 3D effect:

class MySolidButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: CustomClipPath(),
      child: Container(
        width: 200,
        height: 100,
        decoration: ShapeDecoration(
          shape: CustomShape(),
          shadows: [
            BoxShadow(color: Colors.white),
            BoxShadow(
              color: Color(0xFF550091),
              offset: Offset(0, 20),
              blurRadius: 5,
              spreadRadius: 10,
            ),
            BoxShadow(
              color: Color(0xFFA93EF0),
              blurRadius: 10,
              spreadRadius: -2,
            ),
          ],
        ),
        child: FlatButton(
          onPressed: () {},
        ),
      ),
    );
  }
}

Result

enter image description here


Update:

BoxShadow 1 BoxShadow 2 BoxShadow 3 Combine (Before Clip)
color: Colors.white color: Color(0xFF550091)
blurRadius: 5
spreadRadius: 10
color: Color(0xFFA93EF0)
blurRadius: 10
spreadRadius: -2
(shadow 2 move down)
offset: Offset(0, 20)
shadow 1 shadow 2 shadow 3 enter image description here

LayoutBuilder

  child: Container(
    width: 300,
    height: 200,
    child: LayoutBuilder(
      builder: (context, constraint) {
        // get constraint here 300 x 200
        return Container(
          decoration: ShapeDecoration(
            shape: CustomShape(),
yellowgray
  • 4,006
  • 6
  • 28
  • This looks amazing! – Ketan Ramteke Dec 12 '20 at 07:39
  • This is very impressive!!!!! The one thing that wasn't quite right for me was the shadows when I make the button bigger. Do you know how to make the shadows also follow the width and height? Some proportional constant or something? – Guerlando OCs Dec 13 '20 at 01:31
  • The shadow effect is combine with 3 colors of `BoxShadow`. The shadow default is **same shape as container**. `spreadRadius` represent the shadow size(which mean negative shrink the shadow). `blurRadius` represent the blur effect. (https://api.flutter.dev/flutter/painting/BoxShadow-class.html) You can try these values to get your result. – yellowgray Dec 13 '20 at 03:28
  • By the way the shape formula is by my assumption, it may cause the shape break when `height > width`. – yellowgray Dec 13 '20 at 03:30
  • "You can try these values to get your result", which values? – Guerlando OCs Dec 14 '20 at 01:18
  • I update with how the shadows combine. Because the values all need to set in pixel, you can wrap the LayoutBuilder which can get parent size and multiple it with a factor: [How can I layout widgets based on the size of the parent?](https://stackoverflow.com/questions/41558368/how-can-i-layout-widgets-based-on-the-size-of-the-parent) – yellowgray Dec 14 '20 at 07:10
  • @yellowgray I missed the bounty award so it auto awarded but I opened another one to award your extremely detailed and great answer. I have to wait 24hrs to award you, so just wait. Thanks!!!!!!!! – PPP Dec 15 '20 at 00:27
  • (this is a friend's account) – PPP Dec 15 '20 at 00:27