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