Browse Source

refactor(floating_ui): 重构悬浮窗插件并重命名为floating_ui_plugin

将原生插件native_plugin重构为floating_ui_plugin,实现悬浮窗服务独立管理
新增悬浮窗状态管理及内容更新功能,优化代码结构
移除主工程中冗余的悬浮窗相关代码,统一通过插件管理
newdev_shunjiawei
lxm 9 months ago
parent
commit
1b9eecf440
  1. 6
      android/app/src/main/AndroidManifest.xml
  2. 4
      android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt
  3. 6
      android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt
  4. 18
      lib/core/services/floating_window_manager.dart
  5. 2
      lib/modules/meeting/controllers/meeting_record_controller.dart
  6. 2
      lib/modules/translation/controllers/translation_controller.dart
  7. 3
      local_plugins/native_plugin/android/src/main/AndroidManifest.xml
  8. 405
      local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/FloatingWindowService.java
  9. 190
      local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java
  10. 7
      local_plugins/native_plugin/ios/floating_ui_plugin.podspec
  11. 30
      local_plugins/native_plugin/lib/native_plugin.dart
  12. 2
      local_plugins/native_plugin/pubspec.yaml
  13. 2
      pubspec.yaml

6
android/app/src/main/AndroidManifest.xml

@ -136,12 +136,6 @@
android:value="78e7946dd602778df82c6ae143a88184"/> android:value="78e7946dd602778df82c6ae143a88184"/>
<service android:name="com.amap.api.location.APSService"></service> <service android:name="com.amap.api.location.APSService"></service>
<!-- 悬浮窗服务 -->
<service
android:name="com.saitong.voitrans.FloatingWindowService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync"/>
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

4
android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt

@ -104,10 +104,6 @@ class MainActivity: FlutterActivity() {
Log.d(TAG, "📦 注册 Flutter 插件") Log.d(TAG, "📦 注册 Flutter 插件")
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine)
// 注册悬浮窗相关插件
flutterEngine.plugins.add(FloatingWindowPlugin())
flutterEngine.plugins.add(OverlayPermissionPlugin())
} else { } else {
Log.d(TAG, "⚠️ 插件已注册,跳过重复注册") Log.d(TAG, "⚠️ 插件已注册,跳过重复注册")
} }

6
android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt

