Poperton
Poperton

Reputation: 2206

How to apply glass-like 3D effect on a RoundedRectangleBorder button on Flutter?

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

Upvotes: 0

Views: 4346

Answers (2)

yellowgray
yellowgray

Reputation: 4509

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(),

Upvotes: 6

Ketan Ramteke
Ketan Ramteke

Reputation: 10675

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)),
            ),
          ),
        ),
      ],
    );
  }
}

Upvotes: 7

Related Questions