DEV Community

fathidevs
fathidevs

Posted on

Day 2: crafting HAPOC UI

I thought I will finish the UI fast since it's not a very complicated interface, but I had to learn how to make custom 3D buttons in Flutter. I didn't think about how the button should look like when doing the UX, it was a design decision when I finished off the main screen.


it's a game right? it has to offer some interactive experience. so I looked up few tutorials and source codes on Github, here are the two Githubs that actually helped me:


  1. Clicky button very intense 3D shape and satisfying to click on and watch it bounce. Preview
  2. Button3D very simple and straightforward, unlike Clicky Button it has two shapes and gives the illusion of a 3D button viewed from angel between top and front. Preview The button I created is inspired by both source codes above, it is like Clicky in animation and Button3D in design.

In Button3D the design is two widget above each other, the top is triggered when pressed and the bottom (I called it _body()) is static, both are wrapped in Stack() => GestureDetector() the GestureDetector() job is to toggle between true and false when tap detected, and because the top widget returns AnimatedPosition() I can play with the Duration() to achieve smooth animation.



here is the top widget, the one that detects taps:

Widget _topFace() {
    return AnimatedPadding(
      duration: Duration(milliseconds:600),
      curve: Curves.ease,
      padding: EdgeInsets.only(bottom: isTapped ? 0.0 : 20.0),
      child: Container(
        decoration: BoxDecoration(
            color: Colors.red,
            borderRadius: BorderRadius.circular(5.0)),
        child: Padding(
          padding: EdgeInsets.symmetric(
              vertical: 18.0, horizontal: 18.0),
          child: LimitedBox(
            maxWidth: MediaQuery.maybeOf(context)!.size.width *.9,
            child: Text(
              "Click me",
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

The important variable in above widget is isTapped the key to the juicey movements lol, it is false by default and changes to true only when onTapDown // finger is not lifted or moved from its position in GestureDetector() is triggered, then setState(() => istapped = false); when onTapup // finger lifted from the screen and onTapCanceled // finger slid from its position, also the VoidCallback onPressed() is detected when onTapUp is triggered, because user lift the finger after "press" action is done as a complete action and expect feedback.



here is the methods for detecting gestures:

void _tapDown(TapDownDetails details) {
    setState(() {
      isTapped = true;
    });
  }

  void _tapCancel() {
    setState(() {
      isTapped = false;
    });
  }

  void _tapUp(TapUpDetails details) {
    setState(() {
      isTapped = false;
    });
    widget.onPressed();
  }
Enter fullscreen mode Exit fullscreen mode

the static Widget _body(){...} that doesn't animate or interact with click event, or any event:

Widget _body() {
    return Container(
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(5.0),
      ),
      child: Padding(
        padding: EdgeInsets.symmetric(
            vertical: 18.0, horizontal: 18.0 - .1),
        child: LimitedBox(
          maxWidth: MediaQuery.maybeOf(context)!.size.width * .9,
          child: Visibility(
            visible: false,
            maintainAnimation: true,
            maintainSize: true,
            maintainState: true,
            child: Text(
              "Click Me",
              style: TextStyle(color: Colors.blue),
            ),
          ),
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

you can see useless Text() widget in the static Widget _body(){...} with visibility: false, it's because I set the Widget _topFace(){...} to wrap the Text() and I could not achieve that without setting copy of the Text() in the Widget _body(){...} otherwise it will wrap on itself and be smaller than Widget _topFace(){...}





check the complete code

Top comments (0)