feat: native tab panel overlay, webapp height no longer reserved
This commit is contained in:
@@ -6,12 +6,15 @@ import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
@@ -19,6 +22,9 @@ import android.view.WindowInsetsController
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.gson.Gson
|
||||
import top.yeij.hearth.app.AndroidAppSource
|
||||
@@ -51,11 +57,12 @@ class MainActivity : Activity() {
|
||||
// Placeholder manifest/catalog URLs; replace with the real server on device
|
||||
private const val MANIFEST_URL = "https://example.com/hearth/manifest.json"
|
||||
private const val CATALOG_URL = "https://example.com/hearth/catalog.json"
|
||||
// 内容 WebView 顶部预留高度(dp):顶栏(约 54dp)+ 横向标签面板(约 45dp),
|
||||
// 取 104dp 留余量,确保标签面板完全不被内容 WebView 覆盖
|
||||
// Content WebView top reserve (dp): topbar (~54dp) + horizontal tab panel
|
||||
// (~45dp); use 104dp with margin so the tab panel is never covered
|
||||
private const val TOPBAR_HEIGHT_DP = 104
|
||||
// 内容 WebView 顶部预留高度(dp):默认只留顶栏(约 54dp);标签面板弹出时
|
||||
// 通过 setContentTopMargin 临时增大,收起后恢复,避免一直压缩 webapp 高度
|
||||
// Content WebView top reserve (dp): default only the topbar (~54dp); the tab
|
||||
// panel temporarily grows it via setContentTopMargin and restores on collapse,
|
||||
// avoiding permanently shrinking the webapp height
|
||||
private const val TOPBAR_HEIGHT_DP = 56
|
||||
// 全局侧边栏收起宽度(dp),内容 WebView 左侧预留,避免挡住侧边栏
|
||||
// Global sidebar collapsed width (dp); content WebViews reserve it on the
|
||||
// left to avoid covering the sidebar
|
||||
@@ -197,6 +204,7 @@ class MainActivity : Activity() {
|
||||
brightness = brightnessProvider,
|
||||
serverUrl = serverUrlProvider,
|
||||
wallpaper = wallpaperProvider,
|
||||
onShowTabPanel = { showNativeTabPanel() },
|
||||
)
|
||||
root = FrameLayout(this)
|
||||
setContentView(root)
|
||||
@@ -347,6 +355,67 @@ class MainActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出原生标签面板(PopupWindow,在内容 WebView 之上,不占用预留高度)
|
||||
// Show the native tab panel (PopupWindow above the content WebView, no reserved height)
|
||||
private fun showNativeTabPanel() {
|
||||
val tabs = webAppContainer.tabs()
|
||||
if (tabs.isEmpty()) return
|
||||
|
||||
val container = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(dp(8), dp(8), dp(8), dp(8))
|
||||
background = GradientDrawable().apply {
|
||||
setColor(0xE6202024.toInt())
|
||||
cornerRadius = dp(16).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
val popup = PopupWindow(container, dp(220), ViewGroup.LayoutParams.WRAP_CONTENT, true)
|
||||
popup.isOutsideTouchable = true
|
||||
tabs.forEach { tab ->
|
||||
val row = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
setPadding(dp(12), dp(8), dp(12), dp(8))
|
||||
if (tab.active) background = GradientDrawable().apply {
|
||||
setColor(Color.parseColor("#ff6900"))
|
||||
cornerRadius = dp(10).toFloat()
|
||||
}
|
||||
}
|
||||
val name = TextView(this).apply {
|
||||
text = tab.name
|
||||
textSize = 14f
|
||||
setTextColor(Color.WHITE)
|
||||
isSingleLine = true
|
||||
ellipsize = TextUtils.TruncateAt.END
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
val close = TextView(this).apply {
|
||||
text = "✕"
|
||||
textSize = 14f
|
||||
setTextColor(Color.WHITE)
|
||||
setPadding(dp(10), 0, 0, 0)
|
||||
setOnClickListener {
|
||||
bridge.closeWebApp(tab.id)
|
||||
popup.dismiss()
|
||||
}
|
||||
}
|
||||
row.addView(name)
|
||||
row.addView(close)
|
||||
row.setOnClickListener {
|
||||
bridge.switchTab(tab.id)
|
||||
popup.dismiss()
|
||||
}
|
||||
container.addView(row)
|
||||
}
|
||||
|
||||
popup.showAtLocation(root, Gravity.TOP or Gravity.END, dp(16), dp(64))
|
||||
}
|
||||
|
||||
// dp 转 px
|
||||
// dp to px
|
||||
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
|
||||
|
||||
// 检测通知使用权是否已授予,未授予时打日志提示(UI 引导入口留后续)
|
||||
// Check whether notification access is granted; log a hint when not
|
||||
// (the settings UI entry is deferred to a later task)
|
||||
|
||||
@@ -69,6 +69,11 @@ class JsBridge(
|
||||
// 壁纸提供接口(base64)
|
||||
// Wallpaper provider (base64)
|
||||
private val wallpaper: WallpaperProvider? = null,
|
||||
// 标签面板回调:由 MainActivity 实现,弹出原生标签面板(PopupWindow,在内容
|
||||
// WebView 之上,不占用预留高度)
|
||||
// Tab panel callback implemented by MainActivity: shows a native tab panel
|
||||
// (PopupWindow above the content WebView, no reserved height)
|
||||
private val onShowTabPanel: (() -> Unit)? = null,
|
||||
) {
|
||||
private val gson = com.google.gson.Gson()
|
||||
|
||||
@@ -212,6 +217,14 @@ class JsBridge(
|
||||
Log.d("HearthBridge", "hideWebApps called")
|
||||
}
|
||||
|
||||
// 弹出原生标签面板(H5 顶栏标签按钮点击时调用)
|
||||
// Show the native tab panel (called when the H5 topbar tab button is tapped)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun showTabPanel() {
|
||||
Log.d("HearthBridge", "showTabPanel called")
|
||||
onShowTabPanel?.invoke()
|
||||
}
|
||||
|
||||
// 清单内存缓存:首次拉取成功后缓存,后续复用(避免 openWebApp 重复 I/O)
|
||||
// 空结果(离线失败)不缓存,下次调用重试拉取,避免空清单被永久缓存
|
||||
// In-memory manifest cache: cache only on a successful non-empty fetch (avoid
|
||||
|
||||
Reference in New Issue
Block a user