在Android端翻开H5页面,最简略的办法是运用WebView直接加载H5页面的链接。假如H5页面包括的图片、视频等资源较大或网络状况不好时,会呈现加载很久的状况。能够将H5页面的资源打包放到项目的assets文件夹中或许在装置App后下载到设备上,再经过WebView翻开。

本文简略介绍一下如安在安卓端运行本地服务保管网页,再经过WebView翻开已保管的网页。

测验用网页

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

  • 测验用html:
<!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)
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中查看,如下图:

Android — 经过本地服务翻开assets或storage中的网页

完成本地服务并翻开网页

NanoHttpdAndServer是现在比较流行的,能够在Android端完成本地服务的库,接下来分别介绍下如何运用这两个库。

NanoHttpd

增加依靠

在项目app module的build.gradle中的dependencies中增加依靠:

dependencies {
    implementation("org.nanohttpd:nanohttpd:2.3.1") 
}

配置并发动服务器

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

  • NanoHttpdServer
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)
        }
    }
}
  • 示例页面(重复部分省掉)
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中增加如下代码:
buildscript {
    dependencies {
        classpath("com.yanzhenjie.andserver:plugin:2.1.12")
    }
}
  • 在app module下的build.gradle中增加代码,如下:

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
@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))
        }
    }
}
  • 测验页面(重复部分省掉)
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