diff --git a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/FlutterOverlayWindowPlugin.java b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/FlutterOverlayWindowPlugin.java index 3ffc76b7..ccc012fa 100644 --- a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/FlutterOverlayWindowPlugin.java +++ b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/FlutterOverlayWindowPlugin.java @@ -9,6 +9,7 @@ import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.Log; +import android.view.WindowInsets; import android.view.WindowManager; import androidx.annotation.NonNull; @@ -90,6 +91,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { Map startPosition = call.argument("startPosition"); int startX = startPosition != null ? startPosition.getOrDefault("x", OverlayConstants.DEFAULT_XY) : OverlayConstants.DEFAULT_XY; int startY = startPosition != null ? startPosition.getOrDefault("y", OverlayConstants.DEFAULT_XY) : OverlayConstants.DEFAULT_XY; + Integer windowInsetsInteger = call.argument("windowInsets"); + int windowInsets = windowInsetsInteger != null ? windowInsetsInteger : ~WindowInsets.Type.ime(); WindowSetup.width = width != null ? width : -1; @@ -100,6 +103,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { WindowSetup.overlayTitle = overlayTitle; WindowSetup.overlayContent = overlayContent == null ? "" : overlayContent; WindowSetup.positionGravity = positionGravity; + WindowSetup.windowInsets = windowInsets; WindowSetup.setNotificationVisibility(notificationVisibility); final Intent intent = new Intent(context, OverlayService.class); @@ -112,9 +116,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { } else if (call.method.equals("isOverlayActive")) { result.success(OverlayService.isRunning); return; - } else if (call.method.equals("isOverlayActive")) { - result.success(OverlayService.isRunning); - return; } else if (call.method.equals("moveOverlay")) { int x = call.argument("x"); int y = call.argument("y"); diff --git a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayConstants.java b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayConstants.java index 16298e39..a8b92912 100644 --- a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayConstants.java +++ b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayConstants.java @@ -9,4 +9,6 @@ final public class OverlayConstants { static final String CHANNEL_ID = "Overlay Channel"; static final int NOTIFICATION_ID = 4579; static final int DEFAULT_XY = -6; + static final int MATCH_PARENT = -1; + static final int FULL_COVER = -1999; } diff --git a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayService.java b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayService.java index f50d6a37..4b9f4485 100644 --- a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayService.java +++ b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/OverlayService.java @@ -22,6 +22,7 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; import androidx.annotation.Nullable; @@ -139,7 +140,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { int width = call.argument("width"); int height = call.argument("height"); boolean enableDrag = call.argument("enableDrag"); - resizeOverlay(width, height, enableDrag, result); + int windowInsets = call.argument("windowInsets"); + resizeOverlay(width, height, enableDrag, windowInsets == -1 ? null : windowInsets, result); } }); overlayMessageChannel.setMessageHandler((message, reply) -> { @@ -156,13 +158,14 @@ public int onStartCommand(Intent intent, int flags, int startId) { int h = displaymetrics.heightPixels; szWindow.set(w, h); } + int dx = startX == OverlayConstants.DEFAULT_XY ? 0 : startX; - int dy = startY == OverlayConstants.DEFAULT_XY ? -statusBarHeightPx() : startY; + int dy = startY == OverlayConstants.DEFAULT_XY ? 0 : startY; WindowManager.LayoutParams params = new WindowManager.LayoutParams( - WindowSetup.width == -1999 ? -1 : WindowSetup.width, - WindowSetup.height != -1999 ? WindowSetup.height : screenHeight(), - 0, - -statusBarHeightPx(), + WindowSetup.width == -1999 ? WindowManager.LayoutParams.MATCH_PARENT : WindowSetup.width, + WindowSetup.height == -1999 ? WindowManager.LayoutParams.MATCH_PARENT : WindowSetup.height,// screenHeight(), + dx, + dy, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE, WindowSetup.flag | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN @@ -173,10 +176,16 @@ public int onStartCommand(Intent intent, int flags, int startId) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && WindowSetup.flag == clickableFlag) { params.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER; } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Log.d("OverlayService", "Setting window insets to " + WindowSetup.windowInsets); + params.setFitInsetsTypes(WindowSetup.windowInsets); + params.setFitInsetsIgnoringVisibility(false); + } + params.gravity = WindowSetup.gravity; flutterView.setOnTouchListener(this); windowManager.addView(flutterView, params); - moveOverlay(dx, dy, null); return START_STICKY; } @@ -240,12 +249,17 @@ private void updateOverlayFlag(MethodChannel.Result result, String flag) { } } - private void resizeOverlay(int width, int height, boolean enableDrag, MethodChannel.Result result) { + private void resizeOverlay(int width, int height, boolean enableDrag, Integer windowInsets, MethodChannel.Result result) { if (windowManager != null) { WindowManager.LayoutParams params = (WindowManager.LayoutParams) flutterView.getLayoutParams(); - params.width = (width == -1999 || width == -1) ? -1 : dpToPx(width); - params.height = (height != 1999 || height != -1) ? dpToPx(height) : height; + params.width = (width == OverlayConstants.FULL_COVER || width == OverlayConstants.MATCH_PARENT) ? OverlayConstants.MATCH_PARENT : dpToPx(width); + params.height = (height == OverlayConstants.FULL_COVER || height == OverlayConstants.MATCH_PARENT) ? OverlayConstants.MATCH_PARENT : dpToPx(height); WindowSetup.enableDrag = enableDrag; + + if (windowInsets != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + params.setFitInsetsTypes(windowInsets); + } + windowManager.updateViewLayout(flutterView, params); result.success(true); } else { diff --git a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/WindowSetup.java b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/WindowSetup.java index 3563a1ca..0f8b5de1 100644 --- a/android/src/main/java/flutter/overlay/window/flutter_overlay_window/WindowSetup.java +++ b/android/src/main/java/flutter/overlay/window/flutter_overlay_window/WindowSetup.java @@ -2,6 +2,7 @@ import android.view.Gravity; +import android.view.WindowInsets; import android.view.WindowManager; import androidx.core.app.NotificationCompat; @@ -20,6 +21,7 @@ public abstract class WindowSetup { static String positionGravity = "none"; static int notificationVisibility = NotificationCompat.VISIBILITY_PRIVATE; static boolean enableDrag = false; + static int windowInsets = WindowInsets.Type.systemBars(); static void setNotificationVisibility(String name) { diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 9080a7af..221bf6fc 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -18,6 +18,34 @@ class _HomePageState extends State { final _receivePort = ReceivePort(); SendPort? homePort; String? latestMessageFromOverlay; + + // Selected window insets types + Set selectedInsetsTypes = {WindowInsetsType.statusBars, WindowInsetsType.navigationBars}; + + // Collapsible section state + bool _isInsetsExpanded = false; + + // Available window insets types + final List> insetsTypes = [ + MapEntry('Status Bars', WindowInsetsType.statusBars), + MapEntry('Navigation Bars', WindowInsetsType.navigationBars), + MapEntry('Caption Bar', WindowInsetsType.captionBar), + MapEntry('IME', WindowInsetsType.ime), + MapEntry('System Gestures', WindowInsetsType.systemGestures), + MapEntry('Mandatory System Gestures', WindowInsetsType.mandatorySystemGestures), + MapEntry('Tappable Element', WindowInsetsType.tappableElement), + MapEntry('Display Cutout', WindowInsetsType.displayCutout), + MapEntry('Window Decor', WindowInsetsType.windowDecor), + MapEntry('System Overlays', WindowInsetsType.systemOverlays), + ]; + + int combineSelectedInsets() { + int combined = 0; + for (var type in selectedInsetsTypes) { + combined |= type; + } + return combined; + } @override void initState() { @@ -42,9 +70,59 @@ class _HomePageState extends State { appBar: AppBar( title: const Text('Plugin example app'), ), - body: Center( + body: SingleChildScrollView( child: Column( children: [ + InkWell( + onTap: () { + setState(() { + _isInsetsExpanded = !_isInsetsExpanded; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Row( + children: [ + Icon( + _isInsetsExpanded ? Icons.expand_less : Icons.expand_more, + size: 20, + ), + const SizedBox(width: 4), + const Text( + 'Window Insets', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + ], + ), + ), + ), + if (_isInsetsExpanded) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: insetsTypes.map((entry) { + final isSelected = selectedInsetsTypes.contains(entry.value); + return FilterChip( + label: Text(entry.key, style: const TextStyle(fontSize: 11)), + selected: isSelected, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + onSelected: (selected) { + setState(() { + if (selected) { + selectedInsetsTypes.add(entry.value); + } else { + selectedInsetsTypes.remove(entry.value); + } + }); + }, + ); + }).toList(), + ), + ), + const Divider(height: 1), TextButton( onPressed: () async { final status = await FlutterOverlayWindow.isPermissionGranted(); @@ -75,11 +153,31 @@ class _HomePageState extends State { height: (MediaQuery.of(context).size.height * 0.6).toInt(), width: WindowSize.matchParent, startPosition: const OverlayPosition(0, -259), + windowInsets: combineSelectedInsets(), ); }, child: const Text("Show Overlay"), ), const SizedBox(height: 10.0), + TextButton( + onPressed: () async { + if (await FlutterOverlayWindow.isActive()) return; + await FlutterOverlayWindow.showOverlay( + enableDrag: false, + overlayTitle: "X-SLAYER", + overlayContent: 'Fullscreen Overlay Enabled', + flag: OverlayFlag.defaultFlag, + visibility: NotificationVisibility.visibilityPublic, + //positionGravity: PositionGravity.none, + height: WindowSize.fullCover, + width: WindowSize.fullCover, + startPosition: const OverlayPosition(0, 0), + windowInsets: combineSelectedInsets(), + ); + }, + child: const Text("Show Fullscreen Overlay"), + ), + const SizedBox(height: 10.0), TextButton( onPressed: () async { final status = await FlutterOverlayWindow.isActive(); diff --git a/example/lib/main.dart b/example/lib/main.dart index e64a2a69..2c5c9e40 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_overlay_window_example/home_page.dart'; +import 'package:flutter_overlay_window_example/overlays/fullscreen_overlay.dart'; import 'package:flutter_overlay_window_example/overlays/true_caller_overlay.dart'; void main() { @@ -14,6 +15,7 @@ void overlayMain() { const MaterialApp( debugShowCheckedModeBanner: false, home: TrueCallerOverlay(), + // home: FullscreenOverlay(), /// Uncomment to test FullscreenOverlay ), ); } diff --git a/example/lib/overlays/fullscreen_overlay.dart b/example/lib/overlays/fullscreen_overlay.dart new file mode 100644 index 00000000..cb4cee5a --- /dev/null +++ b/example/lib/overlays/fullscreen_overlay.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_overlay_window/flutter_overlay_window.dart'; + +class FullscreenOverlay extends StatelessWidget { + const FullscreenOverlay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.black, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 3), + ), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Overlay With White Border', + style: TextStyle( + color: Colors.white, + fontSize: 24, + ), + ), + ElevatedButton( + onPressed: FlutterOverlayWindow.closeOverlay, + child: Text( + 'Close', + )) + ], + ), + ), + ), + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 254428b8..878e111f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -100,7 +100,7 @@ packages: path: ".." relative: true source: path - version: "0.4.5" + version: "0.5.0" flutter_test: dependency: "direct dev" description: flutter @@ -110,26 +110,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -166,10 +166,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" multiavatar: dependency: "direct main" description: @@ -291,10 +291,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" typed_data: dependency: transitive description: @@ -307,10 +307,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -320,5 +320,5 @@ packages: source: hosted version: "14.3.1" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/src/overlay_config.dart b/lib/src/overlay_config.dart index 4e4e06cb..ecec5006 100644 --- a/lib/src/overlay_config.dart +++ b/lib/src/overlay_config.dart @@ -78,3 +78,73 @@ class WindowSize { /// even the statusbar and the navigationbar static const int fullCover = -1999; } + +/// Class that matches closely to native Android WindowInsets.Type. +/// You can select any combination of insets to be applied to the overlay window. +class WindowInsetsType { + static const int _first = 1 << 0; + static const int statusBars = _first; + static const int navigationBars = 1 << 1; + static const int captionBar = 1 << 2; + static const int ime = 1 << 3; + static const int systemGestures = 1 << 4; + static const int mandatorySystemGestures = 1 << 5; + static const int tappableElement = 1 << 6; + static const int displayCutout = 1 << 7; + static const int windowDecor = 1 << 8; + static const int systemOverlays = 1 << 9; + static const int size = 10; + static const int defaultVisible = ~ime; + + // Private constructor to prevent instantiation + WindowInsetsType._(); + static String typeToString(int types) { + final result = StringBuffer(); + if ((types & statusBars) != 0) { + result.write('statusBars '); + } + if ((types & navigationBars) != 0) { + result.write('navigationBars '); + } + if ((types & captionBar) != 0) { + result.write('captionBar '); + } + if ((types & ime) != 0) { + result.write('ime '); + } + if ((types & systemGestures) != 0) { + result.write('systemGestures '); + } + if ((types & mandatorySystemGestures) != 0) { + result.write('mandatorySystemGestures '); + } + if ((types & tappableElement) != 0) { + result.write('tappableElement '); + } + if ((types & displayCutout) != 0) { + result.write('displayCutout '); + } + if ((types & windowDecor) != 0) { + result.write('windowDecor '); + } + if ((types & systemOverlays) != 0) { + result.write('systemOverlays '); + } + if (result.isNotEmpty) { + // Remove trailing space + return result.toString().trimRight(); + } + return result.toString(); + } + + /// Returns all system bars. Includes [statusBars], [captionBar] as well as + /// [navigationBars], [systemOverlays], but not [ime]. + static int getSystemBars() { + return statusBars | navigationBars | captionBar | systemOverlays; + } + + /// Returns all inset types combined. + static int all() { + return 0xFFFFFFFF; + } +} diff --git a/lib/src/overlay_window.dart b/lib/src/overlay_window.dart index fef7d45d..96fa782f 100644 --- a/lib/src/overlay_window.dart +++ b/lib/src/overlay_window.dart @@ -50,6 +50,7 @@ class FlutterOverlayWindow { bool enableDrag = false, PositionGravity positionGravity = PositionGravity.none, OverlayPosition? startPosition, + int windowInsets = WindowInsetsType.defaultVisible, }) async { await _channel.invokeMethod( 'showOverlay', @@ -64,6 +65,7 @@ class FlutterOverlayWindow { "notificationVisibility": visibility.name, "positionGravity": positionGravity.name, "startPosition": startPosition?.toMap(), + "windowInsets": windowInsets, }, ); } @@ -117,10 +119,19 @@ class FlutterOverlayWindow { } /// Update the overlay size in the screen + /// + /// `width` the overlay width + /// `height` the overlay height + /// `enableDrag` to enable/disable dragging the overlay + /// `windowInsets` the overlay insets, optional. You can combine the flags of + /// WindowInsetsType with the binary | operator. If not specified, the insets + /// will not be updated and remain at previous value. + /// `return` true if the size updated successfully static Future resizeOverlay( int width, int height, bool enableDrag, + {int windowInsets = -1} ) async { final bool? _res = await _overlayChannel.invokeMethod( 'resizeOverlay', @@ -128,6 +139,7 @@ class FlutterOverlayWindow { 'width': width, 'height': height, 'enableDrag': enableDrag, + 'windowInsets': windowInsets }, ); return _res;