1

Is it possible to make an overlay in Flame for Flutter made of Flutter widgets, and have some transparent widgets, where a game could be seen through the overlay?

I have found information that transparent widgets can be done (How to create a transparent container in flutter), but I am not sure if they can make a Flame overlay transparent.


Question update:

Here is an minimum example of what I am trying to achieve.

import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Flame.device.fullScreen();
  await Flame.device.setPortrait();

  runApp(MainApp());
}

class MainApp extends StatelessWidget {
  MainApp({Key? key}) : super(key: key);

  final game = MainGame();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      darkTheme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.deepPurple,
      ),
      home: GameWidget(
        game: game,
        overlayBuilderMap: {
          'MainMenu': (context, MainGame game) => MainMenu(game: game),
        },
      ),
    );
  }
}

class MainGame extends FlameGame with MultiTouchTapDetector {
  @override
  void render(Canvas canvas) {
    super.render(canvas);
    Paint paint = Paint()
     ..color = Colors.white
     ..style = PaintingStyle.fill;
    canvas.drawRect(Rect.fromPoints(const Offset(10.0, 10.0), const Offset(30.0, 30.0)), paint);
  }

  @override
  void onTapDown(int pointerId, TapDownInfo info) {
    if (!overlays.isActive('MainMenu')) {
      overlays.add('MainMenu');
    }
    else {
      overlays.remove('MainMenu');
    }
    super.onTapDown(pointerId, info);
  }
}

class MainMenu extends StatefulWidget {
  const MainMenu({Key? key, required this.game}) : super(key: key);

  /// The reference to the game.
  final MainGame game;

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

class _MainMenuState extends State<MainMenu> with TickerProviderStateMixin {
  static Duration duration = const Duration(milliseconds: 250);
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: duration)
    ..addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.dismissed) {
        widget.game.overlays.remove('MainMenu');
      }
    });
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void deactivate() {
    //_controller.reverse(from: 1.0);
    super.deactivate();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, _) {
        double animationVal = _controller.value;
        double translateVal = (animationVal - 1.0) * 320.0;
        return Transform.translate(
          offset: Offset(translateVal, 0.0),
          child: Drawer(
            child: ListView(
              children: <Widget>[
                DrawerHeader(
                  decoration: const BoxDecoration(
                    color: Colors.deepPurple,
                  ),
                  child: Column(
                    children: const <Widget>[
                      Text('MyApp Menu'),
                    ],
                  ),
                ),
                ListTile(
                  title: const Text('Item 1'),
                  onTap: () => _controller.reverse(),
                ),
                ListTile(
                  title: const Text('Item 2'),
                  onTap: () => _controller.reverse(),
                ),
              ]
            ),
          ),
        );
      },
    );
  }
}

In the example drawer menu is sliding into the screen. Press on "Item 1" closes the menu as it should. Pressing somewhere else in the black area of the game closes the drawer menu by just hiding it. I see two options here:

  1. Animate the menu before hiding it, but I do not know how to reference the MainMenu class from MainGame class. In that case I would not call overlays.remove, but _controller.reverse() which ultimately calls overlays.remove.
  2. Make a transparent widget as a part of MainMenu that occupies the rest of the screen and catch onTap event to call _controller.reverse().

Then I have two subquestions:

  1. There is a value 320 in the source for Drawer widget. Can I get the Drawer widget width in order to put the correct value there. I do not want to set the width of Drawer. I want it to be the default width.
  2. Can I put an image or icon on the upper outer right edge of the drawer, that would look like a tab? When Drawer is out it would be "<<". When Drawer is hidden there would be an image in the game that would look like that part of the Drawer is visible to activate it ">>". I know how to put an image in the corner of the game. I do not know how to attach it to a Drawer.

Another update:

I changed the example according to @spydon's answer. Now the whole new set of problems appeared. Here is the changed source:

import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';

final GlobalKey<ScaffoldState> _key = GlobalKey();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Flame.device.fullScreen();
  await Flame.device.setPortrait();

  runApp(const MainApp());
}

class MainApp extends StatefulWidget {
  const MainApp({Key? key}) : super(key: key);

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  final game = MainGame();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      darkTheme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.deepPurple,
      ),
      home: Scaffold(
        key: _key,
        drawer: Drawer(
          child: ListView(
              children: <Widget>[
                DrawerHeader(
                  decoration: const BoxDecoration(
                    color: Colors.deepPurple,
                  ),
                  child: Column(
                    children: const <Widget>[
                      Text('MyApp Menu'),
                    ],
                  ),
                ),
                ListTile(
                  title: const Text('Item 1'),
                  onTap: () { Navigator.of(context).pop(); },
                ),
                ListTile(
                  title: const Text('Item 2'),
                  onTap: () { Navigator.of(context).pop(); },
                ),
              ]
          ),
        ),
        body: GameWidget(game: game),
      ),
    );
  }
}

class MainGame extends FlameGame with MultiTouchTapDetector {
  @override
  void render(Canvas canvas) {
    super.render(canvas);
    Paint paint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;
    canvas.drawRect(Rect.fromPoints(const Offset(10.0, 10.0), const Offset(30.0, 30.0)), paint);
  }

  @override
  void onTapDown(int pointerId, TapDownInfo info) {
    _key.currentState!.openDrawer();
    super.onTapDown(pointerId, info);
  }
}
nobody
  • 64
  • 5
  • 15

2 Answers2

0

