From 3b6bde0e97f0399a22800a438a671cab5fdc849d Mon Sep 17 00:00:00 2001 From: "A.Mahidwei" Date: Mon, 25 Aug 2025 06:36:25 +0200 Subject: [PATCH] adding the ability to make the overlay bounce up and down --- .../FlutterOverlayWindowPlugin.java | 6 ++ .../OverlayService.java | 77 ++++++++++++++++++- .../flutter_overlay_window/WindowSetup.java | 5 +- lib/src/overlay_window.dart | 59 ++++++++------ 4 files changed, 120 insertions(+), 27 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 3ffc76b7..185ffd23 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 @@ -86,6 +86,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { String overlayContent = call.argument("overlayContent"); String notificationVisibility = call.argument("notificationVisibility"); boolean enableDrag = call.argument("enableDrag"); + boolean enableBounce = call.argument("enableBounce"); + int bounceHeight = call.argument("bounceHeight"); + int bounceDuration = call.argument("bounceDuration"); String positionGravity = call.argument("positionGravity"); Map startPosition = call.argument("startPosition"); int startX = startPosition != null ? startPosition.getOrDefault("x", OverlayConstants.DEFAULT_XY) : OverlayConstants.DEFAULT_XY; @@ -95,6 +98,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { WindowSetup.width = width != null ? width : -1; WindowSetup.height = height != null ? height : -1; WindowSetup.enableDrag = enableDrag; + WindowSetup.enableBounce = enableBounce; + WindowSetup.bounceHeight = bounceHeight; + WindowSetup.bounceDuration = bounceDuration; WindowSetup.setGravityFromAlignment(alignment != null ? alignment : "center"); WindowSetup.setFlag(flag != null ? flag : "flagNotFocusable"); WindowSetup.overlayTitle = overlayTitle; 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..6b06c9d7 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 @@ -23,6 +23,9 @@ import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.view.animation.LinearInterpolator; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -71,6 +74,9 @@ public class OverlayService extends Service implements View.OnTouchListener { private Point szWindow = new Point(); private Timer mTrayAnimationTimer; private TrayAnimationTimerTask mTrayTimerTask; + private int bounceBaseY = 0; // px + private int bounceAmplitudePx = 0; // px + private ObjectAnimator bounceAnimator; @Nullable @Override @@ -82,6 +88,8 @@ public IBinder onBind(Intent intent) { @Override public void onDestroy() { Log.d("OverLay", "Destroying the overlay window service"); + stopBounce(); + stopTrayAnimation(); if (windowManager != null) { windowManager.removeView(flutterView); windowManager = null; @@ -102,6 +110,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { int startY = intent.getIntExtra("startY", OverlayConstants.DEFAULT_XY); boolean isCloseWindow = intent.getBooleanExtra(INTENT_EXTRA_IS_CLOSE_WINDOW, false); if (isCloseWindow) { + stopBounce(); + stopTrayAnimation(); if (windowManager != null) { windowManager.removeView(flutterView); windowManager = null; @@ -112,6 +122,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } if (windowManager != null) { + stopBounce(); + stopTrayAnimation(); windowManager.removeView(flutterView); windowManager = null; flutterView.detachFromFlutterEngine(); @@ -139,7 +151,10 @@ 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); + boolean enableBounce = call.argument("enableBounce"); + int bounceHeight = call.argument("bounceHeight"); + int bounceDuration = call.argument("bounceDuration"); + resizeOverlay(width, height, enableDrag, enableBounce, bounceHeight, bounceDuration, result); } }); overlayMessageChannel.setMessageHandler((message, reply) -> { @@ -177,6 +192,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { flutterView.setOnTouchListener(this); windowManager.addView(flutterView, params); moveOverlay(dx, dy, null); + if (WindowSetup.enableBounce) { + WindowManager.LayoutParams p = (WindowManager.LayoutParams) flutterView.getLayoutParams(); + bounceBaseY = p.y; + bounceAmplitudePx = dpToPx(WindowSetup.bounceHeight); + startBounce(); + } return START_STICKY; } @@ -240,13 +261,22 @@ 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, boolean enableBounce, int bounceHeight, int bounceDuration, 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; WindowSetup.enableDrag = enableDrag; + WindowSetup.enableBounce = enableBounce; + WindowSetup.bounceHeight = bounceHeight; + WindowSetup.bounceDuration = bounceDuration; windowManager.updateViewLayout(flutterView, params); + if (WindowSetup.enableBounce) { + bounceAmplitudePx = dpToPx(WindowSetup.bounceHeight); + startBounce(); + } else { + stopBounce(); + } result.success(true); } else { result.success(false); @@ -380,6 +410,7 @@ public boolean onTouch(View view, MotionEvent event) { WindowManager.LayoutParams params = (WindowManager.LayoutParams) flutterView.getLayoutParams(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: + stopBounce(); dragging = false; lastX = event.getRawX(); lastY = event.getRawY(); @@ -417,6 +448,11 @@ public boolean onTouch(View view, MotionEvent event) { mTrayAnimationTimer = new Timer(); mTrayAnimationTimer.schedule(mTrayTimerTask, 0, 25); } + if (WindowSetup.enableBounce) { + // reset base to current position and resume bounce + bounceBaseY = params.y; + startBounce(); + } return false; default: return false; @@ -467,5 +503,42 @@ public void run() { } } + private void startBounce() { + stopBounce(); + if (flutterView == null) return; + if (bounceAmplitudePx <= 0) bounceAmplitudePx = dpToPx(WindowSetup.bounceHeight); + float from = -bounceAmplitudePx; + float to = bounceAmplitudePx; + bounceAnimator = ObjectAnimator.ofFloat(flutterView, View.TRANSLATION_Y, from, to); + // Interpret bounceDuration as duration per full cycle (ms) + int duration = WindowSetup.bounceDuration <= 50 ? WindowSetup.bounceDuration * 100 : WindowSetup.bounceDuration; + duration = Math.max(300, duration); + bounceAnimator.setDuration(duration); + bounceAnimator.setRepeatMode(ValueAnimator.REVERSE); + bounceAnimator.setRepeatCount(ValueAnimator.INFINITE); + bounceAnimator.setInterpolator(new LinearInterpolator()); + bounceAnimator.start(); + } + + private void stopBounce() { + if (bounceAnimator != null) { + bounceAnimator.cancel(); + bounceAnimator = null; + } + if (flutterView != null) { + flutterView.clearAnimation(); + flutterView.setTranslationY(0f); + } + } + private void stopTrayAnimation() { + if (mTrayTimerTask != null) { + mTrayTimerTask.cancel(); + mTrayTimerTask = null; + } + if (mTrayAnimationTimer != null) { + mTrayAnimationTimer.cancel(); + mTrayAnimationTimer = null; + } + } } \ No newline at end of file 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..35c16390 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 @@ -1,6 +1,5 @@ package flutter.overlay.window.flutter_overlay_window; - import android.view.Gravity; import android.view.WindowManager; @@ -20,7 +19,9 @@ public abstract class WindowSetup { static String positionGravity = "none"; static int notificationVisibility = NotificationCompat.VISIBILITY_PRIVATE; static boolean enableDrag = false; - + static boolean enableBounce = false; + static int bounceHeight = 20; // dp + static int bounceDuration = 1500; // ms interval per tick static void setNotificationVisibility(String name) { if (name.equalsIgnoreCase("visibilityPublic")) { diff --git a/lib/src/overlay_window.dart b/lib/src/overlay_window.dart index fef7d45d..d972ac22 100644 --- a/lib/src/overlay_window.dart +++ b/lib/src/overlay_window.dart @@ -36,6 +36,12 @@ class FlutterOverlayWindow { /// /// `enableDrag` to enable/disable dragging the overlay over the screen and default is "false" /// + /// `enableBounce` to enable/disable bouncing the overlay over the screen and default is "false" + /// + /// `bounceHeight` the bounce height and default is 20 + /// + /// `bounceDuration` the bounce speed and default is 1500 in milliseconds + /// /// `positionGravity` the overlay postion after drag and default is [PositionGravity.none] /// /// `startPosition` the overlay start position and default is null @@ -50,22 +56,25 @@ class FlutterOverlayWindow { bool enableDrag = false, PositionGravity positionGravity = PositionGravity.none, OverlayPosition? startPosition, + bool enableBounce = false, + int bounceHeight = 20, + int bounceDuration = 1500, }) async { - await _channel.invokeMethod( - 'showOverlay', - { - "height": height, - "width": width, - "alignment": alignment.name, - "flag": flag.name, - "overlayTitle": overlayTitle, - "overlayContent": overlayContent, - "enableDrag": enableDrag, - "notificationVisibility": visibility.name, - "positionGravity": positionGravity.name, - "startPosition": startPosition?.toMap(), - }, - ); + await _channel.invokeMethod('showOverlay', { + "height": height, + "width": width, + "alignment": alignment.name, + "flag": flag.name, + "overlayTitle": overlayTitle, + "overlayContent": overlayContent, + "enableDrag": enableDrag, + "notificationVisibility": visibility.name, + "positionGravity": positionGravity.name, + "startPosition": startPosition?.toMap(), + "enableBounce": enableBounce, + "bounceHeight": bounceHeight, + "bounceDuration": bounceDuration, + }); } /// Check if overlay permission is granted @@ -121,15 +130,19 @@ class FlutterOverlayWindow { int width, int height, bool enableDrag, + bool enableBounce, + int bounceHeight, + int bounceDuration, ) async { - final bool? _res = await _overlayChannel.invokeMethod( - 'resizeOverlay', - { - 'width': width, - 'height': height, - 'enableDrag': enableDrag, - }, - ); + final bool? _res = await _overlayChannel + .invokeMethod('resizeOverlay', { + 'width': width, + 'height': height, + 'enableDrag': enableDrag, + 'enableBounce': enableBounce, + 'bounceHeight': bounceHeight, + 'bounceDuration': bounceDuration, + }); return _res; }