在 Kotlin 项目中控制返回键只回退 WebView 的路由,需要重写 onBackPressed() 方法并检查 WebView 的浏览历史。以下是完整的实现方案:
1. 基础实现
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
setupWebView()
webView.loadUrl("https://your-website.com")
}
// 核心:重写返回键逻辑
override fun onBackPressed() {
if (webView.canGoBack()) {
// WebView 有历史记录,回退到上一个页面
webView.goBack()
} else {
// WebView 没有历史记录,执行默认返回操作(退出应用)
super.onBackPressed()
}
}
private fun setupWebView() {
val webSettings = webView.settings
webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true
webView.webViewClient = WebViewClient()
}
}
2. 增强版本(推荐)
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private var backPressedTime: Long = 0
// 配置项
companion object {
private const val EXIT_DELAY = 2000L // 2秒内按两次退出
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
setupWebView()
webView.loadUrl("https://your-website.com")
}
override fun onBackPressed() {
when {
webView.canGoBack() -> {
// 情况1: WebView 有历史记录,回退网页
webView.goBack()
}
supportFragmentManager.backStackEntryCount > 0 -> {
// 情况2: 有 Fragment 在回退栈中,回退 Fragment
supportFragmentManager.popBackStack()
}
else -> {
// 情况3: 没有历史记录,提示双击退出
if (backPressedTime + EXIT_DELAY > System.currentTimeMillis()) {
super.onBackPressed() // 真正退出
} else {
Toast.makeText(this, "再按一次退出应用", Toast.LENGTH_SHORT).show()
backPressedTime = System.currentTimeMillis()
}
}
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
val webSettings = webView.settings
webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true
webSettings.setSupportZoom(true)
webSettings.builtInZoomControls = true
webSettings.displayZoomControls = false
// 确保所有链接都在 WebView 内打开
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
view.loadUrl(request.url.toString())
return true
}
// 兼容旧版本
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
view.loadUrl(url)
return true
}
}
}
}
3. 监听 WebView 历史变化
如果需要更精确的控制,可以监听 WebView 的历史变化:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private var isOnFirstPage = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
setupWebView()
webView.loadUrl("https://your-website.com")
}
override fun onBackPressed() {
if (!isOnFirstPage) {
// 不在首页,回退 WebView
webView.goBack()
} else {
// 在首页,执行退出逻辑
if (backPressedTime + EXIT_DELAY > System.currentTimeMillis()) {
super.onBackPressed()
} else {
Toast.makeText(this, "再按一次退出应用", Toast.LENGTH_SHORT).show()
backPressedTime = System.currentTimeMillis()
}
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
val webSettings = webView.settings
webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true
webView.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
updateBackButtonState()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
updateBackButtonState()
}
}
// 监听 URL 变化
webView.webViewClient = object : WebViewClient() {
override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {
super.doUpdateVisitedHistory(view, url, isReload)
updateBackButtonState()
}
}
}
private fun updateBackButtonState() {
// 检查是否在第一个页面(通常是初始加载的页面)
isOnFirstPage = !webView.canGoBack()
// 可选:更新 UI,比如显示/隐藏返回按钮
updateNavigationUI()
}
private fun updateNavigationUI() {
// 例如:在 Toolbar 中显示/隐藏返回按钮
supportActionBar?.setDisplayHomeAsUpEnabled(!isOnFirstPage)
}
// 处理 Toolbar 的返回按钮
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
}
4. 与 JavaScript 路由配合
如果你的 WebView 加载的是 SPA(单页应用),可能需要与 JavaScript 路由配合:
Kotlin 端:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private var isSPA = true // 是否是单页应用
override fun onBackPressed() {
if (isSPA) {
// 对于 SPA,通过 JavaScript 处理路由回退
webView.evaluateJavascript("""
if (window.history.length > 1) {
window.history.back();
false; // 表示 JavaScript 处理了回退
} else {
true; // 表示需要原生处理回退
}
""".trimIndent()) { result ->
// result 为 "true" 表示需要退出应用
if (result == "true") {
handleAppExit()
}
}
} else {
// 传统多页应用
if (webView.canGoBack()) {
webView.goBack()
} else {
handleAppExit()
}
}
}
private fun handleAppExit() {
if (backPressedTime + EXIT_DELAY > System.currentTimeMillis()) {
super.onBackPressed()
} else {
Toast.makeText(this, "再按一次退出应用", Toast.LENGTH_SHORT).show()
backPressedTime = System.currentTimeMillis()
}
}
// 添加 JavaScript 接口用于双向通信
private fun setupWebView() {
webView.addJavascriptInterface(WebAppInterface(), "Android")
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
// 注入 JavaScript 来监听路由变化
injectRouteListener()
}
}
}
private fun injectRouteListener() {
webView.evaluateJavascript("""
// 监听 SPA 路由变化
if (typeof window.history !== 'undefined') {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
window.Android.onRouteChanged(window.location.href);
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
window.Android.onRouteChanged(window.location.href);
};
window.addEventListener('popstate', function() {
window.Android.onRouteChanged(window.location.href);
});
}
""".trimIndent(), null)
}
inner class WebAppInterface {
@JavascriptInterface
fun onRouteChanged(url: String) {
// JavaScript 通知路由变化
runOnUiThread {
updateBackButtonState()
}
}
@JavascriptInterface
fun canGoBack(canGoBack: Boolean) {
// JavaScript 告知是否可以回退
runOnUiThread {
isOnFirstPage = !canGoBack
}
}
}
}
JavaScript 端配合:
javascript
// 在 JavaScript 中监听路由变化
let currentHistoryLength = window.history.length;
function updateBackState() {
const canGoBack = window.history.length > 1;
if (window.Android) {
window.Android.canGoBack(canGoBack);
}
}
// 监听路由变化
window.addEventListener('popstate', updateBackState);
window.addEventListener('pushState', updateBackState);
window.addEventListener('replaceState', updateBackState);
// 初始状态
updateBackState();
5. 完整的最佳实践方案
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private var backPressedTime: Long = 0
private var isOnHomePage = true
companion object {
private const val EXIT_DELAY = 2000L
private const val HOME_PAGE_URL = "https://your-website.com"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initWebView()
}
override fun onBackPressed() {
when {
webView.canGoBack() -> {
webView.goBack() // 只回退 WebView 路由
}
else -> {
// 双击退出逻辑
if (backPressedTime + EXIT_DELAY > System.currentTimeMillis()) {
super.onBackPressed()
} else {
showExitToast()
backPressedTime = System.currentTimeMillis()
}
}
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
webView = findViewById(R.id.webView)
val webSettings = webView.settings
webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
// 更新首页状态
isOnHomePage = url == HOME_PAGE_URL || !webView.canGoBack()
updateUIState()
}
}
webView.loadUrl(HOME_PAGE_URL)
}
private fun showExitToast() {
Toast.makeText(this, "再按一次退出应用", Toast.LENGTH_SHORT).show()
}
private fun updateUIState() {
// 可以根据状态更新 UI,比如显示/隐藏导航按钮
}
}
关键点总结
- 核心方法 :重写
onBackPressed(),检查webView.canGoBack() - 用户体验:添加双击退出提示,避免误操作
- SPA 支持:对于单页应用,需要与 JavaScript 路由配合
- 状态管理:监听页面变化,准确判断是否在首页
这样就能确保返回键只回退 WebView 的路由,只有在没有历史记录时才执行退出逻辑。