feat: native tab panel overlay, webapp height no longer reserved
This commit is contained in:
@@ -28,9 +28,9 @@ window.showTopbar = function (tabs) {
|
|||||||
fwd.onclick = () => bridge.call('webGoForward');
|
fwd.onclick = () => bridge.call('webGoForward');
|
||||||
reload.onclick = () => bridge.call('webReload');
|
reload.onclick = () => bridge.call('webReload');
|
||||||
}
|
}
|
||||||
// 每次刷新都重绑,避免捕获旧 tabs(陈旧闭包)
|
// 标签按钮:调原生弹出标签面板(PopupWindow 在内容 WebView 之上)
|
||||||
// Rebind on every refresh to avoid capturing stale tabs (stale closure)
|
// Tab button: show the native tab panel (PopupWindow above the content WebView)
|
||||||
document.getElementById('tb-tabs').onclick = () => showTabPanel(tabs);
|
document.getElementById('tb-tabs').onclick = () => bridge.call('showTabPanel');
|
||||||
document.getElementById('tb-tabs').textContent = `标签 (${tabs.length})`;
|
document.getElementById('tb-tabs').textContent = `标签 (${tabs.length})`;
|
||||||
const active = tabs.find(t => t.active);
|
const active = tabs.find(t => t.active);
|
||||||
document.getElementById('tb-title').textContent = active ? active.name : '';
|
document.getElementById('tb-title').textContent = active ? active.name : '';
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import android.content.Intent
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
@@ -19,6 +22,9 @@ import android.view.WindowInsetsController
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import top.yeij.hearth.app.AndroidAppSource
|
import top.yeij.hearth.app.AndroidAppSource
|
||||||
@@ -51,11 +57,12 @@ class MainActivity : Activity() {
|
|||||||
// Placeholder manifest/catalog URLs; replace with the real server on device
|
// 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 MANIFEST_URL = "https://example.com/hearth/manifest.json"
|
||||||
private const val CATALOG_URL = "https://example.com/hearth/catalog.json"
|
private const val CATALOG_URL = "https://example.com/hearth/catalog.json"
|
||||||
// 内容 WebView 顶部预留高度(dp):顶栏(约 54dp)+ 横向标签面板(约 45dp),
|
// 内容 WebView 顶部预留高度(dp):默认只留顶栏(约 54dp);标签面板弹出时
|
||||||
// 取 104dp 留余量,确保标签面板完全不被内容 WebView 覆盖
|
// 通过 setContentTopMargin 临时增大,收起后恢复,避免一直压缩 webapp 高度
|
||||||
// Content WebView top reserve (dp): topbar (~54dp) + horizontal tab panel
|
// Content WebView top reserve (dp): default only the topbar (~54dp); the tab
|
||||||
// (~45dp); use 104dp with margin so the tab panel is never covered
|
// panel temporarily grows it via setContentTopMargin and restores on collapse,
|
||||||
private const val TOPBAR_HEIGHT_DP = 104
|
// avoiding permanently shrinking the webapp height
|
||||||
|
private const val TOPBAR_HEIGHT_DP = 56
|
||||||
// 全局侧边栏收起宽度(dp),内容 WebView 左侧预留,避免挡住侧边栏
|
// 全局侧边栏收起宽度(dp),内容 WebView 左侧预留,避免挡住侧边栏
|
||||||
// Global sidebar collapsed width (dp); content WebViews reserve it on the
|
// Global sidebar collapsed width (dp); content WebViews reserve it on the
|
||||||
// left to avoid covering the sidebar
|
// left to avoid covering the sidebar
|
||||||
@@ -197,6 +204,7 @@ class MainActivity : Activity() {
|
|||||||
brightness = brightnessProvider,
|
brightness = brightnessProvider,
|
||||||
serverUrl = serverUrlProvider,
|
serverUrl = serverUrlProvider,
|
||||||
wallpaper = wallpaperProvider,
|
wallpaper = wallpaperProvider,
|
||||||
|
onShowTabPanel = { showNativeTabPanel() },
|
||||||
)
|
)
|
||||||
root = FrameLayout(this)
|
root = FrameLayout(this)
|
||||||
setContentView(root)
|
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 引导入口留后续)
|
// 检测通知使用权是否已授予,未授予时打日志提示(UI 引导入口留后续)
|
||||||
// Check whether notification access is granted; log a hint when not
|
// Check whether notification access is granted; log a hint when not
|
||||||
// (the settings UI entry is deferred to a later task)
|
// (the settings UI entry is deferred to a later task)
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ class JsBridge(
|
|||||||
// 壁纸提供接口(base64)
|
// 壁纸提供接口(base64)
|
||||||
// Wallpaper provider (base64)
|
// Wallpaper provider (base64)
|
||||||
private val wallpaper: WallpaperProvider? = null,
|
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()
|
private val gson = com.google.gson.Gson()
|
||||||
|
|
||||||
@@ -212,6 +217,14 @@ class JsBridge(
|
|||||||
Log.d("HearthBridge", "hideWebApps called")
|
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)
|
// 清单内存缓存:首次拉取成功后缓存,后续复用(避免 openWebApp 重复 I/O)
|
||||||
// 空结果(离线失败)不缓存,下次调用重试拉取,避免空清单被永久缓存
|
// 空结果(离线失败)不缓存,下次调用重试拉取,避免空清单被永久缓存
|
||||||
// In-memory manifest cache: cache only on a successful non-empty fetch (avoid
|
// In-memory manifest cache: cache only on a successful non-empty fetch (avoid
|
||||||
|
|||||||
Reference in New Issue
Block a user