Day 2: crafting HAPOC UI
Tags: appwritehack
Published: 2022-04-18 20:52:27

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.
<br>
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:
<br><br>
1. [Clicky button](https://github.com/raj457036/Flutter-Clicky-Button)
very intense 3D shape and satisfying to click on and watch it bounce. [Preview](https://github.com/raj457036/Flutter-Clicky-Button/blob/master/images/demo.gif)
<br>
2. [Button3D](https://github.com/nguyenvanduocit/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](https://camo.githubusercontent.com/a2558c859c1ac02b494972e6301a6fb3ac56651ff7df1f96682977b5554818f9/68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f313630302f312a636d37587a5133486333654e4a3845616537563462512e706e67)
<br><br>
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.
<br><br>
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),
            ),
          ),
        ),
      ),
    );
  }
```

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.
<br><br>
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();
  }
```

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

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(){...}`
<br><br><br><br>
[check the complete code](https://gist.github.com/fathidevs/28751f3529d497727946aaea820a9967)