@ -8,8 +8,6 @@ import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugins.GeneratedPluginRegistrant import io.flutter.plugins.GeneratedPluginRegistrant
import com.saitong.voitrans.FloatingWindowPlugin
import com.saitong.voitrans.OverlayPermissionPlugin
// Application.kt // Application.kt
class MyApp : FlutterApplication() { class MyApp : FlutterApplication() {
@ -70,10 +68,6 @@ class MyApp : FlutterApplication() {
// 注册插件(只在这里注册一次) // 注册插件(只在这里注册一次)
Log.d(TAG, "📦 注册 Flutter 插件") Log.d(TAG, "📦 注册 Flutter 插件")
GeneratedPluginRegistrant.registerWith(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine)
// 注册悬浮窗相关插件
flutterEngine.plugins.add(FloatingWindowPlugin())
flutterEngine.plugins.add(OverlayPermissionPlugin())
try { try {
Log.d(TAG, "🚀 开始执行 Dart 入口点") Log.d(TAG, "🚀 开始执行 Dart 入口点")

18
lib/core/services/floating_window_manager.dart

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:floating_ui_plugin/native_plugin.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../utils/logger.dart'; import '../utils/logger.dart';
import '../utils/overlay_permission_util.dart'; import '../utils/overlay_permission_util.dart';
@ -45,7 +45,9 @@ class FloatingWindowData {
/// 悬浮窗管理器 /// 悬浮窗管理器
class FloatingWindowManager extends GetxController { class FloatingWindowManager extends GetxController {
static const String TAG = 'FloatingWindowManager'; static const String TAG = 'FloatingWindowManager';
static const MethodChannel _channel = MethodChannel('floating_window'); static MethodChannel get _channel => TranslationFloatingWindowApi.channel;
final TranslationFloatingWindowApi _api = TranslationFloatingWindowApi();
final _isEnabled = false.obs; final _isEnabled = false.obs;
final _currentState = FloatingWindowState.hidden.obs; final _currentState = FloatingWindowState.hidden.obs;
@ -102,7 +104,7 @@ class FloatingWindowManager extends GetxController {
} }
try { try {
await _channel.invokeMethod('enableFloatingWindow'); await _api.enable();
_isEnabled.value = true; _isEnabled.value = true;
Logger.info('悬浮窗功能已启用'); Logger.info('悬浮窗功能已启用');
return true; return true;
@ -115,7 +117,7 @@ class FloatingWindowManager extends GetxController {
/// 禁用悬浮窗功能 /// 禁用悬浮窗功能
Future<void> disableFloatingWindow() async { Future<void> disableFloatingWindow() async {
try { try {
await _channel.invokeMethod('disableFloatingWindow'); await _api.disable();
_isEnabled.value = false; _isEnabled.value = false;
_currentState.value = FloatingWindowState.hidden; _currentState.value = FloatingWindowState.hidden;
Logger.info('悬浮窗功能已禁用'); Logger.info('悬浮窗功能已禁用');
@ -129,7 +131,7 @@ class FloatingWindowManager extends GetxController {
if (!_isEnabled.value) return; if (!_isEnabled.value) return;
try { try {
await _channel.invokeMethod('showFloatingWindow'); await _api.show();
_currentState.value = FloatingWindowState.minimized; _currentState.value = FloatingWindowState.minimized;
Logger.info('悬浮窗已显示'); Logger.info('悬浮窗已显示');
} catch (e) { } catch (e) {
@ -140,7 +142,7 @@ class FloatingWindowManager extends GetxController {
/// 隐藏悬浮窗 /// 隐藏悬浮窗
Future<void> hideFloatingWindow() async { Future<void> hideFloatingWindow() async {
try { try {
await _channel.invokeMethod('hideFloatingWindow'); await _api.hide();
_currentState.value = FloatingWindowState.hidden; _currentState.value = FloatingWindowState.hidden;
Logger.info('悬浮窗已隐藏'); Logger.info('悬浮窗已隐藏');
} catch (e) { } catch (e) {
@ -156,7 +158,7 @@ class FloatingWindowManager extends GetxController {
} }
try { try {
await _channel.invokeMethod('updateContent', data.toMap()); await _api.updateContent(data.toMap());
Logger.info('悬浮窗内容已更新'); Logger.info('悬浮窗内容已更新');
} catch (e) { } catch (e) {
Logger.error('更新悬浮窗内容失败: $e'); Logger.error('更新悬浮窗内容失败: $e');
@ -166,7 +168,7 @@ class FloatingWindowManager extends GetxController {
/// 设置悬浮窗位置 /// 设置悬浮窗位置
Future<void> setFloatingWindowPosition(double x, double y) async { Future<void> setFloatingWindowPosition(double x, double y) async {
try { try {
await _channel.invokeMethod('setPosition', {'x': x, 'y': y}); await _api.setPosition(x: x, y: y);
} catch (e) { } catch (e) {
Logger.error('设置悬浮窗位置失败: $e'); Logger.error('设置悬浮窗位置失败: $e');
} }

2
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -10,7 +10,7 @@ import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:native_plugin/native_plugin.dart'; import 'package:floating_ui_plugin/native_plugin.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';

2
lib/modules/translation/controllers/translation_controller.dart

@ -6,7 +6,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:native_plugin/native_plugin.dart'; import 'package:floating_ui_plugin/native_plugin.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';

3
local_plugins/native_plugin/android/src/main/AndroidManifest.xml

@ -7,5 +7,8 @@
<service <service
android:name=".RecordingFloatingService" android:name=".RecordingFloatingService"
android:exported="false" /> android:exported="false" />
<service
android:name=".FloatingWindowService"
android:exported="false" />
</application> </application>
</manifest> </manifest>

405
local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/FloatingWindowService.java

@ -0,0 +1,405 @@
package com.example.native_plugin;
import android.app.Service;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import io.flutter.plugin.common.MethodChannel;
public class FloatingWindowService extends Service {
static final String ACTION_UPDATE_CONTENT = "UPDATE_CONTENT";
static final String ACTION_SET_POSITION = "SET_POSITION";
static final String EXTRA_SOURCE_TEXT = "source_text";
static final String EXTRA_TRANSLATED_TEXT = "translated_text";
static final String EXTRA_SOURCE_LANGUAGE = "source_language";
static final String EXTRA_TARGET_LANGUAGE = "target_language";
static final String EXTRA_TIMESTAMP = "timestamp";
static final String EXTRA_IS_INTERMEDIATE = "is_intermediate";
static final String EXTRA_X = "x";
static final String EXTRA_Y = "y";
static volatile MethodChannel methodChannel;
static volatile FloatingWindowService serviceInstance;
private WindowManager windowManager;
private View floatingView;
private WindowManager.LayoutParams layoutParams;
private boolean isMinimized = true;
private TextView sourceTextView;
private TextView translatedTextView;
private TextView languageInfoView;
private TextView timestampView;
private View minimizedView;
private View expandedView;
private TextView closeButton;
@Override
public void onCreate() {
super.onCreate();
serviceInstance = this;
createFloatingWindow();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
String action = intent.getAction();
if (ACTION_UPDATE_CONTENT.equals(action)) {
updateContent(intent);
} else if (ACTION_SET_POSITION.equals(action)) {
setPosition(intent);
}
}
return START_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
removeFloatingWindow();
serviceInstance = null;
super.onDestroy();
}
private void createFloatingWindow() {
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
if (windowManager == null) return;
floatingView = createFloatingLayout();
initViews();
setupClickListeners();
setupDragListener();
layoutParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
: WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT
);
layoutParams.gravity = Gravity.TOP | Gravity.START;
layoutParams.x = dp(100);
layoutParams.y = dp(100);
windowManager.addView(floatingView, layoutParams);
updateViewState(true);
}
private View createFloatingLayout() {
FrameLayout root = new FrameLayout(this);
root.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
minimizedView = createMinimizedView();
expandedView = createExpandedView();
root.addView(minimizedView);
root.addView(expandedView);
return root;
}
private View createMinimizedView() {
FrameLayout container = new FrameLayout(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(dp(60), dp(60));
int margin = dp(8);
lp.setMargins(margin, margin, margin, margin);
container.setLayoutParams(lp);
GradientDrawable bg = new GradientDrawable();
bg.setColor(0xFFE3F2FD);
bg.setCornerRadius(dp(30));
container.setBackground(bg);
container.setElevation(dp(8));
TextView icon = new TextView(this);
icon.setText("译");
icon.setTextColor(0xFF1976D2);
icon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
icon.setGravity(Gravity.CENTER);
icon.setTypeface(Typeface.DEFAULT_BOLD);
container.addView(icon, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
return container;
}
private View createExpandedView() {
LinearLayout container = new LinearLayout(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(dp(320), ViewGroup.LayoutParams.WRAP_CONTENT);
int margin = dp(8);
lp.setMargins(margin, margin, margin, margin);
container.setLayoutParams(lp);
container.setOrientation(LinearLayout.VERTICAL);
container.setPadding(dp(16), dp(16), dp(16), dp(16));
container.setVisibility(View.GONE);
container.setElevation(dp(12));
GradientDrawable bg = new GradientDrawable();
bg.setColor(Color.WHITE);
bg.setCornerRadius(dp(16));
container.setBackground(bg);
LinearLayout titleRow = new LinearLayout(this);
titleRow.setOrientation(LinearLayout.HORIZONTAL);
titleRow.setGravity(Gravity.CENTER_VERTICAL);
titleRow.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
TextView title = new TextView(this);
title.setText("实时翻译");
title.setTextColor(0xFF333333);
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
title.setTypeface(Typeface.DEFAULT_BOLD);
LinearLayout.LayoutParams titleLp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
title.setLayoutParams(titleLp);
closeButton = new TextView(this);
closeButton.setText("✕");
closeButton.setTextColor(0xFF666666);
closeButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
closeButton.setGravity(Gravity.CENTER);
closeButton.setPadding(dp(8), dp(8), dp(8), dp(8));
titleRow.addView(title);
titleRow.addView(closeButton);
languageInfoView = new TextView(this);
languageInfoView.setText("中文 → 英文");
languageInfoView.setTextColor(0xFF666666);
languageInfoView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
languageInfoView.setPadding(0, dp(8), 0, dp(8));
sourceTextView = new TextView(this);
sourceTextView.setText("正在识别...");
sourceTextView.setTextColor(0xFF333333);
sourceTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
sourceTextView.setPadding(0, dp(8), 0, dp(4));
sourceTextView.setMaxLines(3);
sourceTextView.setEllipsize(TextUtils.TruncateAt.END);
translatedTextView = new TextView(this);
translatedTextView.setText("翻译中...");
translatedTextView.setTextColor(0xFF1976D2);
translatedTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
translatedTextView.setPadding(0, dp(4), 0, dp(8));
translatedTextView.setMaxLines(3);
translatedTextView.setEllipsize(TextUtils.TruncateAt.END);
timestampView = new TextView(this);
timestampView.setText("");
timestampView.setTextColor(0xFF999999);
timestampView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
timestampView.setGravity(Gravity.END);
container.addView(titleRow);
container.addView(languageInfoView);
container.addView(sourceTextView);
container.addView(translatedTextView);
container.addView(timestampView);
return container;
}
private void initViews() {
if (!(floatingView instanceof ViewGroup)) return;
// Views already created and stored in fields
}
private void setupClickListeners() {
if (floatingView != null) {
floatingView.setOnLongClickListener(v -> {
stopSelf();
notifyClosed();
return true;
});
}
if (closeButton != null) {
closeButton.setOnClickListener(v -> {
stopSelf();
notifyClosed();
});
}
}
private void setupDragListener() {
if (floatingView == null) return;
final int threshold = dp(4);
floatingView.setOnTouchListener(new View.OnTouchListener() {
private int initialX;
private int initialY;
private float initialTouchX;
private float initialTouchY;
private boolean dragging;
@Override
public boolean onTouch(View v, MotionEvent event) {
if (layoutParams == null || windowManager == null) return false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = layoutParams.x;
initialY = layoutParams.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
dragging = false;
return true;
case MotionEvent.ACTION_MOVE: {
int dx = (int) (event.getRawX() - initialTouchX);
int dy = (int) (event.getRawY() - initialTouchY);
if (!dragging && (Math.abs(dx) > threshold || Math.abs(dy) > threshold)) {
dragging = true;
}
final int maxX = getResources().getDisplayMetrics().widthPixels - (floatingView == null ? 0 : floatingView.getWidth());
final int maxY = getResources().getDisplayMetrics().heightPixels - (floatingView == null ? 0 : floatingView.getHeight());
int nextX = initialX + dx;
int nextY = initialY + dy;
if (maxX > 0) nextX = Math.max(0, Math.min(nextX, maxX));
if (maxY > 0) nextY = Math.max(0, Math.min(nextY, maxY));
layoutParams.x = nextX;
layoutParams.y = nextY;
windowManager.updateViewLayout(floatingView, layoutParams);
return true;
}
case MotionEvent.ACTION_UP:
if (!dragging) {
updateViewState(!isMinimized);
}
dragging = false;
return true;
default:
return false;
}
}
});
}
private void updateViewState(boolean minimized) {
isMinimized = minimized;
if (minimizedView != null) minimizedView.setVisibility(minimized ? View.VISIBLE : View.GONE);
if (expandedView != null) expandedView.setVisibility(minimized ? View.GONE : View.VISIBLE);
notifyStateChanged(minimized ? "minimized" : "expanded");
}
private void updateContent(Intent intent) {
String sourceText = intent.getStringExtra(EXTRA_SOURCE_TEXT);
String translatedText = intent.getStringExtra(EXTRA_TRANSLATED_TEXT);
String sourceLang = intent.getStringExtra(EXTRA_SOURCE_LANGUAGE);
String targetLang = intent.getStringExtra(EXTRA_TARGET_LANGUAGE);
long timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, 0L);
boolean isIntermediate = intent.getBooleanExtra(EXTRA_IS_INTERMEDIATE, false);
if (languageInfoView != null) {
String langText = (sourceLang == null ? "" : sourceLang) + " → " + (targetLang == null ? "" : targetLang);
languageInfoView.setText(langText);
}
if (sourceTextView != null) {
String value = sourceText == null ? "" : sourceText;
sourceTextView.setText(TextUtils.isEmpty(value) ? "正在识别..." : value);
}
if (translatedTextView != null) {
String value = translatedText == null ? "" : translatedText;
translatedTextView.setText(TextUtils.isEmpty(value) ? "翻译中..." : value);
translatedTextView.setAlpha(isIntermediate ? 0.7f : 1.0f);
}
if (timestampView != null) {
timestampView.setText(timestamp > 0 ? formatTimestamp(timestamp) : "");
}
if (isMinimized) {
// keep minimized, content updated for next expand
}
}
private void setPosition(Intent intent) {
if (layoutParams == null || windowManager == null) return;
int x = intent.getIntExtra(EXTRA_X, layoutParams.x);
int y = intent.getIntExtra(EXTRA_Y, layoutParams.y);
layoutParams.x = x;
layoutParams.y = y;
windowManager.updateViewLayout(floatingView, layoutParams);
}
private void removeFloatingWindow() {
if (windowManager != null && floatingView != null) {
try {
windowManager.removeView(floatingView);
} catch (Exception ignored) {
}
floatingView = null;
}
notifyStateChanged("hidden");
}
private void notifyStateChanged(String state) {
MethodChannel channel = methodChannel;
if (channel == null) return;
Map<String, Object> args = new HashMap<>();
args.put("state", state);
channel.invokeMethod("onFloatingWindowStateChanged", args);
}
private void notifyClosed() {
MethodChannel channel = methodChannel;
if (channel == null) return;
channel.invokeMethod("onFloatingWindowClosed", null);
}
private void notifyClicked() {
MethodChannel channel = methodChannel;
if (channel == null) return;
channel.invokeMethod("onFloatingWindowClicked", null);
}
private String formatTimestamp(long ts) {
if (ts <= 0) return "";
try {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
return sdf.format(new Date(ts));
} catch (Exception ignored) {
return "";
}
}
private int dp(int value) {
return (int) (value * getResources().getDisplayMetrics().density);
}
}

190
local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java

@ -4,8 +4,14 @@ import androidx.annotation.NonNull;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.provider.Settings;
import java.util.Map;
import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.EventChannel;
@ -21,6 +27,8 @@ public class NativePlugin implements FlutterPlugin, MethodCallHandler, EventChan
/// This local reference serves to register the plugin with the Flutter Engine and unregister it /// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity /// when the Flutter Engine is detached from the Activity
private MethodChannel channel; private MethodChannel channel;
private MethodChannel floatingWindowChannel;
private MethodChannel overlayPermissionChannel;
private EventChannel recordingFloatingActionsChannel; private EventChannel recordingFloatingActionsChannel;
private Context applicationContext; private Context applicationContext;
@ -36,6 +44,13 @@ public class NativePlugin implements FlutterPlugin, MethodCallHandler, EventChan
recordingFloatingActionsChannel = recordingFloatingActionsChannel =
new EventChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin/recording_floating_actions"); new EventChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin/recording_floating_actions");
recordingFloatingActionsChannel.setStreamHandler(this); recordingFloatingActionsChannel.setStreamHandler(this);
floatingWindowChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "floating_window");
floatingWindowChannel.setMethodCallHandler((call, result) -> handleFloatingWindowCall(call, result));
FloatingWindowService.methodChannel = floatingWindowChannel;
overlayPermissionChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "overlay_permission");
overlayPermissionChannel.setMethodCallHandler((call, result) -> handleOverlayPermissionCall(call, result));
} }
@Override @Override
@ -91,6 +106,172 @@ public class NativePlugin implements FlutterPlugin, MethodCallHandler, EventChan
applicationContext.startService(intent); applicationContext.startService(intent);
} }
private void handleFloatingWindowCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "enableFloatingWindow":
result.success(startFloatingWindowService(null, null));
break;
case "disableFloatingWindow":
result.success(stopFloatingWindowService());
break;
case "showFloatingWindow":
result.success(true);
break;
case "hideFloatingWindow":
try {
FloatingWindowService instance = FloatingWindowService.serviceInstance;
if (instance != null) {
instance.stopSelf();
} else {
stopFloatingWindowService();
}
result.success(true);
} catch (Exception e) {
result.error("HIDE_ERROR", "Failed to hide floating window: " + e.getMessage(), null);
}
break;
case "updateContent":
try {
@SuppressWarnings("unchecked")
Map<String, Object> args = (Map<String, Object>) call.arguments;
result.success(startFloatingWindowService(FloatingWindowService.ACTION_UPDATE_CONTENT, args));
} catch (Exception e) {
result.error("UPDATE_ERROR", "Failed to update content: " + e.getMessage(), null);
}
break;
case "setPosition":
try {
@SuppressWarnings("unchecked")
Map<String, Object> args = (Map<String, Object>) call.arguments;
result.success(startFloatingWindowService(FloatingWindowService.ACTION_SET_POSITION, args));
} catch (Exception e) {
result.error("POSITION_ERROR", "Failed to set position: " + e.getMessage(), null);
}
break;
default:
result.notImplemented();
break;
}
}
private boolean startFloatingWindowService(String action, Map<String, Object> args) {
if (applicationContext == null) return false;
try {
Intent intent = new Intent(applicationContext, FloatingWindowService.class);
if (action != null) {
intent.setAction(action);
}
if (args != null) {
if (FloatingWindowService.ACTION_UPDATE_CONTENT.equals(action)) {
Object sourceText = args.get("sourceText");
Object translatedText = args.get("translatedText");
Object sourceLanguage = args.get("sourceLanguage");
Object targetLanguage = args.get("targetLanguage");
Object timestamp = args.get("timestamp");
Object isIntermediate = args.get("isIntermediate");
if (sourceText instanceof String) {
intent.putExtra(FloatingWindowService.EXTRA_SOURCE_TEXT, (String) sourceText);
}
if (translatedText instanceof String) {
intent.putExtra(FloatingWindowService.EXTRA_TRANSLATED_TEXT, (String) translatedText);
}
if (sourceLanguage instanceof String) {
intent.putExtra(FloatingWindowService.EXTRA_SOURCE_LANGUAGE, (String) sourceLanguage);
}
if (targetLanguage instanceof String) {
intent.putExtra(FloatingWindowService.EXTRA_TARGET_LANGUAGE, (String) targetLanguage);
}
if (timestamp instanceof Number) {
intent.putExtra(FloatingWindowService.EXTRA_TIMESTAMP, ((Number) timestamp).longValue());
}
if (isIntermediate instanceof Boolean) {
intent.putExtra(FloatingWindowService.EXTRA_IS_INTERMEDIATE, (Boolean) isIntermediate);
}
} else if (FloatingWindowService.ACTION_SET_POSITION.equals(action)) {
Object x = args.get("x");
Object y = args.get("y");
if (x instanceof Number) {
intent.putExtra(FloatingWindowService.EXTRA_X, ((Number) x).intValue());
}
if (y instanceof Number) {
intent.putExtra(FloatingWindowService.EXTRA_Y, ((Number) y).intValue());
}
}
}
applicationContext.startService(intent);
return true;
} catch (Exception e) {
return false;
}
}
private boolean stopFloatingWindowService() {
if (applicationContext == null) return false;
try {
Intent intent = new Intent(applicationContext, FloatingWindowService.class);
return applicationContext.stopService(intent);
} catch (Exception e) {
return false;
}
}
private void handleOverlayPermissionCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "hasOverlayPermission":
result.success(hasOverlayPermission());
break;
case "requestOverlayPermission":
result.success(requestOverlayPermission());
break;
default:
result.notImplemented();
break;
}
}
private boolean hasOverlayPermission() {
if (applicationContext == null) return false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(applicationContext);
}
return true;
}
private boolean requestOverlayPermission() {
if (applicationContext == null) return false;
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
if (hasOverlayPermission()) {
return true;
}
boolean openedBySystem = startActivitySafe(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
.setData(Uri.parse("package:" + applicationContext.getPackageName())));
if (openedBySystem) return true;
return startActivitySafe(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:" + applicationContext.getPackageName())));
} catch (Exception e) {
return false;
}
}
private boolean startActivitySafe(@NonNull Intent intent) {
if (applicationContext == null) return false;
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PackageManager pm = applicationContext.getPackageManager();
if (intent.resolveActivity(pm) == null) return false;
try {
applicationContext.startActivity(intent);
return true;
} catch (Exception e) {
return false;
}
}
static void emitRecordingFloatingAction(String action) { static void emitRecordingFloatingAction(String action) {
final EventChannel.EventSink sink = recordingFloatingActionSink; final EventChannel.EventSink sink = recordingFloatingActionSink;
if (sink == null) return; if (sink == null) return;
@ -115,10 +296,19 @@ public class NativePlugin implements FlutterPlugin, MethodCallHandler, EventChan
@Override @Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null); channel.setMethodCallHandler(null);
if (floatingWindowChannel != null) {
floatingWindowChannel.setMethodCallHandler(null);
}
if (overlayPermissionChannel != null) {
overlayPermissionChannel.setMethodCallHandler(null);
}
FloatingWindowService.methodChannel = null;
if (recordingFloatingActionsChannel != null) { if (recordingFloatingActionsChannel != null) {
recordingFloatingActionsChannel.setStreamHandler(null); recordingFloatingActionsChannel.setStreamHandler(null);
} }
recordingFloatingActionsChannel = null; recordingFloatingActionsChannel = null;
floatingWindowChannel = null;
overlayPermissionChannel = null;
applicationContext = null; applicationContext = null;
} }
} }

7
local_plugins/native_plugin/ios/native_plugin.podspec → local_plugins/native_plugin/ios/floating_ui_plugin.podspec

@ -1,9 +1,9 @@
# #
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint native_plugin.podspec` to validate before publishing. # Run `pod lib lint floating_ui_plugin.podspec` to validate before publishing.
# #
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'native_plugin' s.name = 'floating_ui_plugin'
s.version = '0.0.1' s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.' s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC s.description = <<-DESC
@ -25,5 +25,6 @@ A new Flutter plugin project.
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information, # plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'native_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']} # s.resource_bundles = {'floating_ui_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end end

30
local_plugins/native_plugin/lib/native_plugin.dart

@ -1,4 +1,5 @@
import 'native_plugin_platform_interface.dart'; import 'native_plugin_platform_interface.dart';
import 'package:flutter/services.dart';
class NativePlugin { class NativePlugin {
Future<String?> getPlatformVersion() { Future<String?> getPlatformVersion() {
@ -48,3 +49,32 @@ class NativePlugin {
Stream<String> get recordingFloatingActionStream => Stream<String> get recordingFloatingActionStream =>
NativePluginPlatform.instance.recordingFloatingActionStream; NativePluginPlatform.instance.recordingFloatingActionStream;
} }
class TranslationFloatingWindowApi {
static const MethodChannel channel = MethodChannel('floating_window');
Future<bool> enable() async {
final result = await channel.invokeMethod<bool>('enableFloatingWindow');
return result ?? false;
}
Future<void> disable() {
return channel.invokeMethod<void>('disableFloatingWindow');
}
Future<void> show() {
return channel.invokeMethod<void>('showFloatingWindow');
}
Future<void> hide() {
return channel.invokeMethod<void>('hideFloatingWindow');
}
Future<void> updateContent(Map<String, dynamic> data) {
return channel.invokeMethod<void>('updateContent', data);
}
Future<void> setPosition({required double x, required double y}) {
return channel.invokeMethod<void>('setPosition', {'x': x, 'y': y});
}
}

2
local_plugins/native_plugin/pubspec.yaml

@ -1,4 +1,4 @@
name: native_plugin name: floating_ui_plugin
description: "A new Flutter plugin project." description: "A new Flutter plugin project."
version: 0.0.1 version: 0.0.1
homepage: homepage:

2
pubspec.yaml

@ -116,7 +116,7 @@ dependencies:
path: local_plugins/user_portrait path: local_plugins/user_portrait
qq_music: qq_music:
path: local_plugins/qq_music path: local_plugins/qq_music
native_plugin: floating_ui_plugin:
path: local_plugins/native_plugin path: local_plugins/native_plugin
test: test:
path: local_plugins/test path: local_plugins/test

Loading…
Cancel
Save