From 002a1be60cd1bd9cc969406ba2c8954699af8356 Mon Sep 17 00:00:00 2001 From: X-SLAYER Date: Sun, 20 Apr 2025 16:29:56 +0100 Subject: [PATCH 1/3] fix: improve overlay window drag and animation - Fixes drag issues and animation - Adds null checks and cleanup - Improves animation easing and speed --- .../FlutterOverlayWindowPlugin.java | 3 + .../OverlayService.java | 268 +++++++++++------- example/lib/home_page.dart | 2 +- example/lib/main.dart | 4 +- example/pubspec.lock | 2 +- 5 files changed, 173 insertions(+), 106 deletions(-) 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 64f193bd..369f4ed9 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 @@ -143,6 +143,7 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { mActivity = binding.getActivity(); + binding.addActivityResultListener(this); if (FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG) == null) { FlutterEngineGroup enn = new FlutterEngineGroup(context); DartExecutor.DartEntrypoint dEntry = new DartExecutor.DartEntrypoint( @@ -155,6 +156,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { @Override public void onDetachedFromActivityForConfigChanges() { + this.mActivity = null; } @Override @@ -164,6 +166,7 @@ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBindin @Override public void onDetachedFromActivity() { + this.mActivity = null; } @Override 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..4044216e 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 @@ -1,5 +1,6 @@ package flutter.overlay.window.flutter_overlay_window; +import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -47,13 +48,12 @@ public class OverlayService extends Service implements View.OnTouchListener { private final int DEFAULT_NAV_BAR_HEIGHT_DP = 48; private final int DEFAULT_STATUS_BAR_HEIGHT_DP = 25; - + private int mScreenHeight = -1; + private float mDensity = -1; private Integer mStatusBarHeight = -1; private Integer mNavigationBarHeight = -1; private Resources mResources; - public static final String INTENT_EXTRA_IS_CLOSE_WINDOW = "IsCloseWindow"; - private static OverlayService instance; public static boolean isRunning = false; private WindowManager windowManager = null; @@ -82,6 +82,14 @@ public IBinder onBind(Intent intent) { @Override public void onDestroy() { Log.d("OverLay", "Destroying the overlay window service"); + if (mTrayAnimationTimer != null) { + mTrayAnimationTimer.cancel(); + mTrayAnimationTimer = null; + } + if (mTrayTimerTask != null) { + mTrayTimerTask.cancel(); + mTrayTimerTask = null; + } if (windowManager != null) { windowManager.removeView(flutterView); windowManager = null; @@ -98,28 +106,24 @@ public void onDestroy() { @Override public int onStartCommand(Intent intent, int flags, int startId) { mResources = getApplicationContext().getResources(); - int startX = intent.getIntExtra("startX", OverlayConstants.DEFAULT_XY); - int startY = intent.getIntExtra("startY", OverlayConstants.DEFAULT_XY); + boolean isCloseWindow = intent.getBooleanExtra(INTENT_EXTRA_IS_CLOSE_WINDOW, false); if (isCloseWindow) { - if (windowManager != null) { - windowManager.removeView(flutterView); - windowManager = null; - flutterView.detachFromFlutterEngine(); - stopSelf(); - } - isRunning = false; + cleanupAndStopSelf(); return START_STICKY; } + if (windowManager != null) { - windowManager.removeView(flutterView); - windowManager = null; - flutterView.detachFromFlutterEngine(); - stopSelf(); + cleanupAndStopSelf(); } + + int startX = intent.getIntExtra("startX", OverlayConstants.DEFAULT_XY); + int startY = intent.getIntExtra("startY", OverlayConstants.DEFAULT_XY); + isRunning = true; Log.d("onStartCommand", "Service started"); FlutterEngine engine = FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG); + assert engine != null; engine.getLifecycleChannel().appIsResumed(); flutterView = new FlutterView(getApplicationContext(), new FlutterTextureView(getApplicationContext())); flutterView.attachToFlutterEngine(FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG)); @@ -128,18 +132,22 @@ public int onStartCommand(Intent intent, int flags, int startId) { flutterView.setFocusableInTouchMode(true); flutterView.setBackgroundColor(Color.TRANSPARENT); flutterChannel.setMethodCallHandler((call, result) -> { - if (call.method.equals("updateFlag")) { - String flag = call.argument("flag").toString(); - updateOverlayFlag(result, flag); - } else if (call.method.equals("updateOverlayPosition")) { - int x = call.argument("x"); - int y = call.argument("y"); - moveOverlay(x, y, result); - } else if (call.method.equals("resizeOverlay")) { - int width = call.argument("width"); - int height = call.argument("height"); - boolean enableDrag = call.argument("enableDrag"); - resizeOverlay(width, height, enableDrag, result); + switch (call.method) { + case "updateFlag": + String flag = call.argument("flag"); + updateOverlayFlag(result, flag); + break; + case "updateOverlayPosition": + int x = call.argument("x"); + int y = call.argument("y"); + moveOverlay(x, y, result); + break; + case "resizeOverlay": + int width = call.argument("width"); + int height = call.argument("height"); + boolean enableDrag = call.argument("enableDrag"); + resizeOverlay(width, height, enableDrag, result); + break; } }); overlayMessageChannel.setMessageHandler((message, reply) -> { @@ -181,15 +189,30 @@ public int onStartCommand(Intent intent, int flags, int startId) { } + private void cleanupAndStopSelf() { + if (windowManager != null) { + windowManager.removeView(flutterView); + flutterView.detachFromFlutterEngine(); + windowManager = null; + flutterView = null; + } + isRunning = false; + stopSelf(); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) private int screenHeight() { - Display display = windowManager.getDefaultDisplay(); - DisplayMetrics dm = new DisplayMetrics(); - display.getRealMetrics(dm); - return inPortrait() ? - dm.heightPixels + statusBarHeightPx() + navigationBarHeightPx() - : - dm.heightPixels + statusBarHeightPx(); + if (mScreenHeight == -1) { + Display display = windowManager.getDefaultDisplay(); + DisplayMetrics dm = new DisplayMetrics(); + display.getRealMetrics(dm); + mScreenHeight = inPortrait() ? + dm.heightPixels + statusBarHeightPx() + navigationBarHeightPx() + : + dm.heightPixels + statusBarHeightPx(); + } + return mScreenHeight; } private int statusBarHeightPx() { @@ -324,19 +347,18 @@ public void onCreate() { createNotificationChannel(); Intent notificationIntent = new Intent(this, FlutterOverlayWindowPlugin.class); - int pendingFlags; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - pendingFlags = PendingIntent.FLAG_IMMUTABLE; - } else { - pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT; + int pendingFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? + PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT; + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, pendingFlags); + + int notifyIcon = getDrawableResourceId("mipmap", "launcher"); + if (notifyIcon == 0) { + notifyIcon = R.drawable.notification_icon; } - PendingIntent pendingIntent = PendingIntent.getActivity(this, - 0, notificationIntent, pendingFlags); - final int notifyIcon = getDrawableResourceId("mipmap", "launcher"); Notification notification = new NotificationCompat.Builder(this, OverlayConstants.CHANNEL_ID) .setContentTitle(WindowSetup.overlayTitle) .setContentText(WindowSetup.overlayContent) - .setSmallIcon(notifyIcon == 0 ? R.drawable.notification_icon : notifyIcon) + .setSmallIcon(notifyIcon) .setContentIntent(pendingIntent) .setVisibility(WindowSetup.notificationVisibility) .build(); @@ -362,88 +384,118 @@ private int getDrawableResourceId(String resType, String name) { } private int dpToPx(int dp) { - return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - Float.parseFloat(dp + ""), mResources.getDisplayMetrics()); + if (mDensity == -1) { + mDensity = mResources.getDisplayMetrics().density; + } + return Math.round(dp * mDensity); } private double pxToDp(int px) { - return (double) px / mResources.getDisplayMetrics().density; + if (mDensity == -1) { + mDensity = mResources.getDisplayMetrics().density; + } + return (double) px / mDensity; } private boolean inPortrait() { return mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; } + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View view, MotionEvent event) { - if (windowManager != null && WindowSetup.enableDrag) { - WindowManager.LayoutParams params = (WindowManager.LayoutParams) flutterView.getLayoutParams(); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - dragging = false; - lastX = event.getRawX(); - lastY = event.getRawY(); - break; - case MotionEvent.ACTION_MOVE: - float dx = event.getRawX() - lastX; - float dy = event.getRawY() - lastY; - if (!dragging && dx * dx + dy * dy < 25) { - return false; - } - lastX = event.getRawX(); - lastY = event.getRawY(); - boolean invertX = WindowSetup.gravity == (Gravity.TOP | Gravity.RIGHT) - || WindowSetup.gravity == (Gravity.CENTER | Gravity.RIGHT) - || WindowSetup.gravity == (Gravity.BOTTOM | Gravity.RIGHT); - boolean invertY = WindowSetup.gravity == (Gravity.BOTTOM | Gravity.LEFT) - || WindowSetup.gravity == Gravity.BOTTOM - || WindowSetup.gravity == (Gravity.BOTTOM | Gravity.RIGHT); - int xx = params.x + ((int) dx * (invertX ? -1 : 1)); - int yy = params.y + ((int) dy * (invertY ? -1 : 1)); - params.x = xx; - params.y = yy; - if (windowManager != null) { - windowManager.updateViewLayout(flutterView, params); - } - dragging = true; - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - lastYPosition = params.y; - if (!WindowSetup.positionGravity.equals("none")) { - if (windowManager == null) return false; - windowManager.updateViewLayout(flutterView, params); - mTrayTimerTask = new TrayAnimationTimerTask(); - mTrayAnimationTimer = new Timer(); - mTrayAnimationTimer.schedule(mTrayTimerTask, 0, 25); - } + if (windowManager == null || !WindowSetup.enableDrag) { + return false; + } + WindowManager.LayoutParams params = (WindowManager.LayoutParams) flutterView.getLayoutParams(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + dragging = false; + lastX = event.getRawX(); + lastY = event.getRawY(); + return false; + case MotionEvent.ACTION_MOVE: + float dx = event.getRawX() - lastX; + float dy = event.getRawY() - lastY; + if (!dragging && dx * dx + dy * dy < 25) { return false; - default: + } + updateOverlayPosition(params, dx, dy); + lastX = event.getRawX(); + lastY = event.getRawY(); + dragging = true; + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + lastYPosition = params.y; + if (WindowSetup.positionGravity.equals("none")) { return false; - } - return false; + } + + if (windowManager != null) { + startTrayAnimation(params); + } + return dragging; } return false; } + private void updateOverlayPosition(WindowManager.LayoutParams params, float dx, float dy) { + boolean invertX = WindowSetup.gravity == (Gravity.TOP | Gravity.RIGHT) + || WindowSetup.gravity == (Gravity.CENTER | Gravity.RIGHT) + || WindowSetup.gravity == (Gravity.BOTTOM | Gravity.RIGHT); + boolean invertY = WindowSetup.gravity == (Gravity.BOTTOM | Gravity.LEFT) + || WindowSetup.gravity == Gravity.BOTTOM + || WindowSetup.gravity == (Gravity.BOTTOM | Gravity.RIGHT); + + params.x += ((int) dx * (invertX ? -1 : 1)); + params.y += ((int) dy * (invertY ? -1 : 1)); + + if (windowManager != null) { + windowManager.updateViewLayout(flutterView, params); + } + } + + private void startTrayAnimation(WindowManager.LayoutParams params) { + windowManager.updateViewLayout(flutterView, params); + + if (mTrayAnimationTimer != null) { + mTrayAnimationTimer.cancel(); + mTrayAnimationTimer = null; + } + + mTrayTimerTask = new TrayAnimationTimerTask(); + mTrayAnimationTimer = new Timer(); + mTrayAnimationTimer.schedule(mTrayTimerTask, 0, 25); + } + private class TrayAnimationTimerTask extends TimerTask { int mDestX; int mDestY; - WindowManager.LayoutParams params = (WindowManager.LayoutParams) flutterView.getLayoutParams(); + float mAnimationSpeed = 3.0f; // Configurable animation speed + float mStopThreshold = 2.0f; // When to stop the animation + WindowManager.LayoutParams params; public TrayAnimationTimerTask() { super(); + params = (WindowManager.LayoutParams) flutterView.getLayoutParams(); mDestY = lastYPosition; + + calculateDestinationX(); + } + + private void calculateDestinationX() { switch (WindowSetup.positionGravity) { case "auto": - mDestX = (params.x + (flutterView.getWidth() / 2)) <= szWindow.x / 2 ? 0 : szWindow.x - flutterView.getWidth(); - return; + mDestX = (params.x + (flutterView.getWidth() / 2)) <= szWindow.x / 2 ? + 0 : szWindow.x - flutterView.getWidth(); + break; case "left": mDestX = 0; - return; + break; case "right": mDestX = szWindow.x - flutterView.getWidth(); - return; + break; default: mDestX = params.x; mDestY = params.y; @@ -454,18 +506,30 @@ public TrayAnimationTimerTask() { @Override public void run() { mAnimationHandler.post(() -> { - params.x = (2 * (params.x - mDestX)) / 3 + mDestX; - params.y = (2 * (params.y - mDestY)) / 3 + mDestY; + // Use improved easing function + params.x = (int)((params.x - mDestX) / mAnimationSpeed) + mDestX; + params.y = (int)((params.y - mDestY) / mAnimationSpeed) + mDestY; + if (windowManager != null) { windowManager.updateViewLayout(flutterView, params); } - if (Math.abs(params.x - mDestX) < 2 && Math.abs(params.y - mDestY) < 2) { - TrayAnimationTimerTask.this.cancel(); - mTrayAnimationTimer.cancel(); + + // Stop when close enough + if (Math.abs(params.x - mDestX) < mStopThreshold && + Math.abs(params.y - mDestY) < mStopThreshold) { + params.x = mDestX; + params.y = mDestY; + if (windowManager != null) { + windowManager.updateViewLayout(flutterView, params); + } + cancel(); + if (mTrayAnimationTimer != null) { + mTrayAnimationTimer.cancel(); + mTrayAnimationTimer = null; + } } }); } } - } \ No newline at end of file diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 9080a7af..80824b83 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -71,7 +71,7 @@ class _HomePageState extends State { overlayContent: 'Overlay Enabled', flag: OverlayFlag.defaultFlag, visibility: NotificationVisibility.visibilityPublic, - positionGravity: PositionGravity.auto, + positionGravity: PositionGravity.left, height: (MediaQuery.of(context).size.height * 0.6).toInt(), width: WindowSize.matchParent, startPosition: const OverlayPosition(0, -259), diff --git a/example/lib/main.dart b/example/lib/main.dart index e64a2a69..ebb9f30e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_overlay_window_example/home_page.dart'; -import 'package:flutter_overlay_window_example/overlays/true_caller_overlay.dart'; +import 'package:flutter_overlay_window_example/overlays/messanger_chathead.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -13,7 +13,7 @@ void overlayMain() { runApp( const MaterialApp( debugShowCheckedModeBanner: false, - home: TrueCallerOverlay(), + home: MessangerChatHead(), ), ); } diff --git a/example/pubspec.lock b/example/pubspec.lock index 254428b8..d7fa403c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -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 From bb0b2d54bc7bb1ceabbb7f4b9cfb2bdaf312c0fc Mon Sep 17 00:00:00 2001 From: X-SLAYER Date: Sun, 24 Aug 2025 11:55:09 +0100 Subject: [PATCH 2/3] feat: improve overlay positioning and boundaries - Respect system bars (status/navigation) - Constrain overlay to safe boundaries - Add auto position gravity --- .../OverlayService.java | 121 +++++++++++++++++- example/analysis_options.yaml | 25 ---- example/lib/home_page.dart | 6 +- example/pubspec.lock | 16 +-- pubspec.yaml | 2 +- 5 files changed, 128 insertions(+), 42 deletions(-) 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 4044216e..8eb1e0a2 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 @@ -155,7 +155,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { }); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // Get real screen dimensions (including system bars) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Display display = windowManager.getDefaultDisplay(); + DisplayMetrics dm = new DisplayMetrics(); + display.getRealMetrics(dm); + szWindow.set(dm.widthPixels, dm.heightPixels); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { windowManager.getDefaultDisplay().getSize(szWindow); } else { DisplayMetrics displaymetrics = new DisplayMetrics(); @@ -242,6 +248,61 @@ int navigationBarHeightPx() { return mNavigationBarHeight; } + + /** + * Get safe boundaries for overlay positioning that respect system bars + */ + private int[] getSafeBoundaries() { + int screenWidth = szWindow.x; + int screenHeight = szWindow.y; + int statusBarHeight = statusBarHeightPx(); + int navigationBarHeight = inPortrait() ? navigationBarHeightPx() : 0; + + // Calculate usable area boundaries + int leftBound = 0; + int rightBound = screenWidth; + int topBound = statusBarHeight; // Account for status bar + int bottomBound = screenHeight - navigationBarHeight; // Account for navigation bar + + return new int[]{leftBound, topBound, rightBound, bottomBound}; + } + + /** + * Apply safe boundary constraints to position based on gravity + */ + private void constrainToSafeBoundaries(WindowManager.LayoutParams params, int overlayWidth, int overlayHeight) { + int[] bounds = getSafeBoundaries(); + int leftBound = bounds[0]; + int topBound = bounds[1]; + int rightBound = bounds[2]; + int bottomBound = bounds[3]; + + if ((WindowSetup.gravity & Gravity.RIGHT) == Gravity.RIGHT) { + // Right-based coordinate system + params.x = Math.max(0, Math.min(params.x, rightBound - overlayWidth)); + } else if ((WindowSetup.gravity & Gravity.LEFT) == Gravity.LEFT) { + // Left-based coordinate system + params.x = Math.max(leftBound, Math.min(params.x, rightBound - overlayWidth)); + } else { + // Center-based coordinate system + int maxLeftOffset = -(rightBound / 2) + leftBound; + int maxRightOffset = (rightBound / 2) - overlayWidth; + params.x = Math.max(maxLeftOffset, Math.min(params.x, maxRightOffset)); + } + + if ((WindowSetup.gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { + // Bottom-based coordinate system + params.y = Math.max(0, Math.min(params.y, bottomBound - overlayHeight)); + } else if ((WindowSetup.gravity & Gravity.TOP) == Gravity.TOP) { + // Top-based coordinate system + params.y = Math.max(topBound, Math.min(params.y, bottomBound - overlayHeight)); + } else { + // Center-based coordinate system + int maxTopOffset = -(bottomBound / 2) + topBound; + int maxBottomOffset = (bottomBound / 2) - overlayHeight; + params.y = Math.max(maxTopOffset, Math.min(params.y, maxBottomOffset)); + } + } private void updateOverlayFlag(MethodChannel.Result result, String flag) { @@ -339,6 +400,7 @@ public void onCreate() { FlutterEngineCache.getInstance().put(OverlayConstants.CACHED_TAG, flutterEngine); } + // Create the MethodChannel with the properly initialized FlutterEngine if (flutterEngine != null) { flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), OverlayConstants.OVERLAY_TAG); @@ -441,6 +503,7 @@ public boolean onTouch(View view, MotionEvent event) { } private void updateOverlayPosition(WindowManager.LayoutParams params, float dx, float dy) { + // During dragging, allow free movement but constrain to safe boundaries boolean invertX = WindowSetup.gravity == (Gravity.TOP | Gravity.RIGHT) || WindowSetup.gravity == (Gravity.CENTER | Gravity.RIGHT) || WindowSetup.gravity == (Gravity.BOTTOM | Gravity.RIGHT); @@ -448,8 +511,12 @@ private void updateOverlayPosition(WindowManager.LayoutParams params, float dx, || WindowSetup.gravity == Gravity.BOTTOM || WindowSetup.gravity == (Gravity.BOTTOM | Gravity.RIGHT); + // Apply movement params.x += ((int) dx * (invertX ? -1 : 1)); params.y += ((int) dy * (invertY ? -1 : 1)); + + // Apply safe boundary constraints that respect status bar and navigation bar + constrainToSafeBoundaries(params, flutterView.getWidth(), flutterView.getHeight()); if (windowManager != null) { windowManager.updateViewLayout(flutterView, params); @@ -485,16 +552,53 @@ public TrayAnimationTimerTask() { } private void calculateDestinationX() { + // Calculate destination X based on position gravity using safe boundaries + int leftEdgeX, rightEdgeX; + int[] bounds = getSafeBoundaries(); + int leftBound = bounds[0]; + int rightBound = bounds[2]; + int overlayWidth = flutterView.getWidth(); + + // Calculate safe edge positions based on window gravity coordinate system + if ((WindowSetup.gravity & Gravity.RIGHT) == Gravity.RIGHT) { + // Right-based coordinate system: x=0 is right edge of screen + rightEdgeX = 0; // Right edge + leftEdgeX = rightBound - overlayWidth; // Left edge (within safe bounds) + } else if ((WindowSetup.gravity & Gravity.LEFT) == Gravity.LEFT) { + // Left-based coordinate system: x=0 is left edge of screen + leftEdgeX = leftBound; // Left edge (respecting safe area) + rightEdgeX = rightBound - overlayWidth; // Right edge (within safe bounds) + } else { + // Center-based coordinate system: x=0 is center of screen + // Calculate positions within safe boundaries + leftEdgeX = -(rightBound / 2) + leftBound; // Left edge (safe area) + rightEdgeX = (rightBound / 2) - overlayWidth; // Right edge (safe area) + } + switch (WindowSetup.positionGravity) { case "auto": - mDestX = (params.x + (flutterView.getWidth() / 2)) <= szWindow.x / 2 ? - 0 : szWindow.x - flutterView.getWidth(); + // For auto, choose left or right based on current position + int currentCenterX; + if ((WindowSetup.gravity & Gravity.RIGHT) == Gravity.RIGHT) { + // In right-based system, convert to absolute position + currentCenterX = rightBound - params.x - (overlayWidth / 2); + } else if ((WindowSetup.gravity & Gravity.LEFT) == Gravity.LEFT) { + // In left-based system, convert to absolute position + currentCenterX = params.x + (overlayWidth / 2); + } else { + // In center-based system, params.x is already relative to center + currentCenterX = params.x; + } + + // Choose left if center is on left half of safe area, right otherwise + boolean isOnLeftSide = currentCenterX <= (leftBound + rightBound) / 2; + mDestX = isOnLeftSide ? leftEdgeX : rightEdgeX; break; case "left": - mDestX = 0; + mDestX = leftEdgeX; break; case "right": - mDestX = szWindow.x - flutterView.getWidth(); + mDestX = rightEdgeX; break; default: mDestX = params.x; @@ -510,6 +614,9 @@ public void run() { params.x = (int)((params.x - mDestX) / mAnimationSpeed) + mDestX; params.y = (int)((params.y - mDestY) / mAnimationSpeed) + mDestY; + // Apply safe boundary constraints during animation + constrainToSafeBoundaries(params, flutterView.getWidth(), flutterView.getHeight()); + if (windowManager != null) { windowManager.updateViewLayout(flutterView, params); } @@ -519,6 +626,10 @@ public void run() { Math.abs(params.y - mDestY) < mStopThreshold) { params.x = mDestX; params.y = mDestY; + + // Final safe boundary check before setting final position + constrainToSafeBoundaries(params, flutterView.getWidth(), flutterView.getHeight()); + if (windowManager != null) { windowManager.updateViewLayout(flutterView, params); } diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 04e07d56..3b5d29de 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,32 +1,7 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. analyzer: errors: use_build_context_synchronously: ignore include: package:flutter_lints/flutter.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 80824b83..b38ab1b2 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -71,9 +71,9 @@ class _HomePageState extends State { overlayContent: 'Overlay Enabled', flag: OverlayFlag.defaultFlag, visibility: NotificationVisibility.visibilityPublic, - positionGravity: PositionGravity.left, - height: (MediaQuery.of(context).size.height * 0.6).toInt(), - width: WindowSize.matchParent, + positionGravity: PositionGravity.auto, + height: 250, + width: 250, startPosition: const OverlayPosition(0, -259), ); }, diff --git a/example/pubspec.lock b/example/pubspec.lock index d7fa403c..6316358e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -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 @@ -110,10 +110,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -315,10 +315,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" sdks: dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index e06223c4..8583ea47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_overlay_window description: Flutter plugin for displaying your flutter app over other apps on the screen -version: 0.5.0 +version: 0.5.1 homepage: https://github.com/X-SLAYER/flutter_overlay_window environment: From e28d37162739fff3dcfb6f1cdb22a08ae1cb7022 Mon Sep 17 00:00:00 2001 From: X-SLAYER Date: Sun, 24 Aug 2025 16:38:30 +0100 Subject: [PATCH 3/3] fix: improve window boundary constraints - Fix center gravity calculations - Add soft boundary constraints - Enhance logging for debugging --- .../OverlayService.java | 178 ++++++++++++++++-- 1 file changed, 158 insertions(+), 20 deletions(-) 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 8eb1e0a2..c7b956ae 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 @@ -264,9 +264,79 @@ private int[] getSafeBoundaries() { int topBound = statusBarHeight; // Account for status bar int bottomBound = screenHeight - navigationBarHeight; // Account for navigation bar + return new int[]{leftBound, topBound, rightBound, bottomBound}; } + /** + * Apply soft boundary constraints during dragging - allows more freedom while keeping overlay visible + */ + private void applySoftBoundaryConstraints(WindowManager.LayoutParams params, int overlayWidth, int overlayHeight) { + int[] bounds = getSafeBoundaries(); + int leftBound = bounds[0]; + int topBound = bounds[1]; + int rightBound = bounds[2]; + int bottomBound = bounds[3]; + + // During dragging, allow the overlay to go partially off-screen but keep at least 20% visible + int minVisibleWidth = Math.max(overlayWidth / 5, 20); // At least 20% or 20px visible + int minVisibleHeight = Math.max(overlayHeight / 5, 20); // At least 20% or 20px visible + + if ((WindowSetup.gravity & Gravity.RIGHT) == Gravity.RIGHT) { + // Right-based coordinate system - allow going off left edge but keep right edge visible + int maxLeftOffset = rightBound - minVisibleWidth; + int originalX = params.x; + params.x = Math.max(maxLeftOffset, params.x); + if (originalX != params.x) { + Log.d("OverlayService", "SOFT RIGHT gravity: X constrained from " + originalX + " to " + params.x); + } + } else if ((WindowSetup.gravity & Gravity.LEFT) == Gravity.LEFT) { + // Left-based coordinate system - allow going off right edge but keep left edge visible + int maxRightOffset = leftBound + overlayWidth - minVisibleWidth; + int originalX = params.x; + params.x = Math.min(maxRightOffset, params.x); + if (originalX != params.x) { + Log.d("OverlayService", "SOFT LEFT gravity: X constrained from " + originalX + " to " + params.x); + } + } else { + // Center-based coordinate system - allow going off edges but keep center visible + int maxLeftOffset = leftBound - (szWindow.x / 2) + minVisibleWidth; + int maxRightOffset = rightBound - (szWindow.x / 2) - (overlayWidth - minVisibleWidth); + int originalX = params.x; + params.x = Math.max(maxLeftOffset, Math.min(params.x, maxRightOffset)); + if (originalX != params.x) { + Log.d("OverlayService", "SOFT CENTER gravity: X constrained from " + originalX + " to " + params.x); + } + } + + if ((WindowSetup.gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { + // Bottom-based coordinate system - allow going off top edge but keep bottom edge visible + int maxTopOffset = bottomBound - minVisibleHeight; + int originalY = params.y; + params.y = Math.max(maxTopOffset, params.y); + if (originalY != params.y) { + Log.d("OverlayService", "SOFT BOTTOM gravity: Y constrained from " + originalY + " to " + params.y); + } + } else if ((WindowSetup.gravity & Gravity.TOP) == Gravity.TOP) { + // Top-based coordinate system - allow going off bottom edge but keep top edge visible + int maxBottomOffset = topBound + overlayHeight - minVisibleHeight; + int originalY = params.y; + params.y = Math.min(maxBottomOffset, params.y); + if (originalY != params.y) { + Log.d("OverlayService", "SOFT TOP gravity: Y constrained from " + originalY + " to " + params.y); + } + } else { + // Center-based coordinate system - allow going off edges but keep center visible + int maxTopOffset = topBound - (szWindow.y / 2) + minVisibleHeight; + int maxBottomOffset = bottomBound - (szWindow.y / 2) - (overlayHeight - minVisibleHeight); + int originalY = params.y; + params.y = Math.max(maxTopOffset, Math.min(params.y, maxBottomOffset)); + if (originalY != params.y) { + Log.d("OverlayService", "SOFT CENTER gravity: Y constrained from " + originalY + " to " + params.y); + } + } + } + /** * Apply safe boundary constraints to position based on gravity */ @@ -279,28 +349,62 @@ private void constrainToSafeBoundaries(WindowManager.LayoutParams params, int ov if ((WindowSetup.gravity & Gravity.RIGHT) == Gravity.RIGHT) { // Right-based coordinate system + // params.x is offset from right edge, 0 = right edge, positive = left of right edge + int originalX = params.x; params.x = Math.max(0, Math.min(params.x, rightBound - overlayWidth)); + if (originalX != params.x) { + Log.d("OverlayService", "RIGHT gravity: X constrained from " + originalX + " to " + params.x); + } } else if ((WindowSetup.gravity & Gravity.LEFT) == Gravity.LEFT) { // Left-based coordinate system + // params.x is offset from left edge, 0 = left edge, positive = right of left edge + int originalX = params.x; params.x = Math.max(leftBound, Math.min(params.x, rightBound - overlayWidth)); + if (originalX != params.x) { + Log.d("OverlayService", "LEFT gravity: X constrained from " + originalX + " to " + params.x); + } } else { // Center-based coordinate system - int maxLeftOffset = -(rightBound / 2) + leftBound; - int maxRightOffset = (rightBound / 2) - overlayWidth; - params.x = Math.max(maxLeftOffset, Math.min(params.x, maxRightOffset)); + // params.x is offset of window's center from screen's center + // Ensure window's left edge >= leftBound and right edge <= rightBound + int originalX = params.x; + int minX_center_offset = leftBound - (szWindow.x / 2) + (overlayWidth / 2); + int maxX_center_offset = rightBound - (szWindow.x / 2) - (overlayWidth / 2); + params.x = Math.max(minX_center_offset, Math.min(params.x, maxX_center_offset)); + if (originalX != params.x) { + Log.d("OverlayService", "CENTER gravity: X constrained from " + originalX + " to " + params.x + + " (min: " + minX_center_offset + ", max: " + maxX_center_offset + ")"); + } } if ((WindowSetup.gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { // Bottom-based coordinate system + // params.y is offset from bottom edge, 0 = bottom edge, positive = above bottom edge + int originalY = params.y; params.y = Math.max(0, Math.min(params.y, bottomBound - overlayHeight)); + if (originalY != params.y) { + Log.d("OverlayService", "BOTTOM gravity: Y constrained from " + originalY + " to " + params.y); + } } else if ((WindowSetup.gravity & Gravity.TOP) == Gravity.TOP) { // Top-based coordinate system + // params.y is offset from top edge, 0 = top edge, positive = below top edge + int originalY = params.y; params.y = Math.max(topBound, Math.min(params.y, bottomBound - overlayHeight)); + if (originalY != params.y) { + Log.d("OverlayService", "TOP gravity: Y constrained from " + originalY + " to " + params.y); + } } else { // Center-based coordinate system - int maxTopOffset = -(bottomBound / 2) + topBound; - int maxBottomOffset = (bottomBound / 2) - overlayHeight; - params.y = Math.max(maxTopOffset, Math.min(params.y, maxBottomOffset)); + // params.y is offset of window's center from screen's center + // Ensure window's top edge >= topBound and bottom edge <= bottomBound + int originalY = params.y; + int minY_center_offset = topBound - (szWindow.y / 2) + (overlayHeight / 2); + int maxY_center_offset = bottomBound - (szWindow.y / 2) - (overlayHeight / 2); + params.y = Math.max(minY_center_offset, Math.min(params.y, maxY_center_offset)); + if (originalY != params.y) { + Log.d("OverlayService", "CENTER gravity: Y constrained from " + originalY + " to " + params.y + + " (min: " + minY_center_offset + ", max: " + maxY_center_offset + ")"); + } } } @@ -472,6 +576,16 @@ public boolean onTouch(View view, MotionEvent event) { WindowManager.LayoutParams params = (WindowManager.LayoutParams) flutterView.getLayoutParams(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: + // Cancel any ongoing tray animation to prevent interference + if (mTrayAnimationTimer != null) { + mTrayAnimationTimer.cancel(); + mTrayAnimationTimer = null; + } + if (mTrayTimerTask != null) { + mTrayTimerTask.cancel(); + mTrayTimerTask = null; + } + dragging = false; lastX = event.getRawX(); lastY = event.getRawY(); @@ -490,14 +604,17 @@ public boolean onTouch(View view, MotionEvent event) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: lastYPosition = params.y; + boolean wasDragging = dragging; + dragging = false; // Reset dragging state for next touch sequence + if (WindowSetup.positionGravity.equals("none")) { - return false; + return wasDragging; } if (windowManager != null) { startTrayAnimation(params); } - return dragging; + return wasDragging; } return false; } @@ -511,12 +628,24 @@ private void updateOverlayPosition(WindowManager.LayoutParams params, float dx, || WindowSetup.gravity == Gravity.BOTTOM || WindowSetup.gravity == (Gravity.BOTTOM | Gravity.RIGHT); + // Log initial position before movement + Log.d("OverlayService", "Drag movement - dx: " + dx + ", dy: " + dy + + ", gravity: " + WindowSetup.gravity + ", invertX: " + invertX + ", invertY: " + invertY); + // Apply movement + int oldX = params.x; + int oldY = params.y; params.x += ((int) dx * (invertX ? -1 : 1)); params.y += ((int) dy * (invertY ? -1 : 1)); - // Apply safe boundary constraints that respect status bar and navigation bar - constrainToSafeBoundaries(params, flutterView.getWidth(), flutterView.getHeight()); + Log.d("OverlayService", "Position after movement - X: " + oldX + " -> " + params.x + + ", Y: " + oldY + " -> " + params.y); + + // During dragging, only apply soft constraints to prevent going completely off-screen + // This allows free movement while keeping the overlay partially visible + applySoftBoundaryConstraints(params, flutterView.getWidth(), flutterView.getHeight()); + + Log.d("OverlayService", "Position after soft constraints - X: " + params.x + ", Y: " + params.y); if (windowManager != null) { windowManager.updateViewLayout(flutterView, params); @@ -571,28 +700,37 @@ private void calculateDestinationX() { } else { // Center-based coordinate system: x=0 is center of screen // Calculate positions within safe boundaries - leftEdgeX = -(rightBound / 2) + leftBound; // Left edge (safe area) - rightEdgeX = (rightBound / 2) - overlayWidth; // Right edge (safe area) + int screenCenterX = szWindow.x / 2; + // For left edge: overlay's center should be at leftBound + overlayWidth/2 + leftEdgeX = (leftBound + overlayWidth / 2) - screenCenterX; + // For right edge: overlay's center should be at rightBound - overlayWidth/2 + rightEdgeX = (rightBound - overlayWidth / 2) - screenCenterX; } switch (WindowSetup.positionGravity) { case "auto": // For auto, choose left or right based on current position - int currentCenterX; + int currentAbsoluteX; if ((WindowSetup.gravity & Gravity.RIGHT) == Gravity.RIGHT) { // In right-based system, convert to absolute position - currentCenterX = rightBound - params.x - (overlayWidth / 2); + currentAbsoluteX = rightBound - params.x - (overlayWidth / 2); } else if ((WindowSetup.gravity & Gravity.LEFT) == Gravity.LEFT) { // In left-based system, convert to absolute position - currentCenterX = params.x + (overlayWidth / 2); + currentAbsoluteX = params.x + (overlayWidth / 2); } else { - // In center-based system, params.x is already relative to center - currentCenterX = params.x; + // In center-based system, convert params.x (offset from screen center) to absolute position + int screenCenterX = szWindow.x / 2; + currentAbsoluteX = screenCenterX + params.x; } - // Choose left if center is on left half of safe area, right otherwise - boolean isOnLeftSide = currentCenterX <= (leftBound + rightBound) / 2; + // Choose left if overlay center is on left half of safe area, right otherwise + int safeAreaCenterX = (leftBound + rightBound) / 2; + boolean isOnLeftSide = currentAbsoluteX <= safeAreaCenterX; mDestX = isOnLeftSide ? leftEdgeX : rightEdgeX; + + Log.d("OverlayService", "Auto positioning - currentAbsoluteX: " + currentAbsoluteX + + ", safeAreaCenterX: " + safeAreaCenterX + ", isOnLeftSide: " + isOnLeftSide + + ", leftEdgeX: " + leftEdgeX + ", rightEdgeX: " + rightEdgeX + ", destX: " + mDestX); break; case "left": mDestX = leftEdgeX; @@ -614,7 +752,7 @@ public void run() { params.x = (int)((params.x - mDestX) / mAnimationSpeed) + mDestX; params.y = (int)((params.y - mDestY) / mAnimationSpeed) + mDestY; - // Apply safe boundary constraints during animation + // Apply safe boundary constraints during animation to ensure final position is within bounds constrainToSafeBoundaries(params, flutterView.getWidth(), flutterView.getHeight()); if (windowManager != null) {