Create Plugin - wurzelsand/flutter-memos GitHub Wiki
There is (was?) a bug in SystemChrome.setEnabledSystemUIMode for Android versions before SDK level 30 where the navigation bar flickers when switching between normal and full screen. So I wrote a plugin for exactly these versions.
-
Create Android and iOS plugin named fullscreen of fantasy company floatingcircle.com:
flutter create --template=plugin --org com.floatingcircle --platforms=android,ios fullscreen
Running on Android device:
./example/lib/main.dart:
import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:fullscreen/fullscreen.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { String _platformVersion = 'Unknown'; final _fullscreenPlugin = Fullscreen(); @override void initState() { super.initState(); initPlatformState(); } // Platform messages are asynchronous, so we initialize in an async method. Future<void> initPlatformState() async { String platformVersion; // Platform messages may fail, so we use a try/catch PlatformException. // We also handle the message potentially returning null. try { platformVersion = await _fullscreenPlugin.getPlatformVersion() ?? 'Unknown platform version'; } on PlatformException { platformVersion = 'Failed to get platform version.'; } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _platformVersion = platformVersion; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), body: Center( child: Text('Running on: $_platformVersion\n'), ), ), ); } }
./lib/fullscreen.dart:
import 'fullscreen_platform_interface.dart'; class Fullscreen { Future<String?> getPlatformVersion() { return FullscreenPlatform.instance.getPlatformVersion(); } }
./lib/fullscreen_method_channel.dart:
import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'fullscreen_platform_interface.dart'; /// An implementation of [FullscreenPlatform] that uses method channels. class MethodChannelFullscreen extends FullscreenPlatform { /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('fullscreen'); @override Future<String?> getPlatformVersion() async { final version = await methodChannel.invokeMethod<String>('getPlatformVersion'); return version; } }
./lib/fullscreen_platform_interface.dart:
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'fullscreen_method_channel.dart'; abstract class FullscreenPlatform extends PlatformInterface { /// Constructs a FullscreenPlatform. FullscreenPlatform() : super(token: _token); static final Object _token = Object(); static FullscreenPlatform _instance = MethodChannelFullscreen(); /// The default instance of [FullscreenPlatform] to use. /// /// Defaults to [MethodChannelFullscreen]. static FullscreenPlatform get instance => _instance; /// Platform-specific implementations should set this with their own /// platform-specific class that extends [FullscreenPlatform] when /// they register themselves. static set instance(FullscreenPlatform instance) { PlatformInterface.verifyToken(instance, _token); _instance = instance; } Future<String?> getPlatformVersion() { throw UnimplementedError('platformVersion() has not been implemented.'); } }
./android/src/main/kotlin/com/floatingcircle/fullscreen/FullscreenPlugin.kt:
package com.floatingcircle.fullscreen import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result /** FullscreenPlugin */ class FullscreenPlugin: FlutterPlugin, MethodCallHandler { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel : MethodChannel override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "fullscreen") channel.setMethodCallHandler(this) } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "getPlatformVersion") { result.success("Android ${android.os.Build.VERSION.RELEASE}") } else { result.notImplemented() } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } }
fullscreen/ios/Classes/SwiftFullscreenPlugin.swift:
import Flutter import UIKit public class SwiftFullscreenPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "fullscreen", binaryMessenger: registrar.messenger()) let instance = SwiftFullscreenPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { result("iOS " + UIDevice.current.systemVersion) } }
-
Open folder fullscreen/example/ios in Xcode and fullscreen/example/android in Android Studio, to edit the plugins. In Xcode you can find SwiftFullscreenPlugin.swift under Pods/Development Pods/fullscreen/.../Classes.
-
We need the fullscreen plugin only for Android versions before API level 30 (Android.R).
./example/lib/main.dart:
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fullscreen/fullscreen.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { final _fullscreen = Fullscreen(); var _isFullScreen = false; @override void initState() { super.initState(); SystemChrome.setSystemUIChangeCallback((systemOverlaysAreVisible) async => setState(() => _isFullScreen = systemOverlaysAreVisible)); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( floatingActionButton: FloatingActionButton( onPressed: toggleFullScreen, child: Icon(_isFullScreen ? Icons.fullscreen_exit :Icons.fullscreen), ), appBar: AppBar( title: const Text('Fullscreen plugin'), ), body: const Center( child: Text('Flutter full screen plugin'), ), ), ); } void toggleFullScreen() { setState(() { if (_isFullScreen) { _fullscreen.exitFullScreen(); _isFullScreen = false; } else { _fullscreen.enterFullScreen(FullScreenMode.leanBack); _isFullScreen = true; } }); } }
./lib/fullscreen.dart:
import 'fullscreen_platform_interface.dart'; enum FullScreenMode { immersive, immersiveSticky, leanBack } class Fullscreen { Future<void> enterFullScreen(FullScreenMode mode) { return FullscreenPlatform.instance.enterFullScreen(mode); } Future<void> exitFullScreen() { return FullscreenPlatform.instance.exitFullScreen(); } }
./lib/fullscreen_method_channel.dart:
import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'fullscreen.dart'; import 'fullscreen_platform_interface.dart'; /// An implementation of [FullscreenPlatform] that uses method channels. class MethodChannelFullscreen extends FullscreenPlatform { /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('fullscreen'); @override Future<void> exitFullScreen() async { if (Platform.isIOS) { await exitFullScreenFallBack(); } else if (Platform.isAndroid) { try { await methodChannel.invokeMethod('exitFullScreen'); } on PlatformException { await exitFullScreenFallBack(); } } } Future<void> exitFullScreenFallBack() async { await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); } @override Future<void> enterFullScreen(FullScreenMode mode) async { if (Platform.isIOS) { await enterFullScreenFallBack(mode); } else if (Platform.isAndroid) { try { await enterFullScreenAndroid(mode); } on PlatformException { await enterFullScreenFallBack(mode); } } } Future<void> enterFullScreenAndroid(FullScreenMode mode) async { switch (mode) { case FullScreenMode.immersive: await methodChannel.invokeMethod('enterImmersiveMode'); break; case FullScreenMode.immersiveSticky: await methodChannel.invokeMethod('enterImmersiveStickyMode'); break; case FullScreenMode.leanBack: await methodChannel.invokeMethod('enterLeanBackMode'); break; } } Future<void> enterFullScreenFallBack(FullScreenMode mode) async { switch (mode) { case FullScreenMode.immersive: await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); break; case FullScreenMode.immersiveSticky: await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); break; case FullScreenMode.leanBack: await SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack); break; } } }
./lib/fullscreen_platform_interface.dart:
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'fullscreen.dart'; import 'fullscreen_method_channel.dart'; abstract class FullscreenPlatform extends PlatformInterface { /// Constructs a FullscreenPlatform. FullscreenPlatform() : super(token: _token); static final Object _token = Object(); static FullscreenPlatform _instance = MethodChannelFullscreen(); /// The default instance of [FullscreenPlatform] to use. /// /// Defaults to [MethodChannelFullscreen]. static FullscreenPlatform get instance => _instance; /// Platform-specific implementations should set this with their own /// platform-specific class that extends [FullscreenPlatform] when /// they register themselves. static set instance(FullscreenPlatform instance) { PlatformInterface.verifyToken(instance, _token); _instance = instance; } Future<void> enterFullScreen(FullScreenMode mode) { throw UnimplementedError('enterFullScreen() has not been implemented'); } Future<void> exitFullScreen() { throw UnimplementedError('exitFullScreen() has not been implemented'); } }
./android/src/main/kotlin/com/floatingcircle/fullscreen/FullscreenPlugin.kt:
package com.floatingcircle.fullscreen import android.app.Activity import android.os.Build import android.view.View import android.view.WindowManager.* import androidx.annotation.RequiresApi import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result /** FullscreenPlugin */ class FullscreenPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel: MethodChannel private lateinit var activity: Activity override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "fullscreen") channel.setMethodCallHandler(this) } @RequiresApi(Build.VERSION_CODES.KITKAT) override fun onMethodCall(call: MethodCall, result: Result) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return result.error( "pluginRejected", "method call cancelled", "Flutter can handle method in Android > R itself" ) } when (call.method) { "enterImmersiveMode" -> { enterImmersiveMode() } "enterImmersiveStickyMode" -> { enterImmersiveStickyMode() } "enterLeanBackMode" -> { enterLeanBackMode() } "exitFullScreen" -> { exitFullScreen() } else -> { result.notImplemented() } } } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity } override fun onDetachedFromActivity() { activity.finish() } override fun onDetachedFromActivityForConfigChanges() { activity.finish() } private fun showCutoutArea() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // both landscape and portrait should use cutout area: activity.window.attributes.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } } @RequiresApi(Build.VERSION_CODES.KITKAT) private fun enterImmersiveMode() { showCutoutArea() @Suppress("DEPRECATION") activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar or View.SYSTEM_UI_FLAG_IMMERSIVE) } @RequiresApi(Build.VERSION_CODES.KITKAT) private fun enterImmersiveStickyMode() { showCutoutArea() @Suppress("DEPRECATION") activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) } @RequiresApi(Build.VERSION_CODES.KITKAT) private fun enterLeanBackMode() { showCutoutArea() @Suppress("DEPRECATION") activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar or View.SYSTEM_UI_FLAG_FULLSCREEN) // hide status bar } private fun exitFullScreen() { @Suppress("DEPRECATION") activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) } }
fullscreen/ios/Classes/SwiftFullscreenPlugin.swift:
not needed
-
Create new Flutter project (flutterproject):
└── Projects ├── fullscreen └── flutterproject
pubspec.yaml:
... dependencies: flutter: sdk: flutter fullscreen: path: ../fullscreen ...
./lib/main.dart:
import 'package:fullscreen/fullscreen.dart'; ...