UI Challenge 3 – Game App UI by Tanmoy Roy

Game App UI by Tanmoy Roy

Hi folks.

In this post, we are going to implement Game App UI by Tanmoy Roy (check it in the Uplabs).

Let’s jump into implementation. As you may know, we have to split it into smaller problems and then we are going to implement them.

1. Item row section

I think the most important part is the item’s row section.

To Implement this section, we should use a Stack widget to overlay our views.
Firstly we should make the background section, we can use Container and Transform to implement it.

Here we rotate our Container around the y-axis. You can read more about transform here.

Transform(
  alignment: FractionalOffset.center,
  transform: Matrix4.identity()
    ..setEntry(3, 2, 0.01)
    ..rotateY(radians(rotationY)), // degrees from slider
  child: Container(
    height: 180,
    color: Colors.white.withOpacity(0.4),
    margin: EdgeInsets.symmetric(horizontal: 40),
  ),
),
Try it in DartPad

Now we can find an appropriate number. for example I think 5 degrees is enough.

Okay. As you see there is a minor problem. Our shape is not centered anymore.
We need to wrap it by another Transform to translate it to the left.
And also we can implement rounded corners.

Transform.translate(
  offset: Offset(-30, 0),
  child: Transform(
    alignment: FractionalOffset.center,
    transform: Matrix4.identity()
      ..setEntry(3, 2, 0.01)
      ..rotateY(radians(5)),
    child: Container(
      height: 180,
      margin: EdgeInsets.symmetric(horizontal: 40),
      decoration: BoxDecoration(
        color: Colors.white.withOpacity(0.4),
        borderRadius: BorderRadius.all(Radius.circular(22)),
      ),
    ),
  ),
),
Try it in DartPad

(Keep in mind that we didn’t optimize it for all screens, we just hard-coded some values. You may face some problems with bigger screens.)

Okay, now let’s add another Container below it:

Stack(
  alignment: Alignment.center,
  children: [
    Transform.translate(
      offset: Offset(-10, 0),
      child: Transform(
        alignment: FractionalOffset.center,
        transform: Matrix4.identity()
          ..setEntry(3, 2, 0.01)
          ..rotateY(radians(1.5)),
        child: Container(
          height: 216,
          margin: EdgeInsets.symmetric(horizontal: 40),
          decoration: BoxDecoration(
            color: Colors.white.withOpacity(0.1),
            borderRadius: BorderRadius.all(Radius.circular(22)),
          ),
        ),
      ),
    ),
    Transform.translate(
      offset: Offset(-44, 0),
      child: Transform(
        alignment: FractionalOffset.center,
        transform: Matrix4.identity()
          ..setEntry(3, 2, 0.01)
          ..rotateY(radians(8)),
        child: Container(
          height: 188,
          margin: EdgeInsets.symmetric(horizontal: 40),
          decoration: BoxDecoration(
            color: Colors.white.withOpacity(0.4),
            borderRadius: BorderRadius.all(Radius.circular(22)),
          ),
        ),
      ),
    ),
  ],
),

Great! Now it’s time to put our hero character image:

Container(
  height: rowHeight,
  child: Stack(
    alignment: Alignment.center,
    children: [
      Transform.translate(
        offset: Offset(-10, 0),
        child: Transform(
          alignment: FractionalOffset.center,
          transform: Matrix4.identity()
            ..setEntry(3, 2, 0.01)
            ..rotateY(radians(1.5)),
          child: Container(
            height: 216,
            margin: EdgeInsets.symmetric(horizontal: 40),
            decoration: BoxDecoration(
              color: Colors.white.withOpacity(0.1),
              borderRadius: BorderRadius.all(Radius.circular(22)),
            ),
          ),
        ),
      ),
      Transform.translate(
        offset: Offset(-44, 0),
        child: Transform(
          alignment: FractionalOffset.center,
          transform: Matrix4.identity()
            ..setEntry(3, 2, 0.01)
            ..rotateY(radians(8)),
          child: Container(
            height: 188,
            margin: EdgeInsets.symmetric(horizontal: 40),
            decoration: BoxDecoration(
              color: Colors.white.withOpacity(0.4),
              borderRadius: BorderRadius.all(Radius.circular(22)),
            ),
          ),
        ),
      ),
      Align(
        alignment: Alignment.centerLeft,
        child: Transform.translate(
          offset: Offset(-30, 0),
          child: Container(
            child: Image.network(
              'https://flutter4fun.com/wp-content/uploads/2020/11/Player-1.png',
              width: rowHeight,
              height: rowHeight,
            ),
          ),
        ),
      ),
    ],
  ),
);

Awesome! now we are going to implement the attributes section (those circles at the right of the character)

To do that we can use CustomPaint widget.
We use canvas.drawCircle twice (first for a large circle, second for an inner circle) after that we have something like this:

Then we paint the progress bar using canvas.drawArc:

class AttributePainter extends CustomPainter {
  final double progressPercent;
  final double strokeWidth, filledStrokeWidth;

  final bgPaint, strokeBgPaint, strokeFilledPaint;

  AttributePainter({
    this.progressPercent,
    this.strokeWidth = 4.0,
    this.filledStrokeWidth = 8.0,
  })  : bgPaint = Paint()..color = Colors.white.withOpacity(0.25),
        strokeBgPaint = Paint()..color = Color(0xffD264C9),
        strokeFilledPaint = Paint()
          ..color = Colors.white
          ..style = PaintingStyle.stroke
          ..strokeWidth = filledStrokeWidth
          ..strokeCap = StrokeCap.round;

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2;

    canvas.drawCircle(center, radius, bgPaint);
    canvas.drawCircle(center, radius - strokeWidth, strokeBgPaint);
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius - (strokeWidth / 2)),
      -math.pi / 2, // - 45 degrees to start from top
      (progressPercent / 100) * math.pi * 2,
      false,
      strokeFilledPaint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

Okay, now we just need to put our image at the middle of our CustomPaint widget. Fortunately, CustomPaint widget has a child property. It puts the child property at the center of itself.

AttributeWidget(
  progress: progress,
  child: Image.network('https://flutter4fun.com/wp-content/uploads/2020/11/speed.png'),
)

class AttributeWidget extends StatelessWidget {
  final double size;
  final double progress;
  final Widget child;

  const AttributeWidget({
    Key key,
    @required this.progress,
    this.size = 82,
    this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: AttributePainter(
        progressPercent: progress,
      ),
      size: Size(size, size),
      child: Container(
        padding: EdgeInsets.all(size / 3.8),
        width: size,
        height: size,
        child: child,
      ),
    );
  }
}
Try it in DartPad

Fair enough, let’s back to the hero row widget. Now we can put attributes at the right of our character image. Then we use OutlineButton for the “see details” button:

Align(
  alignment: Alignment.centerRight,
  child: Container(
    margin: EdgeInsets.only(right: 58),
    padding: EdgeInsets.symmetric(vertical: 34),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        AttributeWidget(
          progress: hero.speed,
          child: Image.network(speed),
        ),
        AttributeWidget(
          progress: hero.health,
          child: Image.network(heart),
        ),
        AttributeWidget(
          progress: hero.attack,
          child: Image.network(knife),
        ),
        SizedBox(
          height: 32,
          child: OutlineButton(
            child: new Text(
              'See Details',
              style: TextStyle(
                color: Colors.white,
                fontSize: 12,
              ),
            ),
            onPressed: () {},
            color: Colors.white,
            borderSide: BorderSide(
              color: Colors.white,
              width: 1,
            ),
            highlightedBorderColor: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: new BorderRadius.circular(30.0),
            ),
          ),
        )
      ],
    ),
  ),
)

Yay, we did it. now we can have a list to complete the first page.
Also, we need a gradient background, we can use Container and BoxDecoration to implement LinearGradient.

So this is the result of the first part.

2. Item details

In this part we are going to implement the details screen, see below image:

Before everything, we already know how to implement the gradient and the toolbar section. I will implement them without any explanation.

Scaffold(
  body: Container(
    width: double.infinity,
    height: double.infinity,
    child: Stack(
      children: [
        SafeArea(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              SizedBox(
                width: 18,
              ),
              IconButton(
                icon: Icon(
                  Icons.arrow_back_ios,
                  color: Colors.white,
                  size: 20,
                ),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
              Text(
                'Overview',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Expanded(
                  child: Container(
                height: 80,
              )),
              Container(
                width: 80,
                height: 80,
                child: Icon(
                  Icons.menu,
                  color: Colors.white,
                ),
              )
            ],
          ),
        ),
        // _HeroDetailsImage(hero.image),
      ],
    ),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [
          Color(0xFFF4E342),
          Color(0xFFEE3474),
        ],
        begin: Alignment(0.3, -1),
        end: Alignment(-0.8, 1),
      ),
    ),
  ),
);
Try it in DartPad

Now, let’s focus on the image section. There are 3 transparent layers and our selected hero image.
For the layers, we can use our previous section’s trick. We can combine 3 Containers using a Stack widget.

Padding(
  padding: const EdgeInsets.all(28.0),
  child: AspectRatio(
    aspectRatio: 1,
    child: Container(
      child: Stack(
        children: [
          Align(
            child: Container(
              margin: EdgeInsets.only(
                left: 16,
                right: 16,
                top: 16,
              ),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.1),
                borderRadius: BorderRadius.all(Radius.circular(28)),
              ),
            ),
            alignment: Alignment.bottomCenter,
          ),
          Align(
            child: Container(
              margin: EdgeInsets.all(8.0),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.1),
                borderRadius: BorderRadius.all(Radius.circular(28)),
              ),
            ),
            alignment: Alignment.bottomCenter,
          ),
          Align(
            child: Container(
              margin: EdgeInsets.only(
                bottom: 16,
              ),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.4),
                borderRadius: BorderRadius.all(Radius.circular(28)),
              ),
            ),
            alignment: Alignment.bottomCenter,
          ),
        ],
      ),
    ),
  ),
);

