Browse Source
将原生插件native_plugin重构为floating_ui_plugin,实现悬浮窗服务独立管理 新增悬浮窗状态管理及内容更新功能,优化代码结构 移除主工程中冗余的悬浮窗相关代码,统一通过插件管理newdev_shunjiawei
13 changed files with 646 additions and 31 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue