Android — 通过本地服务打开assets或storage中的网页

在Android端打开H5页面,最简单的方法是使用WebView直接加载H5页面的链接。如果H5页面包含的图片、视频等资源较大或网络情况不好时,会出现加载很久的情况。可以将H5页面的资源打包放到项目的assets文件夹中或者在安装App后下载到设备上,再通过WebView打开。

本文简单介绍一下如何在安卓端运行本地服务托管网页,再通过WebView打开已托管的网页。

测试用网页

首先创建一个测试用的html,放在assets文件夹中,并通过代码写入到storage,代码如下:

  • 测试用html:
xml 复制代码
<!DOCTYPE html>
<html lang=zh-CN>
<head>
    <meta charset=utf-8>
    <title>test</title>
    <script>
        function jsCallAndroidWithParams(){
           JsInteractive.jsCallAndroidWithParams('Js params to android')
        }

        function androidCallJsWithParams(arg){
           document.getElementById("message").innerHTML += (arg);
        }
    </script>
</head>
<body>
<div style="position:relative;left:40px;top:100px">
    <p id='message' style="font-size:24px;position:relative;top:20px">receive:</p>
    <button type="button" style="width:280px;height:88px;font-size:24px;position:relative;left:20px"
            onclick="jsCallAndroidWithParams()">
        jsCallAndroidWithParams
    </button>
</div>
<div style="position:relative;left:40px;top:120px">
    <img src="test_icon.jpg" alt="test image">
</div>
<div style="position:relative;left:40px;top:140px">
    <video controls>
        <source src="test_video.mp4" type="video/mp4"/>
    </video>
</div>
</body>
</html>
  • 示例页面(写入storage)
scss 复制代码
class LocalServerExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutLocalServerExampleActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutLocalServerExampleActivityBinding.inflate(layoutInflater).also { setContentView(it.root) }

        copyTestHtmlToStorage()
    }

    private fun copyTestHtmlToStorage() {
        val testHtmlParentDir = File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
            File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), packageName)
        } else {
            File(filesDir, packageName)
        }, "storageweb")
        if (!testHtmlParentDir.exists()) {
            testHtmlParentDir.mkdirs()
        }
        val testHtmlFile = File(testHtmlParentDir, "local_server_example_index.html")
        if (!testHtmlFile.exists()) {
            val inputStream = assets.open("assetsweb/local_server_example_index.html")
            val fileOutputStream = FileOutputStream(testHtmlFile)
            val buffer = ByteArray(1024)
            try {
                var length: Int
                while (inputStream.read(buffer).also { length = it } != -1) {
                    fileOutputStream.write(buffer, 0, length)
                }
                inputStream.close()
                fileOutputStream.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        val testIconFile = File(testHtmlParentDir, "test_icon.jpg")
        if (!testIconFile.exists()) {
            val inputStream = assets.open("assetsweb/test_icon.jpg")
            val fileOutputStream = FileOutputStream(testIconFile)
            val buffer = ByteArray(1024)
            try {
                var length: Int
                while (inputStream.read(buffer).also { length = it } != -1) {
                    fileOutputStream.write(buffer, 0, length)
                }
                inputStream.close()
                fileOutputStream.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        val testVideoFile = File(testHtmlParentDir, "test_video.mp4")
        if (!testVideoFile.exists()) {
            val inputStream = assets.open("assetsweb/test_video.mp4")
            val fileOutputStream = FileOutputStream(testVideoFile)
            val buffer = ByteArray(1024)
            try {
                var length: Int
                while (inputStream.read(buffer).also { length = it } != -1) {
                    fileOutputStream.write(buffer, 0, length)
                }
                inputStream.close()
                fileOutputStream.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
}

写入后可以在Device Explorer中查看,如下图:

实现本地服务并打开网页

NanoHttpdAndServer是目前比较流行的,可以在Android端实现本地服务的库,接下来分别介绍下如何使用这两个库。

NanoHttpd

添加依赖

在项目app module的build.gradle中的dependencies中添加依赖:

scss 复制代码
dependencies { 
    implementation("org.nanohttpd:nanohttpd:2.3.1") 
}

配置并启动服务器

自定义NanoHttpdServer类继承NaoHTTPD类定义端口号,并重写serve()方法,示例代码如下:

  • NanoHttpdServer
kotlin 复制代码
class NanoHttpdServer(private val context: Context) : NanoHTTPD(8080) {

    var openFromStorage = false

    override fun serve(session: IHTTPSession?): Response {
        var mimeType = "*/*"
        session?.run {
            try {
                // 根据链接获取 mimeType
                mimeType = URLConnection.getFileNameMap().getContentTypeFor(session.uri)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        return try {
            val uri = session?.uri
            if (uri == null) {
                super.serve(session)
            } else {
                if (openFromStorage) {
                    // 打开存储空间内的H5资源
                    newChunkedResponse(Response.Status.OK, mimeType, FileInputStream(File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
                        File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.packageName)
                    } else {
                        File(context.filesDir, context.packageName)
                    }, uri)))
                } else {
                    // 打开assets下的H5资源
                    newChunkedResponse(Response.Status.OK, mimeType, context.assets.open(uri.substring(1)))
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            super.serve(session)
        }
    }
}
  • 示例页面(重复部分省略)
kotlin 复制代码
class LocalServerExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutLocalServerExampleActivityBinding

    private var mainWebView: WebView? = null

    private var nanoHttpdServer: NanoHttpdServer? = null

    private val jsInteractive: JsInteractive = object : JsInteractive {

        @JavascriptInterface
        override fun jsCallAndroid() {
        }

        @JavascriptInterface
        override fun jsCallAndroidWithParams(params: String) {
            val message = "receive jsCallAndroidWithParams params:$params"
            showSnakeBar(message)
        }

        @JavascriptInterface
        override fun getPersonJsonArray() {
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutLocalServerExampleActivityBinding.inflate(layoutInflater).also { setContentView(it.root) }
        val insetsController = WindowCompat.getInsetsController(window, window.decorView)
        insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
        insetsController.hide(WindowInsetsCompat.Type.systemBars())

        mainWebView = WebView(this).apply {
            layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
            initWebViewSetting(this)
            binding.webViewContainer.addView(this)
        }

        copyTestHtmlToStorage()

        binding.btnOpenNanohttpd.setOnClickListener {
            (nanoHttpdServer ?: NanoHttpdServer(this)).let {
                nanoHttpdServer = it
                if (!it.isAlive) {
                    it.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true)
                }
            }
        }
        binding.btnCloseNanohttpd.setOnClickListener {
            nanoHttpdServer?.run {
                if (isAlive) {
                    closeAllConnections()
                    mainWebView?.run {
                        clearHistory()
                        loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
                    }
                }
            }
        }
        binding.btnOpenAssetsWebsite.setOnClickListener {
            if (nanoHttpdServer?.isAlive == true) {
                nanoHttpdServer?.openFromStorage = false
                mainWebView?.loadUrl("http://localhost:8080/assetsweb/local_server_example_index.html")
            }
        }
        binding.btnOpenStorageWebsite.setOnClickListener {
            if (nanoHttpdServer?.isAlive == true) {
                nanoHttpdServer?.openFromStorage = true
                mainWebView?.loadUrl("http://localhost:8080/storageweb/local_server_example_index.html")
            }
        }
    }

    override fun onDestroy() {
        destroyWebView(mainWebView)
        nanoHttpdServer?.stop()
        nanoHttpdServer = null
        super.onDestroy()
    }

    @SuppressLint("SetJavaScriptEnabled")
    private fun initWebViewSetting(webView: WebView?) {
        webView?.run {
            settings.cacheMode = WebSettings.LOAD_DEFAULT
            settings.domStorageEnabled = true
            settings.allowContentAccess = true
            settings.allowFileAccess = true
            settings.useWideViewPort = true
            settings.loadWithOverviewMode = true
            settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
            settings.javaScriptEnabled = true
            settings.javaScriptCanOpenWindowsAutomatically = true
            settings.setSupportMultipleWindows(true)

            addJavascriptInterface(jsInteractive, "JsInteractive")
            webChromeClient = object : WebChromeClient() {
                override fun onProgressChanged(view: WebView, newProgress: Int) {
                    super.onProgressChanged(view, newProgress)
                    binding.pbWebLoadProgress.run {
                        post { progress = newProgress }
                        if (newProgress >= 100 && visibility == View.VISIBLE) {
                            postDelayed({ visibility = View.GONE }, 500)
                        }
                    }
                }
            }
            webViewClient = object : WebViewClient() {
                override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                    super.onPageStarted(view, url, favicon)
                    binding.pbWebLoadProgress.run {
                        post { visibility = View.VISIBLE }
                    }
                }

                override fun onPageFinished(view: WebView?, url: String?) {
                    super.onPageFinished(view, url)
                    view?.loadUrl("javascript:androidCallJsWithParams(\"${"message from LocalServerExampleActivity"}\")")
                }

                override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
                    return super.shouldInterceptRequest(view, request)
                }
            }

            WebView.setWebContentsDebuggingEnabled(true)
        }
    }

    private fun showSnakeBar(message: String) {
        runOnUiThread {
            Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
        }
    }

    private fun destroyWebView(webView: WebView?) {
        webView?.run {
            clearHistory()
            loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            binding.webViewContainer.removeView(this)
            destroy()
        }
    }

    ......
}

效果如图:

assets storage

AndServer

添加依赖

  • 在项目下的build.gradle中添加如下代码:
scss 复制代码
buildscript {

    dependencies {
        classpath("com.yanzhenjie.andserver:plugin:2.1.12")
    }
}
  • 在app module下的build.gradle中添加代码,如下:
bash 复制代码
plugins {
    id("kotlin-kapt")
    id 'com.yanzhenjie.andserver'
}

dependencies {
    implementation("com.yanzhenjie.andserver:api:2.1.12")
    kapt("com.yanzhenjie.andserver:processor:2.1.12")
}

配置并启动服务器

自定义AndServerConfig类实现WebConfig接口,重写onConfig()方法添加assets和storage对应的delegate。配置端口并启动服务。示例代码如下:

  • AndServerConfig
kotlin 复制代码
@Config
class AndServerConfig : WebConfig {

    override fun onConfig(context: Context?, delegate: WebConfig.Delegate?) {
        context?.run { delegate?.addWebsite(AssetsWebsite(this, "/assetsweb/")) }
        context?.run {
            delegate?.addWebsite(StorageWebsite(File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
                File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), packageName)
            } else {
                File(filesDir, packageName)
            }, "storageweb").absolutePath))
        }
    }
}
  • 测试页面(重复部分省略)
kotlin 复制代码
class LocalServerExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutLocalServerExampleActivityBinding

    private var mainWebView: WebView? = null

    private var andServer: Server? = null

    ......

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ......
        
        binding.btnOpenAndserver.setOnClickListener {
            (andServer ?: AndServer.webServer(this).port(8080).timeout(10, TimeUnit.SECONDS).build()).let {
                andServer = it
                if (!it.isRunning) {
                    it.startup()
                }
            }
        }
        binding.btnCloseAndserver.setOnClickListener {
            andServer?.run {
                if (isRunning) {
                    shutdown()
                    mainWebView?.run {
                        clearHistory()
                        loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
                    }
                }
            }
        }
        binding.btnOpenAssetsWebsite.setOnClickListener {
            if (andServer?.isRunning == true) {
                mainWebView?.loadUrl("http://localhost:8080/local_server_example_index.html")
            }
        }
        binding.btnOpenStorageWebsite.setOnClickListener {
            if (andServer?.isRunning == true) {
                mainWebView?.loadUrl("http://localhost:8080/local_server_example_index.html")
            }
        }
    }

    ......
}

效果如图:

assets storage

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
androidwork2 小时前
嵌套滚动交互处理总结
android·java·kotlin
fatiaozhang95273 小时前
中兴B860AV1.1强力降级固件包
android·adb·电视盒子·av1·机顶盒rom·魔百盒刷机
橙子199110164 小时前
Kotlin 中的 Object
android·开发语言·kotlin
AD钙奶-lalala8 小时前
android:foregroundServiceType详解
android
大胃粥11 小时前
Android V app 冷启动(13) 焦点窗口更新
android
fatiaozhang952715 小时前
中兴B860AV1.1_晨星MSO9280芯片_4G和8G闪存_TTL-BIN包刷机固件包
android·linux·adb·电视盒子·av1·魔百盒刷机
fatiaozhang952716 小时前
中兴B860AV1.1_MSO9280_降级后开ADB-免刷机破解教程(非刷机)
android·adb·电视盒子·av1·魔百盒刷机·移动魔百盒·魔百盒固件
二流小码农17 小时前
鸿蒙开发:绘制服务卡片
android·ios·harmonyos
微信公众号:AI创造财富17 小时前
adb 查看android 设备的硬盘及存储空间
android·adb