From 785db62d31d960b1dace0b0d9ef34e6a3e8ac136 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 14 Jan 2026 11:16:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=82=AC=E6=B5=AE=E7=AA=97):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=AA=97=E5=8F=A3=E5=A4=A7=E5=B0=8F=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=94=B9=E8=BF=9B=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增右下角拖拽调整窗口大小的功能 - 改进onDestroy方法中的资源清理逻辑 - 添加窗口权限检查和安全异常处理 - 优化UI缩放和布局参数管理 --- .../saitong/voitrans/FloatingWindowService.kt | 5 +- .../native_plugin/FloatingWindowService.kt | 252 ++++++++++++++++-- 2 files changed, 230 insertions(+), 27 deletions(-) diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt index b6e861db2..296310038 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt @@ -370,8 +370,9 @@ class FloatingWindowService : Service() { } override fun onDestroy() { - super.onDestroy() floatingView?.let { windowManager?.removeView(it) } serviceInstance = null + methodChannel = null + super.onDestroy() } -} \ No newline at end of file +} diff --git a/local_plugins/native_plugin/android/src/main/kotlin/com/example/native_plugin/FloatingWindowService.kt b/local_plugins/native_plugin/android/src/main/kotlin/com/example/native_plugin/FloatingWindowService.kt index e69e41144..9fb2e5266 100644 --- a/local_plugins/native_plugin/android/src/main/kotlin/com/example/native_plugin/FloatingWindowService.kt +++ b/local_plugins/native_plugin/android/src/main/kotlin/com/example/native_plugin/FloatingWindowService.kt @@ -11,6 +11,7 @@ import android.os.Build import android.os.Handler import android.os.IBinder import android.os.Looper +import android.provider.Settings import android.text.TextUtils import android.util.TypedValue import android.view.Gravity @@ -59,9 +60,13 @@ class FloatingWindowService : Service() { private var expandedView: View? = null private var minimizedTextView: TextView? = null private var closeButton: TextView? = null - private var expandedContainer: LinearLayout? = null + private var expandedContainer: ViewGroup? = null + private var expandedContentLayout: LinearLayout? = null + private var resizeHandleView: View? = null private var baseExpandedWidth: Int = 0 + private var lastEffectiveWidth: Int = 0 + private var lastEffectiveHeight: Int = 0 private var uiScale: Float = 1.0f private val backgroundBaseColor: Int = Color.argb(150, 40, 40, 40) private val uiHandler: Handler = Handler(Looper.getMainLooper()) @@ -90,14 +95,36 @@ class FloatingWindowService : Service() { override fun onDestroy() { stopAllLoading() + uiHandler.removeCallbacksAndMessages(null) removeFloatingWindow() + sourceTextView = null + translatedTextView = null + sourceLoadingView = null + translatedLoadingView = null + languageInfoView = null + minimizedView = null + expandedView = null + minimizedTextView = null + closeButton = null + resizeHandleView = null + expandedContentLayout = null + expandedContainer = null + layoutParams = null + windowManager = null serviceInstance = null + methodChannel = null super.onDestroy() } private fun createFloatingWindow() { windowManager = getSystemService(WINDOW_SERVICE) as? WindowManager ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) { + notifyStateChanged("hidden") + stopSelf() + return + } + floatingView = createFloatingLayout() initViews() setupClickListeners() @@ -122,7 +149,16 @@ class FloatingWindowService : Service() { y = dp(100) } - windowManager?.addView(floatingView, layoutParams) + try { + windowManager?.addView(floatingView, layoutParams) + } catch (_: SecurityException) { + notifyStateChanged("hidden") + stopSelf() + return + } catch (_: Exception) { + stopSelf() + return + } updateViewState(true) } @@ -202,7 +238,7 @@ class FloatingWindowService : Service() { } private fun createExpandedView(): View { - val container = LinearLayout(this) + val root = FrameLayout(this) val maxWidth = dp(360) val minWidth = dp(280) val horizontalMargins = dp(8) * 2 @@ -212,19 +248,28 @@ class FloatingWindowService : Service() { val lp = FrameLayout.LayoutParams(targetWidth, ViewGroup.LayoutParams.WRAP_CONTENT) val margin = dp(8) lp.setMargins(margin, margin, margin, margin) - container.layoutParams = lp - container.orientation = LinearLayout.VERTICAL - container.setPadding(dp(16), dp(16), dp(16), dp(16)) - container.visibility = View.GONE - container.elevation = dp(12).toFloat() - expandedContainer = container + root.layoutParams = lp + root.setPadding(0, 0, 0, 0) + root.visibility = View.GONE + root.elevation = dp(12).toFloat() + expandedContainer = root val bg = GradientDrawable().apply { setColor(backgroundBaseColor) cornerRadius = dp(16).toFloat() setStroke(dp(1), Color.argb(46, 255, 255, 255)) } - container.background = bg + root.background = bg + + val contentLayout = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(16), dp(16), dp(16), dp(16)) + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + expandedContentLayout = contentLayout val titleRow = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL @@ -303,13 +348,101 @@ class FloatingWindowService : Service() { titleRow.addView(spacer, spacerLp) titleRow.addView(closeButton) - container.addView(titleRow) - container.addView(sourceTextView) - container.addView(sourceLoadingView) - container.addView(translatedTextView) - container.addView(translatedLoadingView) + contentLayout.addView(titleRow) + contentLayout.addView(sourceTextView) + contentLayout.addView(sourceLoadingView) + contentLayout.addView(translatedTextView) + contentLayout.addView(translatedLoadingView) + root.addView(contentLayout) + + val handleSize = dp(30) + val resizeHandle = TextView(this).apply { + text = "⤢" + setTextColor(Color.argb(190, 255, 255, 255)) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) + gravity = Gravity.CENTER + background = null + setPadding(0, 0, 0, 0) + scaleX = -1f + } + resizeHandleView = resizeHandle + val handleLp = FrameLayout.LayoutParams(handleSize, handleSize, Gravity.BOTTOM or Gravity.END).apply { + setMargins(0, 0, 0, 0) + } + root.addView(resizeHandle, handleLp) + + resizeHandle.setOnTouchListener(object : View.OnTouchListener { + private var downRawX: Float = 0f + private var downRawY: Float = 0f + private var startWidth: Int = 0 + private var startHeight: Int = 0 + + override fun onTouch(v: View, event: MotionEvent): Boolean { + val expanded = expandedContainer ?: return false + val rootLp = (expanded.layoutParams as? FrameLayout.LayoutParams) ?: return false + val size = v.width.takeIf { it > 0 } ?: return false + val localX = event.x + val localY = event.y + if (!isInBottomRightTriangle(localX, localY, size.toFloat())) { + return false + } + + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + downRawX = event.rawX + downRawY = event.rawY + startWidth = if (rootLp.width > 0) rootLp.width else expanded.width.takeIf { it > 0 } ?: baseExpandedWidth + val resolvedHeight = if (rootLp.height > 0 && rootLp.height != ViewGroup.LayoutParams.WRAP_CONTENT) { + rootLp.height + } else { + expanded.height.takeIf { it > 0 } ?: dp(220) + } + rootLp.height = resolvedHeight + expanded.layoutParams = rootLp + startHeight = resolvedHeight + baseExpandedWidth = startWidth + return true + } + + MotionEvent.ACTION_MOVE -> { + val dx = (event.rawX - downRawX).toInt() + val dy = (event.rawY - downRawY).toInt() + val minW = dp(240) + val maxW = (resources.displayMetrics.widthPixels - dp(8) * 2).coerceAtLeast(minW) + val minH = dp(140) + val maxH = (resources.displayMetrics.heightPixels - dp(12) * 2).coerceAtLeast(minH) + + val nextW = (startWidth + dx).coerceIn(minW, maxW) + val nextH = (startHeight + dy).coerceIn(minH, maxH) + if (rootLp.width != nextW || rootLp.height != nextH) { + rootLp.width = nextW + rootLp.height = nextH + expanded.layoutParams = rootLp + baseExpandedWidth = nextW + floatingView?.post { + val wm = windowManager ?: return@post + val floatingLp = layoutParams ?: return@post + val (effectiveWidth, effectiveHeight) = getEffectiveFloatingSize() + val maxX = resources.displayMetrics.widthPixels - effectiveWidth + val maxY = resources.displayMetrics.heightPixels - effectiveHeight + if (maxX > 0) floatingLp.x = floatingLp.x.coerceIn(0, maxX) + if (maxY > 0) floatingLp.y = floatingLp.y.coerceIn(0, maxY) + wm.updateViewLayout(floatingView, floatingLp) + } + } + return true + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> return true + + else -> return false + } + } + }) + applyUiScale(uiScale) - return container + return root } private fun initViews() { @@ -356,6 +489,9 @@ class FloatingWindowService : Service() { ) { return false } + if (!isMinimized && isTouchingView(resizeHandleView, event.rawX, event.rawY)) { + return false + } dragging = false scaling = false @@ -363,7 +499,17 @@ class FloatingWindowService : Service() { } MotionEvent.ACTION_POINTER_DOWN -> { - if (event.pointerCount >= 2) { + if (!isMinimized && event.pointerCount >= 2) { + expandedContainer?.let { expanded -> + val expandedLp = expanded.layoutParams as? FrameLayout.LayoutParams + if (expandedLp != null && expandedLp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + val lockedHeight = expanded.height.takeIf { it > 0 } ?: dp(220) + expandedLp.height = lockedHeight + expanded.layoutParams = expandedLp + } + val width = (expanded.layoutParams as? FrameLayout.LayoutParams)?.width ?: 0 + if (width > 0) baseExpandedWidth = width + } scaling = true dragging = false initialSpan = pointerSpan(event) @@ -379,18 +525,25 @@ class FloatingWindowService : Service() { if (initialSpan > 0f && span > 0f) { val next = (initialScale * (span / initialSpan)).coerceIn(0.85f, 1.4f) setUiScale(next) + + val (effectiveWidth, effectiveHeight) = getEffectiveFloatingSize() + val maxX = resources.displayMetrics.widthPixels - effectiveWidth + val maxY = resources.displayMetrics.heightPixels - effectiveHeight + if (maxX > 0) lp.x = lp.x.coerceIn(0, maxX) + if (maxY > 0) lp.y = lp.y.coerceIn(0, maxY) + wm.updateViewLayout(floatingView, lp) } return true } + val dx = (event.rawX - initialTouchX).toInt() val dy = (event.rawY - initialTouchY).toInt() if (!dragging && (kotlin.math.abs(dx) > threshold || kotlin.math.abs(dy) > threshold)) { dragging = true } - val maxX = - resources.displayMetrics.widthPixels - (floatingView?.width ?: 0) - val maxY = - resources.displayMetrics.heightPixels - (floatingView?.height ?: 0) + val (effectiveWidth, effectiveHeight) = getEffectiveFloatingSize() + val maxX = resources.displayMetrics.widthPixels - effectiveWidth + val maxY = resources.displayMetrics.heightPixels - effectiveHeight var nextX = initialX + dx var nextY = initialY + dy if (maxX > 0) nextX = nextX.coerceIn(0, maxX) @@ -549,26 +702,70 @@ class FloatingWindowService : Service() { if (baseExpandedWidth <= 0) return languageInfoView?.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f * scale) + languageInfoView?.setPadding( + (dp(10) * scale).toInt().coerceIn(dp(6), dp(16)), + (dp(6) * scale).toInt().coerceIn(dp(4), dp(12)), + (dp(10) * scale).toInt().coerceIn(dp(6), dp(16)), + (dp(6) * scale).toInt().coerceIn(dp(4), dp(12)) + ) sourceTextView?.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f * scale) translatedTextView?.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f * scale) sourceLoadingView?.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f * scale) translatedLoadingView?.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f * scale) - val buttonTextSizeSp = (18f * scale).coerceIn(14f, 22f) - closeButton?.setTextSize(TypedValue.COMPLEX_UNIT_SP, buttonTextSizeSp) + val closeTextSizeSp = (18f * scale).coerceIn(14f, 22f) + closeButton?.setTextSize(TypedValue.COMPLEX_UNIT_SP, closeTextSizeSp) val closeHPad = (dp(8) * scale).toInt().coerceIn(dp(6), dp(16)) val closeVPad = (dp(8) * scale).toInt().coerceIn(dp(4), dp(12)) closeButton?.setPadding(closeHPad, closeVPad, closeHPad, closeVPad) - val padding = (dp(16) * scale).toInt().coerceIn(dp(12), dp(24)) - container.setPadding(padding, padding, padding, padding) + container.setPadding(0, 0, 0, 0) + val contentPadding = (dp(16) * scale).toInt().coerceIn(dp(12), dp(24)) + expandedContentLayout?.setPadding(contentPadding, contentPadding, contentPadding, contentPadding) val maxLines = if (scale >= 1.15f) 4 else 3 sourceTextView?.maxLines = maxLines translatedTextView?.maxLines = maxLines } + private fun getEffectiveFloatingSize(): Pair { + val view = floatingView + if (view != null) { + val w = view.width + val h = view.height + if (w > 0 && h > 0) { + lastEffectiveWidth = w + lastEffectiveHeight = h + return w to h + } + } + + layoutParams?.let { lp -> + val w = lp.width + val h = lp.height + if (w > 0 && h > 0) { + lastEffectiveWidth = w + lastEffectiveHeight = h + return w to h + } + } + + (expandedContainer?.layoutParams as? FrameLayout.LayoutParams)?.let { lp -> + val w = lp.width + val h = lp.height + if (w > 0 && h > 0) { + lastEffectiveWidth = w + lastEffectiveHeight = h + return w to h + } + } + + if (lastEffectiveWidth <= 0) lastEffectiveWidth = dp(280) + if (lastEffectiveHeight <= 0) lastEffectiveHeight = dp(220) + return lastEffectiveWidth to lastEffectiveHeight + } + private fun startSourceLoading() { val view = sourceLoadingView ?: return if (view.visibility == View.VISIBLE && sourceDotsRunnable != null) return @@ -635,4 +832,9 @@ class FloatingWindowService : Service() { val dy = event.getY(0) - event.getY(1) return kotlin.math.sqrt(dx * dx + dy * dy) } + + private fun isInBottomRightTriangle(x: Float, y: Float, size: Float): Boolean { + if (x < 0f || y < 0f || x > size || y > size) return false + return x >= size - y + } }