Android WebView渲染MarkDown,正确加载手机本地图片的方式

1,705 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

一、简介

按照惯例,象征性的给大家贴上官方WebView文档地址,不知道有没有同学发现,android.jarWebView extends MockView, 而系统WebView的源码里面看到WebView extends AbsoluteLayout

我们发现集成自MockView的WebView只是暴露了接口,没有具体实现。 android.jar中的WebView是一个模拟版本,仅用于开发目的,使用模拟版本webview可以在布局窗口中呈现Webview,可以在代码中调用API。在运行时, 将替换WebView。

二、基本使用

1、网络权限不可少

<uses-permission android:name="android.permission.INTERNET"/>

2、其他常用的方法

  1. setJavaScriptEnabled(boolean flag): 是否支持JS使用。
  2. setDefaultTextEncodingName(String encoding): 设置编码。
  3. setCacheMode(int mode): 设置WebView的缓存模式。
  4. setSupportZoom(boolean support): 是否支持缩放。
  5. setAppCacheEnabled(boolean flag): 是否启用缓存模式。
  6. setDomStorageEnabled(boolean flag): 是否开启DOM缓存。
  7. setDatabaseEnabled(boolean flag): 是否开启数据库缓存
  8. getLoadsImagesAutomatically(): 是否支持自动加载图片。
  9. setLoadsImagesAutomatically(boolean flag): 支持自动加载图片
  10. setAllowFileAccess(boolean allow): 是否允许加载本地Html文件。
  11. WebView.enableSlowWholeDocumentDraw(): 智能的选择HTML文档中需要绘制的部分来减少内存占用并提高性能。

3、WebViewClient:回调对应的方法改变网页内容的呈现方式

/**
 * 在开始加载网页时会回调
 */
public void onPageStarted(WebView view, String url, Bitmap favicon) 
/**
 * 在结束加载网页时会回调
 */
public void onPageFinished(WebView view, String url)
/**
 * 拦截 url 跳转,在里边添加点击链接跳转或者操作
 */
public boolean shouldOverrideUrlLoading(WebView view, String url)
/**
 * 加载错误的时候会回调,在这里可做错误处理;比如再请求加载一次;或者提示404的错误页面
 */
public void onReceivedError(WebView view, int errorCode,String description, String failingUrl)
/**
 * 当接收到https错误时,会回调此函数;这里可以做错误处理
 */
public void onReceivedSslError(WebView view, SslErrorHandler handler,SslError error)
/**
 * 在每一次请求资源时,都会通过这个函数来回调
 */
public WebResourceResponse shouldInterceptRequest(WebView view,
        String url) {
    return null;
}

三、如何加载本地图片?

我们在上面介绍到了WebViewClient里面的shouldInterceptRequest方法,每一次请求资源的时候,都会通过这个函数回调。

如果这个时候,我们的WebView接收到从相册或者相机选择完的图片回来,并设置到了Html里面,Markdown触发渲染,当回调到shouldInterceptRequest方法的时候,我们可以在这里面加载图片资源,那么怎么加载呢?往下看👇

shouldInterceptRequest 可以拿到当前请求资源的url,我们需要先判断这个url是不是我们Android手机图片地址的uri,首行需要增加如下代码:

// WebViewClient#shouldInterceptRequest
if(!url.startsWith("file://") && !url.startsWith("content://")){
    return null
}

匹配成功,我们打印出来,可能我们的uri里面含%2F,这个是URLEncoder.encode自带的转义符,我们可以用URLDecoder.decode返回去除转义后的内容,当然,也可以直接用下面这个代码,也是可行的:

val urlString = url.replace("%2F","/")

我们这里再浅谈一下URLDecoder 和 URLEncoder

  1. URLDecoder类包含一个decode(String s,String enc)静态方法,它可以将application/x-www-form-urlencoded MIME字符串转成普通字符串;
  2. URLEncoder类包含一个encode(String s,String enc)静态方法,它可以将普通字符串转换成application/x-www-form-urlencoded MIME字符串。

为了解决乱码,我们会设置enc = "UTF-8"

如果我们和Web前端开发套壳联调H5的时候,如果出现WebView调用vue.js方法出现scrpit.error这种的,基本上就是没有做好转义处理,切记切记。

从上面看完,我们拿到了文件的Uri,那我们可以看到shouldInterceptRequest方法返回的是WebResourceResponse,这个对象是干啥的?

WebResourceResponse:使用指定的MIME类型、字符编码和输入流,构造资源响应,用来加载本地缓存资源。

既然是手机本地的图片文件,我们可以使用ContentResolver#openFileDescriptor的方法,返回一个指向文件的新ParcelFileDescriptor,我们在WebResourceResponse构造方法中需要传一个InputStream,我们可以使用文件描述符,创建一个FileInputStream,我们看到FileInputStream有下面这个构造方法:

public FileInputStream(FileDescriptor fdObj)

有了上面的准备,我们接下来,需要区分一下,file schemecontent scheme,就可以返回WebResourceResponse啦。

1.File Scheme需要做如下处理

// file scheme
if (filePath.startsWith("file://")) {
   // 去掉file://前缀创建File对象
   val file = File(filePath.substring("file://".length))
   // 通过file.extension得到mimeType
   val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)
   val parcelFileDescriptor: ParcelFileDescriptor?
   try {
      parcelFileDescriptor = context.contentResolver.openFileDescriptor(Uri.parse(filePath), "r")
   } catch (e:Exception){
       return null
   }
   if(null == parcelFileDescriptor){
        return null
   }
   // 返回本地的内容的Response
   return WebResourceResponse(mimeType, StandardCharsets.UTF_8.name(), FileInputStream(parcelFileDescriptor.fileDescriptor))
}

2.Content Scheme需要做如下处理

if (filePath.startsWith("content://")) {
   // 获取FileUri
   val fileUri = Uri.parse(filePath)
   // 获取mimeType
   val mimeType = context.contentResolver.getType(fileUri)
   val parcelFileDescriptor: ParcelFileDescriptor?
   try {
      parcelFileDescriptor = context.contentResolver.openFileDescriptor(fileUri, "r")
   }catch (e:Exception){
       return null
   }
   if(null == parcelFileDescriptor){
         return null
   }
   // 返回本地的内容Response
   return WebResourceResponse(mimeType, StandardCharsets.UTF_8.name(), FileInputStream(parcelFileDescriptor.fileDescriptor))
}

成功返回WebResourceResponse,就可以正常渲染本地资源了!