Updated answer, after the question being updated:

It will be much easier for you to use a Scaffold than to use the overlays system, then you don't have to care about when the overlay should be removed.

class MainApp extends StatelessWidget {
  MainApp({Key? key}) : super(key: key);

  final game = MainGame();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      darkTheme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.deepPurple,
      ),
      home: Scaffold(
        drawer: Theme(
          data: Theme.of(context).copyWith(
            canvasColor: Colors.transparent,
          ),
          child: Drawer(
            // Code for the content of the drawer
          ),
        ),
        body: game,
      ),
    );
  }
}

Then if you need to access the game from any widgets, just pass in the game variable to them when they are initialized.

spydon
  • 9,372
  • 6
  • 33
  • 63
  • Thanks for the answer, but it does not answer anything else being already answered in https://stackoverflow.com/questions/66078974/how-to-create-a-transparent-container-in-flutter/66079024 mentioned in the question. I clarified the question and provided the example. – nobody Jan 04 '22 at 20:09
  • Thanks for the update, it is much clearer what you want to do now. I updated my answer. – spydon Jan 04 '22 at 21:14
  • Your answer provides some idea how to proceed, but I have a lot to solve before it works. First, transparent makes Drawer transparent, so I dropped it. I used key to reference a Drawer, because there is no other way to call its methods for open and close the Drawer. Currently the biggest problem I have is to open and close the drawer. I will look into Scaffold source to see how the Drawer is open/closed there. – nobody Jan 05 '22 at 15:54
  • I was struggling with this solution with no success. I can open the drawer with `openDrawer()`, but only for the first time. All later attempts enter an exception in `findRenderObject`. I open it with accessing the scaffold via `GlobalKey` (https://stackoverflow.com/questions/57748170/flutter-how-to-open-drawer-programmatically). I cannot close it with `Navigator.pop` either. – nobody Jan 05 '22 at 17:54
  • The solution I presented in `Question update` almost works. The only remaining issue is closing drawer without animation, when `overlays.remove()` is called. If there was a reference to overlays, where one could call a member function from game to the overlay to animate the drawer and then remove the overlay, everything would be fine. – nobody Jan 05 '22 at 18:37
0

The solution, I have come up with, works, but I do not like it. Here is the program that works. I am posting it here as a solution, but I will not mark it as accepted answer, becasue I think it should be done better. I do not like the part where the reference to the menu is being put into the game and then the reference to the State is put into the StatefulWidget in order to be able to call hide() from the game. If overlay could be accessed from the game, it would be easier.

import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Flame.device.fullScreen();
  await Flame.device.setPortrait();

  runApp(MainApp());
}

class MainApp extends StatelessWidget {
  MainApp({Key? key}) : super(key: key);

  final game = MainGame();

  @override
  Widget build(BuildContext context) {
    final MainMenu _mainMenu = MainMenu(game: game);
    game.mainMenu = _mainMenu;
    return MaterialApp(
      darkTheme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.deepPurple,
      ),
      home: GameWidget(
        game: game,
        overlayBuilderMap: {
          'MainMenu': (context, MainGame game) => _mainMenu,
        },
      ),
    );
  }
}

class MainGame extends FlameGame with MultiTouchTapDetector {
  late MainMenu mainMenu;
  @override
  void render(Canvas canvas) {
    super.render(canvas);
    Paint paint = Paint()
     ..color = Colors.white
     ..style = PaintingStyle.fill;
    canvas.drawRect(Rect.fromPoints(const Offset(10.0, 10.0), const Offset(30.0, 30.0)), paint);
  }

  @override
  void onTapDown(int pointerId, TapDownInfo info) {
    if (!overlays.isActive('MainMenu')) {
      overlays.add('MainMenu');
    }
    else {
      mainMenu.hide();
    }
    super.onTapDown(pointerId, info);
  }
}

class MainMenu extends StatefulWidget {
  MainMenu({Key? key, required this.game}) : super(key: key);

  final MainGame game;

  void hide() {
    _mainMenuState.hide();
  }

  late _MainMenuState _mainMenuState;
  @override
  _MainMenuState createState() {
    _mainMenuState = _MainMenuState();
    return _mainMenuState;
  }
}

class _MainMenuState extends State<MainMenu> with TickerProviderStateMixin {
  static Duration duration = const Duration(milliseconds: 250);
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: duration)
    ..addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.dismissed) {
        widget.game.overlays.remove('MainMenu');
      }
    });
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void hide() {
    _controller.reverse();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, _) {
        double animationVal = _controller.value;
        double translateVal = (animationVal - 1.0) * 320.0;
        return Transform.translate(
          offset: Offset(translateVal, 0.0),
          child: Drawer(
            child: ListView(
              children: <Widget>[
                DrawerHeader(
                  decoration: const BoxDecoration(
                    color: Colors.deepPurple,
                  ),
                  child: Column(
                    children: const <Widget>[
                      Text('MyApp Menu'),
                    ],
                  ),
                ),
                ListTile(
                  title: const Text('Item 1'),
                  onTap: () => hide(),
                ),
                ListTile(
                  title: const Text('Item 2'),
                  onTap: () => hide(),
                ),
              ]
            ),
          ),
        );
      },
    );
  }
}

There is still 320 in the source, because I do not know how to get the drawer's width to put it in instead of 320.

In the end, the answer does not need any transparency, for which I was asking for.

nobody
  • 64
  • 5
  • 15