For the image, we can put it inside the front Container (the last Container):

children: [
  Align(
    child: Container(
      margin: EdgeInsets.only(
        left: 16,
        right: 16,
        top: 16,
      ),
      decoration: BoxDecoration(
        color: Colors.white.withOpacity(0.1),
        borderRadius: BorderRadius.all(Radius.circular(28)),
      ),
    ),
    alignment: Alignment.bottomCenter,
  ),
  Align(
    child: Container(
      margin: EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        color: Colors.white.withOpacity(0.1),
        borderRadius: BorderRadius.all(Radius.circular(28)),
      ),
    ),
    alignment: Alignment.bottomCenter,
  ),
  Align(
    child: Container(
      margin: EdgeInsets.only(
        bottom: 16,
      ),
      decoration: BoxDecoration(
        color: Colors.white.withOpacity(0.4),
        borderRadius: BorderRadius.all(Radius.circular(28)),
      ),
      child: Padding(
        padding: EdgeInsets.all(8),
        child: Center(
          child: Image.network(url),
        ),
      ),
    ),
    alignment: Alignment.bottomCenter,
  ),
],
Try it in DartPad

Hero name

Now it’s time to implement the hero name section:

We can simply implement it by overlapping two Text widgets using a Stack widget:

Stack(
  children: [
    Align(
      alignment: Alignment.bottomCenter,
      child: Text(
        heroName,
        style: TextStyle(
          color: Colors.white,
          fontWeight: FontWeight.bold,
          fontSize: 42,
        ),
      ),
    ),
    Align(
      alignment: Alignment.bottomCenter,
      child: Padding(
        padding: const EdgeInsets.only(bottom: 18.0),
        child: Text(
          heroName,
          style: TextStyle(
            color: Colors.white.withOpacity(0.1),
            fontWeight: FontWeight.bold,
            fontSize: 56,
          ),
        ),
      ),
    )
  ],
),
Try it in DartPad

After that, I added the remaining sections. Because they are not challenging enough, I skipped to explain them. It saves our time 😉

BTW, here is the final result of the second page:

ListView(
  controller: _scrollController,
  padding: EdgeInsets.only(top: appBarHeight),
  children: [
    _HeroDetailsImage(widget.hero.image),
    _HeroDetailsName(widget.hero.name),
    Padding(
      padding: const EdgeInsets.symmetric(horizontal: 22.0, vertical: 12),
      child: Text(
        "Super smash bros ultimate villagers from the animal crossing series. This troops fight most effectively in large group",
        style:
            TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w300),
        textAlign: TextAlign.center,
      ),
    ),
    SizedBox(height: 28,),
    Row(
      children: [
        SizedBox(
          width: 28,
        ),
        Expanded(
          child: Container(
            height: 54,
            child: OutlineButton(
              child: new Text(
                'Add Favourite',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 12,
                ),
              ),
              onPressed: () {},
              color: Colors.white,
              borderSide: BorderSide(
                color: Colors.white,
                width: 1,
              ),
              highlightedBorderColor: Colors.white,
              shape: RoundedRectangleBorder(
                borderRadius: new BorderRadius.circular(30.0),
              ),
            ),
          ),
        ),
        SizedBox(
          width: 14,
        ),
        Expanded(
          child: Container(
            height: 56,
            child: RaisedButton(
              onPressed: () {},
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(80.0)),
              padding: const EdgeInsets.all(0.0),
              child: Ink(
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    colors: [
                      Color(0xFFF29758),
                      Color(0xFFEF5D67),
                    ],
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter
                  ),
                  borderRadius: BorderRadius.all(Radius.circular(80.0)),
                ),
                child: Container(
                  constraints: const BoxConstraints(minWidth: 88.0, minHeight: 36.0),
                  // min sizes for Material buttons
                  alignment: Alignment.center,
                  child: const Text(
                    'OK',
                    style: TextStyle(color: Colors.white),
                  ),
                ),
              ),
            ),
          ),
        ),
        SizedBox(
          width: 28,
        ),
      ],
    ),
    SizedBox(height: 28,),
  ],
)

Now I wanna use the Hero widget to animate the selected Item into the details page.

And here you go. This is our final result 🙂
You can find the source code here.

Don’t forget to give me feedback in the comments section.

Stay safe!

Get real time updates directly on you device, subscribe now.

2 Comments
  1. szhua says

    great

  2. zoritoler imol says

    Some truly nice and useful information on this web site, too I think the style has great features.

Leave A Reply

Your email address will not be published.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More